@jongleberry/atel 0.0.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.
Files changed (64) hide show
  1. package/README.md +168 -0
  2. package/dist/cli/append.d.mts +3 -0
  3. package/dist/cli/append.mjs +38 -0
  4. package/dist/cli/args.d.mts +14 -0
  5. package/dist/cli/args.mjs +49 -0
  6. package/dist/cli/context.d.mts +15 -0
  7. package/dist/cli/context.mjs +1 -0
  8. package/dist/cli/credentials.d.mts +6 -0
  9. package/dist/cli/credentials.mjs +45 -0
  10. package/dist/cli/env.d.mts +5 -0
  11. package/dist/cli/env.mjs +21 -0
  12. package/dist/cli/errors.d.mts +3 -0
  13. package/dist/cli/errors.mjs +3 -0
  14. package/dist/cli/get.d.mts +9 -0
  15. package/dist/cli/get.mjs +55 -0
  16. package/dist/cli/index.d.mts +18 -0
  17. package/dist/cli/index.mjs +69 -0
  18. package/dist/cli/output.d.mts +4 -0
  19. package/dist/cli/output.mjs +4 -0
  20. package/dist/cli/patch.d.mts +7 -0
  21. package/dist/cli/patch.mjs +57 -0
  22. package/dist/cli/usage.d.mts +1 -0
  23. package/dist/cli/usage.mjs +21 -0
  24. package/dist/client/append.d.mts +5 -0
  25. package/dist/client/append.mjs +18 -0
  26. package/dist/client/auth.d.mts +26 -0
  27. package/dist/client/auth.mjs +23 -0
  28. package/dist/client/credentials.d.mts +13 -0
  29. package/dist/client/credentials.mjs +19 -0
  30. package/dist/client/errors.d.mts +8 -0
  31. package/dist/client/errors.mjs +13 -0
  32. package/dist/client/http.d.mts +22 -0
  33. package/dist/client/http.mjs +66 -0
  34. package/dist/client/journal.d.mts +24 -0
  35. package/dist/client/journal.mjs +35 -0
  36. package/dist/client/journals.d.mts +18 -0
  37. package/dist/client/journals.mjs +28 -0
  38. package/dist/client/patch.d.mts +6 -0
  39. package/dist/client/patch.mjs +12 -0
  40. package/dist/client/stream.d.mts +30 -0
  41. package/dist/client/stream.mjs +90 -0
  42. package/dist/client/types.d.mts +65 -0
  43. package/dist/client/types.mjs +8 -0
  44. package/dist/format-error.d.mts +6 -0
  45. package/dist/format-error.mjs +15 -0
  46. package/dist/index.d.mts +20 -0
  47. package/dist/index.mjs +16 -0
  48. package/dist/mcp/dispatch.d.mts +3 -0
  49. package/dist/mcp/dispatch.mjs +16 -0
  50. package/dist/mcp/schemas.d.mts +2 -0
  51. package/dist/mcp/schemas.mjs +69 -0
  52. package/dist/mcp/server.d.mts +10 -0
  53. package/dist/mcp/server.mjs +35 -0
  54. package/dist/mcp/tool-append.d.mts +3 -0
  55. package/dist/mcp/tool-append.mjs +17 -0
  56. package/dist/mcp/tool-get.d.mts +13 -0
  57. package/dist/mcp/tool-get.mjs +30 -0
  58. package/dist/mcp/tool-patch.d.mts +5 -0
  59. package/dist/mcp/tool-patch.mjs +22 -0
  60. package/dist/mcp/validate.d.mts +6 -0
  61. package/dist/mcp/validate.mjs +28 -0
  62. package/dist/session.d.mts +24 -0
  63. package/dist/session.mjs +68 -0
  64. package/package.json +45 -0
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # @jongleberry/atel
2
+
3
+ Client library, CLI, and MCP server for [atel](https://github.com/jonathanong/atel) (Agent
4
+ Telemetry) — a telemetry stream for autonomous agents to record a stream of
5
+ consciousness while working unmonitored, then pull it back later to distill
6
+ learnings. This package is the piece you install; it talks to the
7
+ (separately deployed) atel HTTP service over `fetch` — no AWS SDK, no server
8
+ code.
9
+
10
+ ```sh
11
+ npx @jongleberry/atel --help
12
+ ```
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ pnpm add @jongleberry/atel
18
+ ```
19
+
20
+ Requires Node 24+.
21
+
22
+ ## Configuration
23
+
24
+ | Env var | Used by | Meaning |
25
+ | ------------------------ | ------------------- | ------------------------------------------------- |
26
+ | `ATEL_URL` | CLI, MCP | Base URL of the atel server |
27
+ | `ATEL_TOKEN` | CLI, MCP | Telemetry credential (`atl_sk_<credId>_<secret>`) |
28
+ | `ATEL_ADMIN_TOKEN` | CLI (`credentials`) | Admin credential (`atl_admin_<name>_<secret>`) |
29
+ | `CLAUDE_CODE_SESSION_ID` | session resolution | Fallback session id (Claude Code) |
30
+ | `CODEX_THREAD_ID` | session resolution | Fallback session id (Codex) |
31
+
32
+ Telemetry and admin tokens are never interchangeable: `/telemetry` requires
33
+ a telemetry token, `/credentials` requires an admin token.
34
+
35
+ ## CLI
36
+
37
+ ```sh
38
+ atel append '{"note": "found a flaky retry in the payments worker"}'
39
+ atel append # reads JSON from stdin if no argument is given
40
+
41
+ atel get # this session, format json (default)
42
+ atel get --format jsonl
43
+ atel get --format markdown
44
+ atel get --all-sessions --agent codex # across every session for this credential
45
+ atel get --archived false
46
+
47
+ atel patch <id> --archived true
48
+ atel patch <id> --data '{"pr": 7777}' # shallow-merged into existing data
49
+ atel patch --file patches.json # batch: a JSON array of { id, archived?, data? }
50
+
51
+ atel credentials create --name "my laptop" # admin-only; prints the token once
52
+ atel credentials list
53
+ atel credentials delete --name "my laptop" # or --id <id>
54
+
55
+ atel mcp # starts the MCP stdio server (see below)
56
+ ```
57
+
58
+ Notes:
59
+
60
+ - `get` streams the raw response body straight to stdout as it arrives, in
61
+ whatever format was requested — it never buffers the full response first,
62
+ regardless of format.
63
+ - By default, `get` and `append` resolve the session id the same way (see
64
+ [Session resolution](#session-resolution) below). Pass `--all-sessions` to
65
+ `get` to skip that and read across every session visible to the credential.
66
+ - Output is JSON by default; `--format jsonl`/`--format markdown` are only
67
+ meaningful for `get`.
68
+ - Exit code is `0` on success, `1` on any error, with a one-line
69
+ `Error: ...` message on stderr — never a raw stack trace.
70
+
71
+ ## Library
72
+
73
+ ```ts
74
+ import { Telemetries, Telemetry, Auth } from '@jongleberry/atel'
75
+
76
+ // Telemetry — bound to one session + agent
77
+ const telemetry = new Telemetry({ baseUrl, token, agent: 'claude-code' }) // sessionId auto-resolved
78
+ await telemetry.append({ note: 'found a flaky retry' })
79
+ for await (const entry of telemetry.get({ archived: false })) {
80
+ console.log(entry)
81
+ }
82
+
83
+ // Telemetries — cross-session operations
84
+ const telemetries = new Telemetries({ baseUrl, token })
85
+ await telemetries.append({ sessionId: 's1', agent: 'claude-code', data: { note: 'hi' } })
86
+ for await (const entry of telemetries.get({ agent: 'claude-code' })) {
87
+ console.log(entry)
88
+ }
89
+ await telemetries.patch([
90
+ { id: 'entry-1', archived: true },
91
+ { id: 'entry-2', data: { pr: 7777 } }, // shallow merge, not a replace
92
+ ])
93
+
94
+ // Auth — admin-only credential management (never exposed over MCP)
95
+ const auth = new Auth({ baseUrl, adminToken })
96
+ const { token: newToken } = await auth.createCredentials({ name: 'ci-bot' }) // shown once
97
+ await auth.listCredentials()
98
+ await auth.deleteCredentials({ name: 'ci-bot' }) // or { id }
99
+ ```
100
+
101
+ Non-2xx responses throw `AtelError` (`status`, `body`).
102
+
103
+ ### Streaming reads — an honest tradeoff
104
+
105
+ `Telemetry.get()` / `Telemetries.get()` return an `AsyncIterable<TelemetryEntry>`.
106
+ Under the hood, reads default to requesting the server's `jsonl`
107
+ (newline-delimited JSON) wire format and parse it **genuinely
108
+ incrementally**: each line is parsed and yielded as soon as it has fully
109
+ arrived off the `ReadableStream` reader, not after buffering the whole
110
+ response. This is the recommended default and what the library uses
111
+ internally wherever it needs entries (including the MCP server).
112
+
113
+ Pass `format: 'json'` to match the server's default array wire format
114
+ instead — this exists for parity, but a JSON array isn't a valid document
115
+ until its closing `]` arrives, so that path **buffers the full response**
116
+ before yielding anything. It is not genuinely incremental; prefer the
117
+ `jsonl` default.
118
+
119
+ The CLI's `get` command sidesteps this entirely for output purposes: it
120
+ never parses entries, it just relays the raw response body straight to
121
+ stdout as bytes arrive — so it's genuinely incremental for every format
122
+ (`json`, `jsonl`, `markdown` alike), since it never needs to wait for a
123
+ complete, parseable document.
124
+
125
+ ## Session resolution
126
+
127
+ `resolveSessionId(explicit?, options?)` resolves the session id to record
128
+ telemetry under, in order:
129
+
130
+ 1. An explicit id passed by the caller.
131
+ 2. A hook-written state file at `.atel/session.json`, found by
132
+ walking up from the current directory to the nearest ancestor containing
133
+ `.git` (falling back to the current directory itself if none is found). A
134
+ missing or unparsable file is treated as absent, not an error.
135
+ 3. `CLAUDE_CODE_SESSION_ID`.
136
+ 4. `CODEX_THREAD_ID`.
137
+ 5. A generated id (`crypto.randomUUID()`), memoized for the lifetime of the
138
+ current process.
139
+
140
+ Steps 2–4 are re-read fresh on every call (never cached) so a long-lived
141
+ process — like the MCP stdio server — picks up a state file rewritten by a
142
+ `SessionStart` hook (e.g. after `/clear`) without needing to restart. Only
143
+ the last-resort generated id is memoized, so repeated calls with no other
144
+ source agree on the same session for that process's lifetime.
145
+
146
+ ## MCP server
147
+
148
+ ```sh
149
+ atel mcp
150
+ ```
151
+
152
+ A stdio MCP server exposing exactly three tools — deliberately **not**
153
+ credential management, which is CLI/admin-only:
154
+
155
+ - **`telemetry_append`** — `{ data: object, sessionId?: string, agent?: string }` → the created entry.
156
+ - **`telemetry_get`** — `{ sessionId?: string, agent?: string, archived?: boolean, format?: 'json' | 'jsonl' }` → `{ entries: TelemetryEntry[] }`.
157
+ `sessionId` defaults to the current session (same resolution as `telemetry_append`), so "read back what I just recorded" needs no arguments.
158
+ - **`telemetry_patch`** — `{ patches: Array<{ id: string, archived?: boolean, data?: object }> }` → `{ patched: TelemetryEntry[] }`.
159
+
160
+ Reads reuse the same genuinely-incremental client internally, then collect
161
+ into the tool response — MCP tool results aren't naturally streaming to the
162
+ model.
163
+
164
+ Reads config from `ATEL_URL` / `ATEL_TOKEN`.
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { CliContext } from './context.mjs';
2
+ /** `atel append <json>` (or stdin if no argument) — posts one entry. */
3
+ export declare function runAppend(argv: string[], ctx: CliContext): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import { appendEntry } from '../client/append.mjs';
2
+ import { resolveSessionId } from '../session.mjs';
3
+ import { parseArgs, stringFlag } from './args.mjs';
4
+ import { CliError } from './errors.mjs';
5
+ import { telemetryConfigFromEnv } from './env.mjs';
6
+ import { writeLine } from './output.mjs';
7
+ const DEFAULT_AGENT = 'claude-code';
8
+ async function readStdin(stdin) {
9
+ const chunks = [];
10
+ for await (const chunk of stdin) {
11
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
12
+ }
13
+ return Buffer.concat(chunks).toString('utf8');
14
+ }
15
+ /** `atel append <json>` (or stdin if no argument) — posts one entry. */
16
+ export async function runAppend(argv, ctx) {
17
+ const { positional, flags } = parseArgs(argv);
18
+ const raw = positional[0] ?? (await readStdin(ctx.stdin));
19
+ let data;
20
+ try {
21
+ data = JSON.parse(raw);
22
+ }
23
+ catch {
24
+ throw new CliError('append expects a JSON object, either as an argument or piped via stdin.');
25
+ }
26
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
27
+ throw new CliError('append expects a JSON object (not an array or primitive).');
28
+ }
29
+ const config = telemetryConfigFromEnv(ctx.env);
30
+ const sessionId = resolveSessionId(stringFlag(flags, 'session-id'), { env: ctx.env });
31
+ const agent = stringFlag(flags, 'agent') ?? DEFAULT_AGENT;
32
+ const entry = await appendEntry(config, {
33
+ sessionId,
34
+ agent,
35
+ data: data,
36
+ });
37
+ writeLine(ctx.stdout, JSON.stringify(entry));
38
+ }
@@ -0,0 +1,14 @@
1
+ export interface ParsedArgs {
2
+ positional: string[];
3
+ flags: Record<string, string | boolean>;
4
+ }
5
+ /**
6
+ * A minimal, zero-dependency `argv` parser. Supports `--flag value`,
7
+ * `--flag=value`, and bare boolean flags (`--flag` with no following
8
+ * value, or immediately followed by another `--flag`).
9
+ */
10
+ export declare function parseArgs(argv: string[]): ParsedArgs;
11
+ /** Reads a flag expected to be a string; `undefined` if unset or given as a bare boolean. */
12
+ export declare function stringFlag(flags: ParsedArgs['flags'], key: string): string | undefined;
13
+ /** Parses a `true`/`false` flag value (string or bare boolean). Throws on anything else. */
14
+ export declare function parseBooleanFlag(value: string | boolean | undefined): boolean | undefined;
@@ -0,0 +1,49 @@
1
+ import { CliError } from './errors.mjs';
2
+ /**
3
+ * A minimal, zero-dependency `argv` parser. Supports `--flag value`,
4
+ * `--flag=value`, and bare boolean flags (`--flag` with no following
5
+ * value, or immediately followed by another `--flag`).
6
+ */
7
+ export function parseArgs(argv) {
8
+ const positional = [];
9
+ const flags = {};
10
+ for (let i = 0; i < argv.length; i++) {
11
+ const arg = argv[i];
12
+ if (!arg.startsWith('--')) {
13
+ positional.push(arg);
14
+ continue;
15
+ }
16
+ const body = arg.slice(2);
17
+ const eqIndex = body.indexOf('=');
18
+ if (eqIndex !== -1) {
19
+ flags[body.slice(0, eqIndex)] = body.slice(eqIndex + 1);
20
+ continue;
21
+ }
22
+ const next = argv[i + 1];
23
+ if (next !== undefined && !next.startsWith('--')) {
24
+ flags[body] = next;
25
+ i += 1;
26
+ }
27
+ else {
28
+ flags[body] = true;
29
+ }
30
+ }
31
+ return { positional, flags };
32
+ }
33
+ /** Reads a flag expected to be a string; `undefined` if unset or given as a bare boolean. */
34
+ export function stringFlag(flags, key) {
35
+ const value = flags[key];
36
+ return typeof value === 'string' ? value : undefined;
37
+ }
38
+ /** Parses a `true`/`false` flag value (string or bare boolean). Throws on anything else. */
39
+ export function parseBooleanFlag(value) {
40
+ if (value === undefined)
41
+ return undefined;
42
+ if (typeof value === 'boolean')
43
+ return value;
44
+ if (value === 'true')
45
+ return true;
46
+ if (value === 'false')
47
+ return false;
48
+ throw new CliError(`Expected "true" or "false", got: ${value}`);
49
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The I/O the CLI depends on, injected so subcommands are testable by
3
+ * calling their exported functions directly (no shelling out to a built
4
+ * binary, no real stdin/stdout).
5
+ */
6
+ export interface CliContext {
7
+ env: NodeJS.ProcessEnv;
8
+ stdin: AsyncIterable<Buffer | string>;
9
+ stdout: {
10
+ write: (chunk: string) => void;
11
+ };
12
+ stderr: {
13
+ write: (chunk: string) => void;
14
+ };
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { CliContext } from './context.mjs';
2
+ /**
3
+ * `atel credentials create|list|delete` — admin-only, uses
4
+ * `ATEL_ADMIN_TOKEN`. Never exposed over MCP.
5
+ */
6
+ export declare function runCredentials(argv: string[], ctx: CliContext): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import { Auth } from '../client/auth.mjs';
2
+ import { parseArgs, stringFlag } from './args.mjs';
3
+ import { CliError } from './errors.mjs';
4
+ import { adminConfigFromEnv } from './env.mjs';
5
+ import { writeLine } from './output.mjs';
6
+ /**
7
+ * `atel credentials create|list|delete` — admin-only, uses
8
+ * `ATEL_ADMIN_TOKEN`. Never exposed over MCP.
9
+ */
10
+ export async function runCredentials(argv, ctx) {
11
+ const [sub, ...rest] = argv;
12
+ const { flags } = parseArgs(rest);
13
+ const config = adminConfigFromEnv(ctx.env);
14
+ const auth = new Auth({ baseUrl: config.baseUrl, adminToken: config.token });
15
+ switch (sub) {
16
+ case 'create': {
17
+ const name = stringFlag(flags, 'name');
18
+ if (name === undefined)
19
+ throw new CliError('credentials create requires --name <name>.');
20
+ writeLine(ctx.stdout, JSON.stringify(await auth.createCredentials({ name })));
21
+ return;
22
+ }
23
+ case 'list': {
24
+ writeLine(ctx.stdout, JSON.stringify(await auth.listCredentials()));
25
+ return;
26
+ }
27
+ case 'delete': {
28
+ const id = stringFlag(flags, 'id');
29
+ const name = stringFlag(flags, 'name');
30
+ if (id !== undefined) {
31
+ await auth.deleteCredentials({ id });
32
+ }
33
+ else if (name !== undefined) {
34
+ await auth.deleteCredentials({ name });
35
+ }
36
+ else {
37
+ throw new CliError('credentials delete requires --id <id> or --name <name>.');
38
+ }
39
+ writeLine(ctx.stdout, JSON.stringify({ deleted: true }));
40
+ return;
41
+ }
42
+ default:
43
+ throw new CliError(`Unknown credentials subcommand: ${sub ?? '(none)'}. Expected one of: create, list, delete.`);
44
+ }
45
+ }
@@ -0,0 +1,5 @@
1
+ import type { ClientConfig } from '../client/types.mjs';
2
+ /** Telemetry config from env — used by `append`/`get`/`patch`. Never the admin token. */
3
+ export declare function telemetryConfigFromEnv(env: NodeJS.ProcessEnv): ClientConfig;
4
+ /** Admin config from env — used only by `credentials`. Never the telemetry token. */
5
+ export declare function adminConfigFromEnv(env: NodeJS.ProcessEnv): ClientConfig;
@@ -0,0 +1,21 @@
1
+ import { CliError } from './errors.mjs';
2
+ /** Telemetry config from env — used by `append`/`get`/`patch`. Never the admin token. */
3
+ export function telemetryConfigFromEnv(env) {
4
+ const baseUrl = env.ATEL_URL;
5
+ if (!baseUrl)
6
+ throw new CliError('ATEL_URL is not set.');
7
+ const token = env.ATEL_TOKEN;
8
+ if (!token)
9
+ throw new CliError('ATEL_TOKEN is not set.');
10
+ return { baseUrl, token };
11
+ }
12
+ /** Admin config from env — used only by `credentials`. Never the telemetry token. */
13
+ export function adminConfigFromEnv(env) {
14
+ const baseUrl = env.ATEL_URL;
15
+ if (!baseUrl)
16
+ throw new CliError('ATEL_URL is not set.');
17
+ const token = env.ATEL_ADMIN_TOKEN;
18
+ if (!token)
19
+ throw new CliError('ATEL_ADMIN_TOKEN is not set.');
20
+ return { baseUrl, token };
21
+ }
@@ -0,0 +1,3 @@
1
+ /** A user-facing CLI error (bad args, missing env, etc.) — never a raw stack trace. */
2
+ export declare class CliError extends Error {
3
+ }
@@ -0,0 +1,3 @@
1
+ /** A user-facing CLI error (bad args, missing env, etc.) — never a raw stack trace. */
2
+ export class CliError extends Error {
3
+ }
@@ -0,0 +1,9 @@
1
+ import type { CliContext } from './context.mjs';
2
+ /**
3
+ * `atel get [--session-id] [--agent] [--archived] [--format]` —
4
+ * streams the raw response body to stdout as it arrives, in whatever wire
5
+ * format was requested (default `json`). By default this resolves and
6
+ * filters to the current session, same as `append`; pass `--all-sessions`
7
+ * to read across every session visible to this credential.
8
+ */
9
+ export declare function runGet(argv: string[], ctx: CliContext): Promise<void>;
@@ -0,0 +1,55 @@
1
+ import { fetchTelemetriesRaw } from '../client/stream.mjs';
2
+ import { resolveSessionId } from '../session.mjs';
3
+ import { parseArgs, parseBooleanFlag, stringFlag } from './args.mjs';
4
+ import { CliError } from './errors.mjs';
5
+ import { telemetryConfigFromEnv } from './env.mjs';
6
+ const VALID_FORMATS = ['json', 'jsonl', 'markdown'];
7
+ function parseFormatFlag(value) {
8
+ if (value === undefined)
9
+ return undefined;
10
+ if (typeof value === 'string' && VALID_FORMATS.includes(value)) {
11
+ return value;
12
+ }
13
+ throw new CliError(`--format must be one of: ${VALID_FORMATS.join(', ')}`);
14
+ }
15
+ /**
16
+ * `atel get [--session-id] [--agent] [--archived] [--format]` —
17
+ * streams the raw response body to stdout as it arrives, in whatever wire
18
+ * format was requested (default `json`). By default this resolves and
19
+ * filters to the current session, same as `append`; pass `--all-sessions`
20
+ * to read across every session visible to this credential.
21
+ */
22
+ export async function runGet(argv, ctx) {
23
+ const { flags } = parseArgs(argv);
24
+ const config = telemetryConfigFromEnv(ctx.env);
25
+ const format = parseFormatFlag(flags.format) ?? 'json';
26
+ const sessionId = flags['all-sessions'] === true
27
+ ? undefined
28
+ : resolveSessionId(stringFlag(flags, 'session-id'), { env: ctx.env });
29
+ const agent = stringFlag(flags, 'agent');
30
+ const archived = parseBooleanFlag(flags.archived);
31
+ const query = { format };
32
+ if (sessionId !== undefined)
33
+ query.sessionId = sessionId;
34
+ if (agent !== undefined)
35
+ query.agent = agent;
36
+ if (archived !== undefined)
37
+ query.archived = archived;
38
+ const response = await fetchTelemetriesRaw(config, query);
39
+ if (!response.body)
40
+ return;
41
+ const reader = response.body.getReader();
42
+ const decoder = new TextDecoder();
43
+ try {
44
+ for (;;) {
45
+ const { done, value } = await reader.read();
46
+ if (value)
47
+ ctx.stdout.write(decoder.decode(value, { stream: true }));
48
+ if (done)
49
+ break;
50
+ }
51
+ }
52
+ finally {
53
+ reader.releaseLock();
54
+ }
55
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { startMcpServer } from '../mcp/server.mjs';
3
+ export interface RunCliOptions {
4
+ env?: NodeJS.ProcessEnv;
5
+ stdin?: AsyncIterable<Buffer | string>;
6
+ stdout?: {
7
+ write: (chunk: string) => void;
8
+ };
9
+ stderr?: {
10
+ write: (chunk: string) => void;
11
+ };
12
+ startMcpServer?: typeof startMcpServer;
13
+ }
14
+ /**
15
+ * Runs one CLI invocation and resolves to a process exit code (never calls
16
+ * `process.exit` itself, so it's directly testable).
17
+ */
18
+ export declare function runCli(argv: string[], options?: RunCliOptions): Promise<number>;
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { formatError } from '../format-error.mjs';
5
+ import { startMcpServer } from '../mcp/server.mjs';
6
+ import { runAppend } from './append.mjs';
7
+ import { runCredentials } from './credentials.mjs';
8
+ import { CliError } from './errors.mjs';
9
+ import { telemetryConfigFromEnv } from './env.mjs';
10
+ import { runGet } from './get.mjs';
11
+ import { writeLine } from './output.mjs';
12
+ import { runPatch } from './patch.mjs';
13
+ import { USAGE } from './usage.mjs';
14
+ const HELP_COMMANDS = new Set(['help', '--help', '-h']);
15
+ /**
16
+ * Runs one CLI invocation and resolves to a process exit code (never calls
17
+ * `process.exit` itself, so it's directly testable).
18
+ */
19
+ export async function runCli(argv, options = {}) {
20
+ const ctx = {
21
+ env: options.env ?? process.env,
22
+ stdin: options.stdin ?? process.stdin,
23
+ stdout: options.stdout ?? process.stdout,
24
+ stderr: options.stderr ?? process.stderr,
25
+ };
26
+ const [command, ...rest] = argv;
27
+ try {
28
+ if (command === undefined || HELP_COMMANDS.has(command)) {
29
+ writeLine(ctx.stdout, USAGE);
30
+ return 0;
31
+ }
32
+ switch (command) {
33
+ case 'append':
34
+ await runAppend(rest, ctx);
35
+ break;
36
+ case 'get':
37
+ await runGet(rest, ctx);
38
+ break;
39
+ case 'patch':
40
+ await runPatch(rest, ctx);
41
+ break;
42
+ case 'credentials':
43
+ await runCredentials(rest, ctx);
44
+ break;
45
+ case 'mcp':
46
+ await (options.startMcpServer ?? startMcpServer)(telemetryConfigFromEnv(ctx.env));
47
+ break;
48
+ default:
49
+ throw new CliError(`Unknown command: ${command}. Expected one of: append, get, patch, credentials, mcp.`);
50
+ }
51
+ return 0;
52
+ }
53
+ catch (err) {
54
+ writeLine(ctx.stderr, `Error: ${formatError(err)}`);
55
+ return 1;
56
+ }
57
+ }
58
+ /* v8 ignore start -- real process entrypoint; exercised via the built `atel` binary, not under vitest */
59
+ // realpathSync, not a raw string compare: every real install path (npx, a
60
+ // global install, or a workspace-linked bin like this package's own) invokes
61
+ // this file through a symlink. `import.meta.url` resolves through it to the
62
+ // real file, but `process.argv[1]` is the symlink path as typed on the
63
+ // command line — comparing them directly would silently never match and
64
+ // this entrypoint would never run.
65
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
66
+ const code = await runCli(process.argv.slice(2));
67
+ process.exitCode = code;
68
+ }
69
+ /* v8 ignore stop */
@@ -0,0 +1,4 @@
1
+ /** Writes `text` followed by a newline to the given output stream. */
2
+ export declare function writeLine(stream: {
3
+ write: (chunk: string) => void;
4
+ }, text: string): void;
@@ -0,0 +1,4 @@
1
+ /** Writes `text` followed by a newline to the given output stream. */
2
+ export function writeLine(stream, text) {
3
+ stream.write(`${text}\n`);
4
+ }
@@ -0,0 +1,7 @@
1
+ import type { CliContext } from './context.mjs';
2
+ /**
3
+ * `atel patch <id> [--archived] [--data <json>]` for a single
4
+ * patch, or `atel patch --file <path>` to apply a batch from a
5
+ * JSON array file (each element shaped like `{ id, archived?, data? }`).
6
+ */
7
+ export declare function runPatch(argv: string[], ctx: CliContext): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { Telemetries } from '../client/journals.mjs';
3
+ import { parseArgs, parseBooleanFlag, stringFlag } from './args.mjs';
4
+ import { CliError } from './errors.mjs';
5
+ import { telemetryConfigFromEnv } from './env.mjs';
6
+ import { writeLine } from './output.mjs';
7
+ function loadPatchesFromFile(path) {
8
+ let raw;
9
+ try {
10
+ raw = readFileSync(path, 'utf8');
11
+ }
12
+ catch {
13
+ throw new CliError(`Could not read patch file: ${path}`);
14
+ }
15
+ const parsed = JSON.parse(raw);
16
+ if (!Array.isArray(parsed))
17
+ throw new CliError(`Patch file must contain a JSON array of patches: ${path}`);
18
+ return parsed;
19
+ }
20
+ function buildSinglePatch(id, flags) {
21
+ const patch = { id };
22
+ const archived = parseBooleanFlag(flags.archived);
23
+ if (archived !== undefined)
24
+ patch.archived = archived;
25
+ const dataFlag = stringFlag(flags, 'data');
26
+ if (dataFlag !== undefined) {
27
+ try {
28
+ patch.data = JSON.parse(dataFlag);
29
+ }
30
+ catch {
31
+ throw new CliError('--data must be a JSON object.');
32
+ }
33
+ }
34
+ return patch;
35
+ }
36
+ /**
37
+ * `atel patch <id> [--archived] [--data <json>]` for a single
38
+ * patch, or `atel patch --file <path>` to apply a batch from a
39
+ * JSON array file (each element shaped like `{ id, archived?, data? }`).
40
+ */
41
+ export async function runPatch(argv, ctx) {
42
+ const { positional, flags } = parseArgs(argv);
43
+ const file = stringFlag(flags, 'file');
44
+ let patches;
45
+ if (file !== undefined) {
46
+ patches = loadPatchesFromFile(file);
47
+ }
48
+ else {
49
+ const id = positional[0];
50
+ if (id === undefined)
51
+ throw new CliError('patch requires an <id> argument, or --file <path> for a batch.');
52
+ patches = [buildSinglePatch(id, flags)];
53
+ }
54
+ const telemetries = new Telemetries(telemetryConfigFromEnv(ctx.env));
55
+ const result = await telemetries.patch(patches);
56
+ writeLine(ctx.stdout, JSON.stringify(result));
57
+ }
@@ -0,0 +1 @@
1
+ export declare const USAGE = "atel \u2014 telemetry for autonomous agents\n\nUsage:\n atel append <json> Append one entry (or pipe JSON via stdin)\n atel get [flags] Stream entries to stdout\n atel patch <id> [flags] Patch one entry (or --file <path> for a batch)\n atel credentials <subcommand> create | list | delete (admin-only)\n atel mcp Start the MCP stdio server\n\nGet flags:\n --session-id <id> Defaults to the resolved current session\n --agent <name> Filter by agent\n --archived true|false Filter by archived status\n --format json|jsonl|markdown Defaults to json\n --all-sessions Read across every session for this credential\n\nEnv:\n ATEL_URL Server base URL\n ATEL_TOKEN Telemetry credential\n ATEL_ADMIN_TOKEN Admin credential (credentials subcommand only)\n";
@@ -0,0 +1,21 @@
1
+ export const USAGE = `atel — telemetry for autonomous agents
2
+
3
+ Usage:
4
+ atel append <json> Append one entry (or pipe JSON via stdin)
5
+ atel get [flags] Stream entries to stdout
6
+ atel patch <id> [flags] Patch one entry (or --file <path> for a batch)
7
+ atel credentials <subcommand> create | list | delete (admin-only)
8
+ atel mcp Start the MCP stdio server
9
+
10
+ Get flags:
11
+ --session-id <id> Defaults to the resolved current session
12
+ --agent <name> Filter by agent
13
+ --archived true|false Filter by archived status
14
+ --format json|jsonl|markdown Defaults to json
15
+ --all-sessions Read across every session for this credential
16
+
17
+ Env:
18
+ ATEL_URL Server base URL
19
+ ATEL_TOKEN Telemetry credential
20
+ ATEL_ADMIN_TOKEN Admin credential (credentials subcommand only)
21
+ `;
@@ -0,0 +1,5 @@
1
+ import type { AppendInput, ClientConfig, TelemetryEntry } from './types.mjs';
2
+ /** Posts a single telemetry entry. `POST /telemetry` echoes back the created entry. */
3
+ export declare function appendEntry(config: ClientConfig, input: AppendInput): Promise<TelemetryEntry>;
4
+ /** Posts a batch of telemetry entries. `POST /telemetry` echoes back the created entries. */
5
+ export declare function appendEntriesBatch(config: ClientConfig, inputs: AppendInput[]): Promise<TelemetryEntry[]>;