Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions docs/cli/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
title: CLI
description: Query logs, inspect flows, and view stats from your terminal.
---

## Installation

```bash
npm install -g timberlogs-cli
```

**Requirements:** Node.js 20 or later.

## REPL

Running `timberlogs` with no arguments launches an interactive REPL session — a persistent prompt where you can run any command without reinvoking the binary.

```bash
timberlogs
```

```
▲ Timberlogs v1.0.0 ● Acme Corp
logs stats flows flows show whoami login logout config list help exit
────────────────────────────────────────────────────────────
❯ logs --level error --from 24h
❯ stats --group-by source
❯ flows show checkout-abc-123
```

Commands work exactly like the CLI — same flags, same output. Commands that produce a table (`logs`, `stats`, `flows show`) take over the full screen with keyboard navigation; press `q` to return to the prompt.

**Builtins:** `help`, `clear`, `exit`

**History:** Use `↑` / `↓` to navigate previous commands. History is saved to `~/.config/timberlogs/history.json`.

## Authentication

The CLI uses OAuth device flow — no API keys needed.

```bash
timberlogs login
```

This opens your browser to sign in via Timberlogs. Once approved, the CLI stores a session token locally at `~/.config/timberlogs/config.json`.

```bash
timberlogs whoami # Check auth status
timberlogs logout # Remove stored session
```

## Querying Logs

```bash
timberlogs logs # Last hour of logs
timberlogs logs --level error --from 24h # Errors from last 24 hours
timberlogs logs --search "payment" # Full-text search
timberlogs logs --source api-server # Filter by source
timberlogs logs --flow-id checkout-abc # Logs for a specific flow
```

### Filters

| Flag | Description |
|------|-------------|
| `--level` | `debug`, `info`, `warn`, or `error` |
| `--source` | Filter by source name |
| `--env` | Filter by environment |
| `--search` | Full-text search |
| `--from` | Start time (`30m`, `1h`, `24h`, `7d`, or ISO 8601) |
| `--to` | End time |
| `--limit` | Max results (default: 50) |
| `--user-id` | Filter by user ID |
| `--session-id` | Filter by session ID |
| `--flow-id` | Filter by flow ID |
| `--dataset` | Filter by dataset |

### Interactive Mode

The default log view is an interactive table. Use arrow keys to navigate, Enter to expand a log entry, and `q` to quit.

### Output Formats

```bash
timberlogs logs --format json # JSON array
timberlogs logs --format jsonl # One JSON object per line
timberlogs logs --format csv # CSV
timberlogs logs --format text # Plain text
timberlogs logs --format syslog # Syslog format
```

## Flows

```bash
timberlogs flows # List recent flows
timberlogs flows --from 7d # Flows from last 7 days
timberlogs flows show <flow-id> # View flow timeline
```

## Stats

```bash
timberlogs stats # Last 24 hours
timberlogs stats --from 7d # Last 7 days
timberlogs stats --group-by source # Group by source
```

| Flag | Description |
|------|-------------|
| `--from` | Start time (default: `24h`) |
| `--to` | End time |
| `--group-by` | `hour`, `day`, or `source` (default: `day`) |
| `--source` | Filter by source |
| `--env` | Filter by environment |
| `--dataset` | Filter by dataset |

## JSON Mode

All commands support `--json` for machine-readable output. JSON mode is also auto-detected when stdout is piped.

```bash
# Explicit
timberlogs logs --level error --json

# Auto-detected (piped)
timberlogs logs --level error | jq '.logs[].message'
```

## Config

```bash
timberlogs config list # Show current config
timberlogs config reset # Delete config file (prompts for confirmation)
timberlogs config reset --force # Delete without confirmation
```

When running inside the REPL, `config reset` requires `--force` — interactive confirmation prompts are not supported in the REPL session.

Session data is stored at `~/.config/timberlogs/config.json` with `600` permissions. Override the directory with the `TIMBERLOGS_CONFIG_DIR` environment variable.
10 changes: 10 additions & 0 deletions docs/mint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"navigation": [
{
"group": "CLI",
"pages": [
"cli/overview"
]
}
]
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "timberlogs-cli",
"version": "1.0.0",
"version": "1.1.0",
"description": "Official Timberlogs CLI — query logs, inspect flows, view stats from your terminal",
"keywords": [
"timberlogs",
Expand Down
27 changes: 2 additions & 25 deletions src/commands/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,5 @@
import {Text, Box} from 'ink';
import ReplView from '../components/ReplView.js';

export default function Index() {
return (
<Box flexDirection="column">
<Text bold>Timberlogs CLI v{CLI_VERSION}</Text>
<Text> </Text>
<Text>Usage: timberlogs {'<command>'} [options]</Text>
<Text> </Text>
<Text bold>Commands:</Text>
<Text> login Authenticate via browser</Text>
<Text> logout Remove stored session</Text>
<Text> whoami Show current auth status</Text>
<Text> logs Query and search logs</Text>
<Text> flows List all flows</Text>
<Text> flows show {'<id>'} View a flow timeline</Text>
<Text> stats Show log volume and distribution</Text>
<Text> config list Show current config</Text>
<Text> config reset Delete config file</Text>
<Text> </Text>
<Text bold>Global Flags:</Text>
<Text> --json Force JSON output</Text>
<Text> --verbose Show debug info</Text>
<Text> --version, -v Show version</Text>
<Text> --help, -h Show help</Text>
</Box>
);
return <ReplView />;
}
19 changes: 8 additions & 11 deletions src/commands/login.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Text, Box} from 'ink';
import Spinner from 'ink-spinner';
import {useState, useEffect} from 'react';
import {exec} from 'node:child_process';
import {spawn} from 'node:child_process';
import {z} from 'zod';
import {readConfig, writeConfig} from '../lib/config.js';
import {handleError} from '../lib/errors.js';
Expand Down Expand Up @@ -34,16 +34,13 @@ type TokenErrorResponse = {
const POLL_INTERVAL_MS = 5000;

function openBrowser(url: string): void {
const command =
process.platform === 'darwin'
? `open "${url}"`
: process.platform === 'win32'
? `start "${url}"`
: `xdg-open "${url}"`;

exec(command, () => {
// Ignore errors — user can open URL manually
});
if (process.platform === 'win32') {
spawn('cmd', ['/c', 'start', '', url], {windowsHide: true, detached: true, stdio: 'ignore'});
} else if (process.platform === 'darwin') {
spawn('open', [url], {detached: true, stdio: 'ignore'});
} else {
spawn('xdg-open', [url], {detached: true, stdio: 'ignore'});
}
}

export default function Login({options}: Props) {
Expand Down
5 changes: 5 additions & 0 deletions src/commands/repl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import ReplView from '../components/ReplView.js';

export default function Repl() {
return <ReplView />;
}
6 changes: 4 additions & 2 deletions src/components/FlowTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type Props = {
stepCount: number;
durationMs: number;
hasErrors: boolean;
onBack?: () => void;
};

function formatDuration(ms: number): string {
Expand All @@ -26,10 +27,11 @@ function formatDataInline(data: Record<string, unknown> | undefined, maxWidth: n
return result.length > maxWidth ? result.slice(0, maxWidth - 1) + '…' : result;
}

export default function FlowTimeline({flowId, logs, stepCount, durationMs, hasErrors}: Props) {
export default function FlowTimeline({flowId, logs, stepCount, durationMs, hasErrors, onBack}: Props) {
useInput((input) => {
if (input === 'q') {
process.exit(0);
if (onBack) onBack();
else process.exit(0);
}
});

Expand Down
6 changes: 4 additions & 2 deletions src/components/LogTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ type Props = {
logs: LogEntry[];
pagination: LogsResponse['pagination'];
filterSummary?: string;
onBack?: () => void;
};

export default function LogTable({logs, pagination, filterSummary}: Props) {
export default function LogTable({logs, pagination, filterSummary, onBack}: Props) {
const [cursor, setCursor] = useState(0);
const [expanded, setExpanded] = useState<number | null>(null);

Expand All @@ -33,7 +34,8 @@ export default function LogTable({logs, pagination, filterSummary}: Props) {
} else if (key.return) {
setExpanded(cursor);
} else if (input === 'q') {
process.exit(0);
if (onBack) onBack();
else process.exit(0);
}
});

Expand Down
19 changes: 19 additions & 0 deletions src/components/ReplOutput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Box, Text} from 'ink';
import type {ReactNode} from 'react';

export type ReplEntry =
| {kind: 'command'; input: string}
| {kind: 'output'; content: ReactNode}
| {kind: 'error'; message: string};

export default function ReplOutput({entry}: {entry: ReplEntry}) {
if (entry.kind === 'command') {
return <Text dimColor>❯ {entry.input}</Text>;
}

if (entry.kind === 'error') {
return <Text color="red">✗ {entry.message}</Text>;
}

return <Box>{entry.content as ReactNode}</Box>;
}
67 changes: 67 additions & 0 deletions src/components/ReplPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {Box, Text, useInput} from 'ink';
import TextInput from 'ink-text-input';
import {useState} from 'react';

type Props = {
history: string[];
onSubmit: (input: string) => void;
disabled: boolean;
};

export default function ReplPrompt({history, onSubmit, disabled}: Props) {
const [value, setValue] = useState('');
const [historyIndex, setHistoryIndex] = useState(-1);
const [draft, setDraft] = useState('');

useInput((_, key) => {
if (disabled) return;
if (key.upArrow) {
if (history.length === 0) return;
if (historyIndex === -1) {
setDraft(value);
const idx = history.length - 1;
setHistoryIndex(idx);
setValue(history[idx]!);
} else if (historyIndex > 0) {
const idx = historyIndex - 1;
setHistoryIndex(idx);
setValue(history[idx]!);
}
} else if (key.downArrow) {
if (historyIndex === -1) return;
if (historyIndex < history.length - 1) {
const idx = historyIndex + 1;
setHistoryIndex(idx);
setValue(history[idx]!);
} else {
setHistoryIndex(-1);
setValue(draft);
}
}
});

function handleChange(val: string) {
if (disabled) return;
setValue(val);
if (historyIndex !== -1) setHistoryIndex(-1);
}

function handleSubmit(val: string) {
if (disabled) return;
setValue('');
setHistoryIndex(-1);
setDraft('');
onSubmit(val);
}

return (
<Box>
<Text color="green">❯ </Text>
{disabled ? (
<Text dimColor>...</Text>
) : (
<TextInput value={value} onChange={handleChange} onSubmit={handleSubmit} />
)}
</Box>
);
}
Loading