octarin-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -133
- package/dist/args.js +1 -3
- package/dist/client.js +3 -70
- package/dist/index.js +13 -117
- package/dist/output.js +4 -49
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# octarin-cli
|
|
2
2
|
|
|
3
3
|
The per-user CLI for [Octarin](https://octarin.ai) AI-usage analytics (binary:
|
|
4
|
-
`octarin`). One
|
|
5
|
-
centralized, `npx`-based install path for AI-coding capture, plus a scoped,
|
|
6
|
-
**read-only SQL** layer over your own data.
|
|
4
|
+
`octarin`). One centralized, `npx`-based install path for AI-coding capture.
|
|
7
5
|
|
|
8
6
|
```bash
|
|
9
7
|
npx octarin-cli@latest init <oct_key> # personal capture install (this machine)
|
|
10
8
|
npx octarin-cli@latest init-repo <org/project> # team install (commits config, opens a PR)
|
|
11
9
|
npx octarin-cli@latest login # authorize a machine (team installs)
|
|
12
|
-
npx octarin-cli@latest sql query "<SELECT ...>" # read-only SQL over your own data
|
|
13
10
|
```
|
|
14
11
|
|
|
15
12
|
No `curl | bash`, no sha256-verify dance — version pinning (`@latest` /
|
|
@@ -17,7 +14,7 @@ No `curl | bash`, no sha256-verify dance — version pinning (`@latest` /
|
|
|
17
14
|
(uses the built-in `fetch`); `init`'s history import additionally uses the
|
|
18
15
|
system `python3`.
|
|
19
16
|
|
|
20
|
-
##
|
|
17
|
+
## Commands
|
|
21
18
|
|
|
22
19
|
`init` and `init-repo` install the Octarin capture hooks for Claude Code,
|
|
23
20
|
Cursor, and Codex entirely from the package's **bundled, version-pinned assets**
|
|
@@ -36,6 +33,9 @@ npx octarin-cli@latest init oct_xxx --backfill off # skip the history import
|
|
|
36
33
|
npx octarin-cli@latest init oct_xxx --tools cursor # only Cursor
|
|
37
34
|
```
|
|
38
35
|
|
|
36
|
+
Options: `--tools <claude,cursor,codex>`, `--backfill <30d|90d|all|off>`,
|
|
37
|
+
`--base-url <url>`, `--url <ingest-url>`, `--port <port>`.
|
|
38
|
+
|
|
39
39
|
### `octarin init-repo <org/project>`
|
|
40
40
|
|
|
41
41
|
Team install. Run at a repo root: commits shared hook config + the public
|
|
@@ -48,147 +48,25 @@ Device-code browser flow that mints a per-user ingest key into
|
|
|
48
48
|
`~/.octarin/octarin.env`. Reads the project from `.octarin/project` (or
|
|
49
49
|
`--project <org/project>`).
|
|
50
50
|
|
|
51
|
-
## SQL
|
|
52
|
-
|
|
53
|
-
A scoped, **read-only SQL** layer over your own data. Run SELECTs against your
|
|
54
|
-
traces, spans, sessions, and events — or pipe machine-readable JSON into `jq`
|
|
55
|
-
and your agents.
|
|
56
|
-
|
|
57
|
-
The CLI holds **only your API key**, never database credentials. All query
|
|
58
|
-
safety (single read-only SELECT, table allowlist, no `system.*`) and tenant
|
|
59
|
-
scoping (every result is filtered to your organization's projects) are enforced
|
|
60
|
-
**server-side** by the Octarin API. A query can only ever see your own data.
|
|
61
|
-
|
|
62
|
-
## Auth (SQL)
|
|
63
|
-
|
|
64
|
-
The `sql` commands authenticate with a read-only **query key** (`oct_...`) — a
|
|
65
|
-
key that can run scoped SELECTs over your org's data but can NEVER ingest. This
|
|
66
|
-
is a *distinct* key from the write-only **ingest key** used for capture (the one
|
|
67
|
-
`octarin init` writes to `~/.octarin/octarin.env`); an ingest key is rejected
|
|
68
|
-
here with a `403`.
|
|
69
|
-
|
|
70
|
-
Mint a query key in the dashboard under **Settings → CLI / Query keys** (it is
|
|
71
|
-
shown exactly once — copy it then). Then either:
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
export OCTARIN_API_KEY=oct_xxxxxxxxxxxxxxxxxxxx
|
|
75
|
-
octarin sql query "SELECT count() FROM spans"
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
or pass it per-command:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
octarin sql query "SELECT count() FROM spans" --api-key oct_xxxx
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Commands
|
|
85
|
-
|
|
86
|
-
### `octarin sql schema`
|
|
87
|
-
|
|
88
|
-
List the tables you can query and their columns.
|
|
89
|
-
|
|
90
|
-
```bash
|
|
91
|
-
octarin sql schema # human-readable tables
|
|
92
|
-
octarin sql schema --json # machine-readable JSON
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Queryable tables: `spans`, `traces`, `session_insights`, `span_vectors`,
|
|
96
|
-
`events` (all in the `octarin` database; you may write them qualified as
|
|
97
|
-
`octarin.spans` or bare as `spans`).
|
|
98
|
-
|
|
99
|
-
### `octarin sql query "<SELECT ...>"`
|
|
100
|
-
|
|
101
|
-
Run a single **read-only** SELECT. Default output is a readable ASCII table;
|
|
102
|
-
`--json` emits structured JSON on stdout.
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
# Top models by token-bearing spans
|
|
106
|
-
octarin sql query "SELECT model, count() AS c FROM spans GROUP BY model ORDER BY c DESC"
|
|
107
|
-
|
|
108
|
-
# Cap the rows
|
|
109
|
-
octarin sql query "SELECT * FROM traces ORDER BY start_time DESC" --limit 20
|
|
110
|
-
|
|
111
|
-
# JSON for piping
|
|
112
|
-
octarin sql query "SELECT model, count() AS c FROM spans GROUP BY model" --json
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
#### Pipe into `jq`
|
|
116
|
-
|
|
117
|
-
Structured output goes to **stdout**; all progress/log/error messages go to
|
|
118
|
-
**stderr**, so JSON pipes cleanly:
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
octarin sql query "SELECT model, count() AS c FROM spans GROUP BY model" --json \
|
|
122
|
-
| jq '.rows[] | {model: .[0], spans: .[1]}'
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
The response shape is:
|
|
126
|
-
|
|
127
|
-
```json
|
|
128
|
-
{
|
|
129
|
-
"columns": ["model", "c"],
|
|
130
|
-
"rows": [["claude-opus-4-8", 7594], ["claude-sonnet-4-6", 6188]],
|
|
131
|
-
"row_count": 2
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## Options
|
|
136
|
-
|
|
137
|
-
| Option | Env var | Default | Description |
|
|
138
|
-
|--------|---------|---------|-------------|
|
|
139
|
-
| `--api-key <key>` | `OCTARIN_API_KEY` | — | Octarin **query** key (`oct_...`). Required. |
|
|
140
|
-
| `--base-url <url>` | `OCTARIN_BASE_URL` | `https://api.octarin.ai` | API base URL. |
|
|
141
|
-
| `--port <port>` | — | — | Override the port (self-host parity). |
|
|
142
|
-
| `--limit <N>` | — | server cap | Max rows (server caps at 10000). |
|
|
143
|
-
| `--json` | — | off | Emit JSON on stdout instead of a table. |
|
|
144
|
-
| `-h`, `--help` | — | — | Show help (on every command). |
|
|
145
|
-
| `--version` | — | — | Print the CLI version. |
|
|
146
|
-
|
|
147
|
-
Self-host example:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
octarin sql schema --base-url http://localhost:8000
|
|
151
|
-
octarin sql schema --port 8000 # shorthand for http://localhost:8000
|
|
152
|
-
```
|
|
153
|
-
|
|
154
51
|
## Agent-friendly I/O
|
|
155
52
|
|
|
156
|
-
- **stdout** —
|
|
53
|
+
- **stdout** — reserved for structured output.
|
|
157
54
|
- **stderr** — all human/progress/error messages.
|
|
158
|
-
- **exit code** — `0` on success, non-zero on failure (`2` for usage
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
This makes the CLI safe to drive from scripts and LLM agents: capture stdout,
|
|
162
|
-
check the exit code, read stderr for diagnostics.
|
|
163
|
-
|
|
164
|
-
## What you can and can't query
|
|
165
|
-
|
|
166
|
-
You can run exactly **one read-only `SELECT`** (a `WITH ... SELECT` CTE is fine)
|
|
167
|
-
over the allowlisted `octarin.*` tables. The server rejects, with a clear `400`:
|
|
168
|
-
|
|
169
|
-
- anything that isn't a single SELECT (no `INSERT/UPDATE/DELETE/ALTER/DROP/...`),
|
|
170
|
-
- multiple `;`-separated statements,
|
|
171
|
-
- references to other databases or tables — especially `system.*` and
|
|
172
|
-
`information_schema.*`,
|
|
173
|
-
- table functions (`url`, `file`, `remote`, `s3`, `numbers`, ...).
|
|
174
|
-
|
|
175
|
-
Every result is automatically scoped to your organization's projects — you
|
|
176
|
-
never need to (and cannot) widen it.
|
|
55
|
+
- **exit code** — `0` on success, non-zero on failure (`2` for usage errors,
|
|
56
|
+
`1` for runtime errors).
|
|
177
57
|
|
|
178
58
|
## Development
|
|
179
59
|
|
|
180
60
|
```bash
|
|
181
61
|
npm install
|
|
182
|
-
npm run build # tsc -> dist/
|
|
183
|
-
node dist/index.js
|
|
62
|
+
npm run build # copy-assets (from ../clients) + tsc -> dist/
|
|
63
|
+
node dist/index.js --help
|
|
184
64
|
# or run the local checkout via npx:
|
|
185
|
-
npx .
|
|
65
|
+
npx . init --help
|
|
186
66
|
```
|
|
187
67
|
|
|
188
68
|
## Publishing (maintainers)
|
|
189
69
|
|
|
190
|
-
This package is publish-ready but not yet published. To release:
|
|
191
|
-
|
|
192
70
|
```bash
|
|
193
71
|
npm version <patch|minor|major>
|
|
194
72
|
npm publish # runs prepublishOnly -> npm run build
|
package/dist/args.js
CHANGED
|
@@ -11,10 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** Flags that consume the following token as their value. */
|
|
13
13
|
const VALUE_FLAGS = new Set([
|
|
14
|
-
"api-key",
|
|
15
14
|
"base-url",
|
|
16
15
|
"port",
|
|
17
|
-
"limit",
|
|
18
16
|
// init / init-repo / login
|
|
19
17
|
"key",
|
|
20
18
|
"url",
|
|
@@ -24,7 +22,7 @@ const VALUE_FLAGS = new Set([
|
|
|
24
22
|
"device-label",
|
|
25
23
|
]);
|
|
26
24
|
/** Boolean flags that never consume a value. */
|
|
27
|
-
const BOOL_FLAGS = new Set(["
|
|
25
|
+
const BOOL_FLAGS = new Set(["help"]);
|
|
28
26
|
/** Map of short aliases to their long-flag names. */
|
|
29
27
|
const ALIASES = { h: "help" };
|
|
30
28
|
/** Parse an argv tail (after the node + script entries) into positionals+flags. */
|
package/dist/client.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Shared CLI helpers: the user-facing error type and base-URL resolution.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the API key — never ClickHouse credentials; all scoping/safety is server-side.
|
|
7
|
-
*
|
|
8
|
-
* Errors map the gateway's JSON envelope (`{ "error": { code, message } }`) to a
|
|
9
|
-
* thrown `CliError` so the command layer can print a clean message to stderr and
|
|
10
|
-
* exit non-zero.
|
|
4
|
+
* Used by every command (`init`, `init-repo`, `login`) to map failures to exit
|
|
5
|
+
* codes and to resolve the Octarin API base URL from flags/env.
|
|
11
6
|
*/
|
|
12
7
|
/** A user-facing error carrying a process exit code. */
|
|
13
8
|
export class CliError extends Error {
|
|
@@ -41,65 +36,3 @@ export function resolveBaseUrl(opts) {
|
|
|
41
36
|
}
|
|
42
37
|
return base.replace(/\/+$/, "");
|
|
43
38
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Resolve the API key from explicit flag or env, or throw a clear `CliError`.
|
|
46
|
-
*
|
|
47
|
-
* Precedence: `--api-key` > `OCTARIN_API_KEY`.
|
|
48
|
-
*/
|
|
49
|
-
export function resolveApiKey(apiKeyFlag) {
|
|
50
|
-
const key = apiKeyFlag || process.env.OCTARIN_API_KEY;
|
|
51
|
-
if (!key) {
|
|
52
|
-
throw new CliError("No API key. Set OCTARIN_API_KEY or pass --api-key <oct_...>.", 2);
|
|
53
|
-
}
|
|
54
|
-
return key;
|
|
55
|
-
}
|
|
56
|
-
/** Perform a request to the gateway and parse its JSON, mapping errors. */
|
|
57
|
-
async function request(cfg, path, init) {
|
|
58
|
-
const url = `${cfg.baseUrl}${path}`;
|
|
59
|
-
let resp;
|
|
60
|
-
try {
|
|
61
|
-
resp = await fetch(url, {
|
|
62
|
-
...init,
|
|
63
|
-
headers: {
|
|
64
|
-
Authorization: `Bearer ${cfg.apiKey}`,
|
|
65
|
-
"Content-Type": "application/json",
|
|
66
|
-
...(init.headers || {}),
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
catch (err) {
|
|
71
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
72
|
-
throw new CliError(`Could not reach ${url}: ${detail}`, 1);
|
|
73
|
-
}
|
|
74
|
-
const text = await resp.text();
|
|
75
|
-
let body = undefined;
|
|
76
|
-
if (text) {
|
|
77
|
-
try {
|
|
78
|
-
body = JSON.parse(text);
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// Non-JSON body (e.g. a proxy 502 page). Fall through to status handling.
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (!resp.ok) {
|
|
85
|
-
const envelope = body;
|
|
86
|
-
const msg = envelope?.error?.message || text || `HTTP ${resp.status}`;
|
|
87
|
-
const code = envelope?.error?.code;
|
|
88
|
-
throw new CliError(`Request failed (${resp.status}${code ? ` ${code}` : ""}): ${msg}`, 1);
|
|
89
|
-
}
|
|
90
|
-
return body;
|
|
91
|
-
}
|
|
92
|
-
/** POST a SELECT to `/v1/sql/query`. */
|
|
93
|
-
export function runQuery(cfg, query, limit) {
|
|
94
|
-
const payload = { query };
|
|
95
|
-
if (limit !== undefined)
|
|
96
|
-
payload.limit = limit;
|
|
97
|
-
return request(cfg, "/v1/sql/query", {
|
|
98
|
-
method: "POST",
|
|
99
|
-
body: JSON.stringify(payload),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
/** GET the queryable schema from `/v1/sql/schema`. */
|
|
103
|
-
export function getSchema(cfg) {
|
|
104
|
-
return request(cfg, "/v1/sql/schema", { method: "GET" });
|
|
105
|
-
}
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* `octarin` —
|
|
3
|
+
* `octarin` — the per-user CLI for Octarin AI-usage analytics.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* octarin
|
|
7
|
-
* octarin
|
|
5
|
+
* Install AI-coding capture and authorize a machine:
|
|
6
|
+
* octarin init <oct_key> personal capture install (this machine)
|
|
7
|
+
* octarin init-repo <org/project> team install (commits config, opens a PR)
|
|
8
|
+
* octarin login authorize a machine via browser sign-in
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* for self-host parity). The CLI holds only the API key — never DB credentials;
|
|
12
|
-
* the server enforces read-only access, the table allowlist, and tenant scoping.
|
|
13
|
-
*
|
|
14
|
-
* I/O contract (so `--json | jq` pipes cleanly): structured results on stdout,
|
|
15
|
-
* all human/progress/error messages on stderr, non-zero exit on failure.
|
|
10
|
+
* All human/progress/error messages go to stderr; the exit code signals success
|
|
11
|
+
* (0) or failure (non-zero), so the CLI is safe to drive from scripts/agents.
|
|
16
12
|
*/
|
|
17
|
-
import { flagBool,
|
|
18
|
-
import { CliError
|
|
13
|
+
import { flagBool, parseArgs } from "./args.js";
|
|
14
|
+
import { CliError } from "./client.js";
|
|
19
15
|
import { cmdLogin } from "./login.js";
|
|
20
16
|
import { cmdInit } from "./init.js";
|
|
21
17
|
import { cmdInitRepo } from "./init_repo.js";
|
|
22
|
-
import { logErr
|
|
23
|
-
const VERSION = "0.
|
|
18
|
+
import { logErr } from "./output.js";
|
|
19
|
+
const VERSION = "0.3.0";
|
|
24
20
|
const ROOT_HELP = `octarin — per-user CLI for Octarin AI-usage analytics
|
|
25
21
|
|
|
26
22
|
USAGE
|
|
27
|
-
octarin <command> [
|
|
23
|
+
octarin <command> [args] [options]
|
|
28
24
|
|
|
29
25
|
COMMANDS
|
|
30
26
|
init <oct_key> Install AI-coding capture on THIS machine (personal).
|
|
@@ -35,120 +31,23 @@ COMMANDS
|
|
|
35
31
|
login Authorize this machine via a browser sign-in.
|
|
36
32
|
Writes ~/.octarin/octarin.env so capture hooks
|
|
37
33
|
pick up your per-user ingest key.
|
|
38
|
-
sql query "<SELECT ...>" Run a read-only SELECT against your octarin.* tables
|
|
39
|
-
sql schema List the queryable tables and their columns
|
|
40
34
|
|
|
41
35
|
GLOBAL OPTIONS
|
|
42
|
-
--api-key <key> Octarin API key (oct_...). Or set OCTARIN_API_KEY.
|
|
43
|
-
(Not used by \`octarin login\` — that's how you GET a key.)
|
|
44
36
|
--base-url <url> API base URL. Or set OCTARIN_BASE_URL.
|
|
45
37
|
Default: https://api.octarin.ai
|
|
46
38
|
--port <port> Override the port (self-host parity).
|
|
47
|
-
|
|
48
|
-
-h, --help Show help.
|
|
39
|
+
-h, --help Show help (on every command).
|
|
49
40
|
--version Show the CLI version.
|
|
50
41
|
|
|
51
42
|
EXAMPLES
|
|
52
43
|
octarin init oct_xxx # personal capture install
|
|
53
44
|
octarin init-repo nace/default # team install (opens a PR)
|
|
54
45
|
octarin login # per-user auth (team installs)
|
|
55
|
-
octarin sql schema
|
|
56
|
-
octarin sql query "SELECT model, count() FROM spans GROUP BY model"
|
|
57
46
|
`;
|
|
58
|
-
const SQL_HELP = `octarin sql — query your own Octarin data (read-only)
|
|
59
|
-
|
|
60
|
-
USAGE
|
|
61
|
-
octarin sql query "<SELECT ...>" [--json] [--limit N]
|
|
62
|
-
octarin sql schema [--json]
|
|
63
|
-
|
|
64
|
-
OPTIONS
|
|
65
|
-
--limit N Max rows to return (server caps at 10000).
|
|
66
|
-
--json Emit JSON on stdout instead of a table.
|
|
67
|
-
--api-key <key> Octarin API key (oct_...). Or set OCTARIN_API_KEY.
|
|
68
|
-
--base-url <url> API base URL. Or set OCTARIN_BASE_URL.
|
|
69
|
-
--port <port> Override the port (self-host parity).
|
|
70
|
-
-h, --help Show this help.
|
|
71
|
-
|
|
72
|
-
NOTES
|
|
73
|
-
Only a single read-only SELECT over your octarin.* tables is allowed
|
|
74
|
-
(spans, traces, session_insights, span_vectors, events). The server enforces
|
|
75
|
-
the table allowlist and scopes every result to your organization's projects.
|
|
76
|
-
`;
|
|
77
|
-
/** Build the client config (auth + base URL) from parsed flags. */
|
|
78
|
-
function buildConfig(flags) {
|
|
79
|
-
const apiKey = resolveApiKey(flagStr(flags, "api-key"));
|
|
80
|
-
const baseUrl = resolveBaseUrl({
|
|
81
|
-
baseUrl: flagStr(flags, "base-url"),
|
|
82
|
-
port: flagStr(flags, "port"),
|
|
83
|
-
});
|
|
84
|
-
return { apiKey, baseUrl };
|
|
85
|
-
}
|
|
86
|
-
/** `octarin sql query "<SELECT>"` */
|
|
87
|
-
async function cmdSqlQuery(positionals, flags) {
|
|
88
|
-
const query = positionals[0];
|
|
89
|
-
if (!query) {
|
|
90
|
-
throw new CliError('Missing query. Usage: octarin sql query "<SELECT ...>"', 2);
|
|
91
|
-
}
|
|
92
|
-
let limit;
|
|
93
|
-
const limitStr = flagStr(flags, "limit");
|
|
94
|
-
if (limitStr !== undefined) {
|
|
95
|
-
const n = Number(limitStr);
|
|
96
|
-
if (!Number.isInteger(n) || n < 1) {
|
|
97
|
-
throw new CliError(`--limit must be a positive integer, got "${limitStr}".`, 2);
|
|
98
|
-
}
|
|
99
|
-
limit = n;
|
|
100
|
-
}
|
|
101
|
-
const cfg = buildConfig(flags);
|
|
102
|
-
logErr(`> querying ${cfg.baseUrl} ...`);
|
|
103
|
-
const result = await runQuery(cfg, query, limit);
|
|
104
|
-
if (flagBool(flags, "json")) {
|
|
105
|
-
outJson(result);
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
outTable(result.columns, result.rows);
|
|
109
|
-
logErr(`(${result.row_count} row${result.row_count === 1 ? "" : "s"})`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
/** `octarin sql schema` */
|
|
113
|
-
async function cmdSqlSchema(flags) {
|
|
114
|
-
const cfg = buildConfig(flags);
|
|
115
|
-
logErr(`> fetching schema from ${cfg.baseUrl} ...`);
|
|
116
|
-
const schema = await getSchema(cfg);
|
|
117
|
-
if (flagBool(flags, "json")) {
|
|
118
|
-
outJson(schema);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
// Human-readable: one block per table.
|
|
122
|
-
for (const table of schema.tables) {
|
|
123
|
-
outTable(["column", "type"], table.columns.map((c) => [c.name, c.type]));
|
|
124
|
-
logErr(` ${table.qualified_name} — ${table.columns.length} columns\n`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
/** Dispatch the `sql` command group. */
|
|
128
|
-
async function cmdSql(positionals, flags) {
|
|
129
|
-
const sub = positionals[0];
|
|
130
|
-
const rest = positionals.slice(1);
|
|
131
|
-
if (flagBool(flags, "help") || !sub) {
|
|
132
|
-
logErr(SQL_HELP);
|
|
133
|
-
if (!sub)
|
|
134
|
-
throw new CliError("Missing subcommand (query | schema).", 2);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
switch (sub) {
|
|
138
|
-
case "query":
|
|
139
|
-
return cmdSqlQuery(rest, flags);
|
|
140
|
-
case "schema":
|
|
141
|
-
return cmdSqlSchema(flags);
|
|
142
|
-
default:
|
|
143
|
-
logErr(SQL_HELP);
|
|
144
|
-
throw new CliError(`Unknown sql subcommand: ${sub}`, 2);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
47
|
/** Entry point: parse argv, route to a command, map errors to exit codes. */
|
|
148
48
|
async function main() {
|
|
149
49
|
const { positionals, flags } = parseArgs(process.argv.slice(2));
|
|
150
50
|
if (flags.version === true) {
|
|
151
|
-
// Version is structured-ish output; print to stdout.
|
|
152
51
|
process.stdout.write(VERSION + "\n");
|
|
153
52
|
return;
|
|
154
53
|
}
|
|
@@ -170,9 +69,6 @@ async function main() {
|
|
|
170
69
|
case "login":
|
|
171
70
|
await cmdLogin(rest, flags);
|
|
172
71
|
break;
|
|
173
|
-
case "sql":
|
|
174
|
-
await cmdSql(rest, flags);
|
|
175
|
-
break;
|
|
176
72
|
case "help":
|
|
177
73
|
logErr(ROOT_HELP);
|
|
178
74
|
break;
|
package/dist/output.js
CHANGED
|
@@ -1,56 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Output
|
|
2
|
+
* Output helper.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* That way `octarin sql query "..." --json | jq` receives clean JSON on stdout
|
|
8
|
-
* with no log noise, and a non-zero exit code signals failure to the caller.
|
|
4
|
+
* The CLI keeps stdout clean and writes all human/progress/diagnostic messages
|
|
5
|
+
* to stderr, so callers (scripts, agents) can rely on the exit code and capture
|
|
6
|
+
* stderr for context without parsing noise.
|
|
9
7
|
*/
|
|
10
8
|
/** Write a line to stderr (human/progress/diagnostic messages). */
|
|
11
9
|
export function logErr(message) {
|
|
12
10
|
process.stderr.write(message + "\n");
|
|
13
11
|
}
|
|
14
|
-
/** Write structured output to stdout (the machine-readable result). */
|
|
15
|
-
export function out(text) {
|
|
16
|
-
process.stdout.write(text + "\n");
|
|
17
|
-
}
|
|
18
|
-
/** Pretty-print a value as indented JSON to stdout. */
|
|
19
|
-
export function outJson(value) {
|
|
20
|
-
out(JSON.stringify(value, null, 2));
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Render rows as a monospace ASCII table to stdout.
|
|
24
|
-
*
|
|
25
|
-
* `columns` are the header names; `rows` are arrays aligned to them. Cells are
|
|
26
|
-
* stringified (null → "" , objects → compact JSON) and each column is padded to
|
|
27
|
-
* its widest cell. Designed for human reading in a terminal — use `--json` for
|
|
28
|
-
* anything a program will parse.
|
|
29
|
-
*/
|
|
30
|
-
export function outTable(columns, rows) {
|
|
31
|
-
if (columns.length === 0) {
|
|
32
|
-
out("(no columns)");
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const cells = rows.map((r) => r.map(formatCell));
|
|
36
|
-
const widths = columns.map((c, i) => Math.max(c.length, ...cells.map((row) => (row[i] ?? "").length), 0));
|
|
37
|
-
const sep = "+" + widths.map((w) => "-".repeat(w + 2)).join("+") + "+";
|
|
38
|
-
const renderRow = (vals) => "| " + vals.map((v, i) => v.padEnd(widths[i])).join(" | ") + " |";
|
|
39
|
-
out(sep);
|
|
40
|
-
out(renderRow(columns));
|
|
41
|
-
out(sep);
|
|
42
|
-
for (const row of cells) {
|
|
43
|
-
// Pad short rows so the table stays rectangular.
|
|
44
|
-
const padded = columns.map((_, i) => row[i] ?? "");
|
|
45
|
-
out(renderRow(padded));
|
|
46
|
-
}
|
|
47
|
-
out(sep);
|
|
48
|
-
}
|
|
49
|
-
/** Stringify a single cell value for the ASCII table. */
|
|
50
|
-
function formatCell(value) {
|
|
51
|
-
if (value === null || value === undefined)
|
|
52
|
-
return "";
|
|
53
|
-
if (typeof value === "object")
|
|
54
|
-
return JSON.stringify(value);
|
|
55
|
-
return String(value);
|
|
56
|
-
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "octarin-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Octarin's per-user CLI: install AI-coding capture (`octarin init` / `init-repo`)
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Octarin's per-user CLI: install AI-coding capture (`octarin init` / `init-repo`) and authorize a machine (`octarin login`). Streams your Claude Code / Cursor / Codex usage to your Octarin workspace.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"octarin",
|
|
7
|
-
"sql",
|
|
8
7
|
"cli",
|
|
9
8
|
"observability",
|
|
10
9
|
"llm",
|