octarin-cli 0.2.0 → 0.3.1

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 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
- ## Capture install
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,154 +48,12 @@ 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** — only the structured result (the table, or `--json` JSON).
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/auth
159
- errors, `1` for request/execution errors).
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.
177
-
178
- ## Development
179
-
180
- ```bash
181
- npm install
182
- npm run build # tsc -> dist/
183
- node dist/index.js sql schema
184
- # or run the local checkout via npx:
185
- npx . sql schema
186
- ```
187
-
188
- ## Publishing (maintainers)
189
-
190
- This package is publish-ready but not yet published. To release:
191
-
192
- ```bash
193
- npm version <patch|minor|major>
194
- npm publish # runs prepublishOnly -> npm run build
195
- ```
196
-
197
- (`files` ships `dist/`, the bundled `assets/`, and this README; `prepublishOnly`
198
- rebuilds, which re-stages `assets/` from `../clients` via `copy-assets`.)
55
+ - **exit code** — `0` on success, non-zero on failure (`2` for usage errors,
56
+ `1` for runtime errors).
199
57
 
200
58
  ## License
201
59
 
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(["json", "help"]);
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
- * HTTP client for the Octarin SQL gateway.
2
+ * Shared CLI helpers: the user-facing error type and base-URL resolution.
3
3
  *
4
- * Resolves auth + base URL from flags/env, then calls `POST /v1/sql/query` and
5
- * `GET /v1/sql/schema` with the API key as a Bearer token. The CLI holds ONLY
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` — a thin, agent-friendly CLI over the Octarin SQL gateway.
3
+ * `octarin` — the per-user CLI for Octarin AI-usage analytics.
4
4
  *
5
- * Two commands today, mirroring `lmnr-cli`'s stable noun-verb surface:
6
- * octarin sql query "<SELECT ...>" [--json] [--limit N]
7
- * octarin sql schema [--json]
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
- * Auth: `OCTARIN_API_KEY` env var or `--api-key`. Base URL: `https://api.octarin.ai`
10
- * by default, overridable with `--base-url` / `OCTARIN_BASE_URL` (and `--port`
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, flagStr, parseArgs } from "./args.js";
18
- import { CliError, getSchema, resolveApiKey, resolveBaseUrl, runQuery, } from "./client.js";
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, outJson, outTable } from "./output.js";
23
- const VERSION = "0.2.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> [subcommand] [args] [options]
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
- --json Emit machine-readable JSON on stdout (pipe to jq).
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 helpers — strict stdout/stderr separation for agent-friendly piping.
2
+ * Output helper.
3
3
  *
4
- * Contract (mirrors Laminar's `lmnr-cli`): *structured* output (the actual
5
- * query result / schema, as JSON or a rendered table) goes to **stdout**;
6
- * *everything else* progress, hints, warnings, errors — goes to **stderr**.
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.2.0",
4
- "description": "Octarin's per-user CLI: install AI-coding capture (`octarin init` / `init-repo`), authorize a machine (`octarin login`), and run scoped read-only SQL over your own data. Never holds database credentials.",
3
+ "version": "0.3.1",
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",