@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.
- package/README.md +168 -0
- package/dist/cli/append.d.mts +3 -0
- package/dist/cli/append.mjs +38 -0
- package/dist/cli/args.d.mts +14 -0
- package/dist/cli/args.mjs +49 -0
- package/dist/cli/context.d.mts +15 -0
- package/dist/cli/context.mjs +1 -0
- package/dist/cli/credentials.d.mts +6 -0
- package/dist/cli/credentials.mjs +45 -0
- package/dist/cli/env.d.mts +5 -0
- package/dist/cli/env.mjs +21 -0
- package/dist/cli/errors.d.mts +3 -0
- package/dist/cli/errors.mjs +3 -0
- package/dist/cli/get.d.mts +9 -0
- package/dist/cli/get.mjs +55 -0
- package/dist/cli/index.d.mts +18 -0
- package/dist/cli/index.mjs +69 -0
- package/dist/cli/output.d.mts +4 -0
- package/dist/cli/output.mjs +4 -0
- package/dist/cli/patch.d.mts +7 -0
- package/dist/cli/patch.mjs +57 -0
- package/dist/cli/usage.d.mts +1 -0
- package/dist/cli/usage.mjs +21 -0
- package/dist/client/append.d.mts +5 -0
- package/dist/client/append.mjs +18 -0
- package/dist/client/auth.d.mts +26 -0
- package/dist/client/auth.mjs +23 -0
- package/dist/client/credentials.d.mts +13 -0
- package/dist/client/credentials.mjs +19 -0
- package/dist/client/errors.d.mts +8 -0
- package/dist/client/errors.mjs +13 -0
- package/dist/client/http.d.mts +22 -0
- package/dist/client/http.mjs +66 -0
- package/dist/client/journal.d.mts +24 -0
- package/dist/client/journal.mjs +35 -0
- package/dist/client/journals.d.mts +18 -0
- package/dist/client/journals.mjs +28 -0
- package/dist/client/patch.d.mts +6 -0
- package/dist/client/patch.mjs +12 -0
- package/dist/client/stream.d.mts +30 -0
- package/dist/client/stream.mjs +90 -0
- package/dist/client/types.d.mts +65 -0
- package/dist/client/types.mjs +8 -0
- package/dist/format-error.d.mts +6 -0
- package/dist/format-error.mjs +15 -0
- package/dist/index.d.mts +20 -0
- package/dist/index.mjs +16 -0
- package/dist/mcp/dispatch.d.mts +3 -0
- package/dist/mcp/dispatch.mjs +16 -0
- package/dist/mcp/schemas.d.mts +2 -0
- package/dist/mcp/schemas.mjs +69 -0
- package/dist/mcp/server.d.mts +10 -0
- package/dist/mcp/server.mjs +35 -0
- package/dist/mcp/tool-append.d.mts +3 -0
- package/dist/mcp/tool-append.mjs +17 -0
- package/dist/mcp/tool-get.d.mts +13 -0
- package/dist/mcp/tool-get.mjs +30 -0
- package/dist/mcp/tool-patch.d.mts +5 -0
- package/dist/mcp/tool-patch.mjs +22 -0
- package/dist/mcp/validate.d.mts +6 -0
- package/dist/mcp/validate.mjs +28 -0
- package/dist/session.d.mts +24 -0
- package/dist/session.mjs +68 -0
- 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,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,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;
|
package/dist/cli/env.mjs
ADDED
|
@@ -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,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>;
|
package/dist/cli/get.mjs
ADDED
|
@@ -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,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[]>;
|