@agentage/cli 0.32.0 → 0.34.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 CHANGED
@@ -94,6 +94,14 @@ too. Account-channel provisioning still needs an interactive `agentage setup` se
94
94
  - [`docs/reference.md`](docs/reference.md) - every command and flag, the full sync
95
95
  options, MCP details, and environment variables.
96
96
 
97
+ ## Telemetry (off by default)
98
+
99
+ Nothing leaves your machine unless you ask for it. The CLI and its local helper ship with
100
+ `@agentage/observability`, which stays inert until `OTEL_EXPORTER_OTLP_ENDPOINT` points at a
101
+ collector you choose - then the CLI reports as `agentage-cli` and the helper as `agentage-daemon`
102
+ (override with `OTEL_SERVICE_NAME`). Every line it writes goes to stderr, so `--json` output and the
103
+ helper's MCP wire stay clean.
104
+
97
105
  ## Development
98
106
 
99
107
  ```bash
package/dist/cli.js CHANGED
@@ -1,54 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import chalk from 'chalk';
3
- import { Command } from 'commander';
4
- import { registerDaemon } from './commands/daemon/daemon-cmd.js';
5
- import { registerMemory } from './commands/memory/memory.js';
6
- import { registerSetup } from './commands/auth/setup.js';
7
- import { registerStatus } from './commands/status/status.js';
8
- import { registerUpdate } from './commands/update/update.js';
9
- import { registerVault } from './commands/vault/vault.js';
10
- import { disableDaemon } from './lib/daemon/daemon-pref.js';
11
- import { refreshUpdateCache, updateHint } from './lib/update/update-cache.js';
12
- import { VERSION } from './utils/version.js';
2
+ // The bin is a loader, nothing else. @agentage/observability instruments node:http, fetch and MCP
3
+ // tool registration through module hooks, and a hook only sees a module imported after it is
4
+ // registered - so the bootstrap runs first and the program is imported dynamically behind it.
13
5
  // Dependency-free guard so the message survives even if deps fail to parse on old Node.
14
6
  const nodeMajor = Number(process.versions.node.split('.')[0]);
15
7
  if (nodeMajor < 22) {
16
8
  process.stderr.write(`agentage requires Node.js >= 22 (you have v${process.versions.node})\n`);
17
9
  process.exit(1);
18
10
  }
19
- const program = new Command();
20
- program
21
- .name('agentage')
22
- .description('The offline-first terminal client for agentage Memory')
23
- .version(VERSION)
24
- .option('--no-daemon', 'run memory verbs in-process instead of via the daemon');
25
- program.addHelpText('after', '\nNew here? Run: agentage vault add <name> --local, or agentage setup to connect an account.');
26
- program.hook('preAction', () => {
27
- if (program.opts().daemon === false)
28
- disableDaemon();
29
- });
30
- // After any command (except the update-aware ones, which report richer state themselves), print a
31
- // one-line hint from the cache, then kick a fire-and-forget refresh. Never awaited, never throws -
32
- // the command output is already done and the cache-only read keeps this instant. The hint is
33
- // suppressed under --json so it never corrupts machine-readable output.
34
- program.hook('postAction', (_thisCommand, actionCommand) => {
35
- const name = actionCommand.name();
36
- if (name === 'status' || name === 'update')
37
- return;
38
- if (!actionCommand.opts()['json']) {
39
- const hint = updateHint();
40
- if (hint)
41
- console.error(chalk.dim(hint)); // stderr: never corrupt piped stdout
42
- }
43
- void refreshUpdateCache();
44
- });
45
- registerSetup(program);
46
- registerStatus(program);
47
- registerVault(program);
48
- registerMemory(program);
49
- registerDaemon(program);
50
- registerUpdate(program);
51
- program.parseAsync().catch((err) => {
52
- console.error(err instanceof Error ? err.message : String(err));
53
- process.exit(1);
54
- });
11
+ const { CLI_SERVICE, startObservability } = await import('./observability.js');
12
+ await startObservability(CLI_SERVICE);
13
+ const { run } = await import('./program.js');
14
+ await run();
15
+ export {};
@@ -0,0 +1,8 @@
1
+ export declare const isEaddrinuse: (err: unknown) => boolean;
2
+ export declare const createStateCleanup: (remove: () => void) => {
3
+ markOwned: () => void;
4
+ cleanup: () => void;
5
+ };
6
+ export declare const safeReschedule: (steps: Array<() => void>, onError: (msg: string) => void) => void;
7
+ export declare const mcpEnabled: (env?: NodeJS.ProcessEnv) => boolean;
8
+ export declare const boot: () => void;
@@ -0,0 +1,113 @@
1
+ import { unwatchFile, watchFile } from 'node:fs';
2
+ import { createClientProvider } from './client-provider.js';
3
+ import { EADDRINUSE_EXIT_CODE, generateDaemonToken, removePidFile, removePortFile, removeTokenFile, resolvePort, writePidFile, writePortFile, writeTokenFile, } from './lifecycle.js';
4
+ import { createDaemonServer } from './server.js';
5
+ import { loadLocalMemoryServer } from '../mcp/local-server.js';
6
+ import { vaultsJsonPath } from '../lib/vault/vaults.js';
7
+ import { createDiscoverWatcher } from '../sync/discover/watcher.js';
8
+ import { createSyncManager } from '../sync/git/manager.js';
9
+ import { VERSION } from '../utils/version.js';
10
+ export const isEaddrinuse = (err) => typeof err === 'object' && err !== null && err.code === 'EADDRINUSE';
11
+ // Gate state-file cleanup on ownership: a loser of an autostart race must never wipe the winner's
12
+ // pid/port/token files. Only the process that actually wrote them may remove them.
13
+ export const createStateCleanup = (remove) => {
14
+ let owned = false;
15
+ return {
16
+ markOwned: () => {
17
+ owned = true;
18
+ },
19
+ cleanup: () => {
20
+ if (owned)
21
+ remove();
22
+ },
23
+ };
24
+ };
25
+ // Run each reschedule independently: a transiently-invalid config edit must not crash the daemon or
26
+ // stop the others rescheduling; the throwing one keeps its last-good schedule.
27
+ export const safeReschedule = (steps, onError) => {
28
+ for (const step of steps) {
29
+ try {
30
+ step();
31
+ }
32
+ catch (err) {
33
+ onError(err instanceof Error ? err.message : String(err));
34
+ }
35
+ }
36
+ };
37
+ // MCP is served at /mcp by default; AGENTAGE_DAEMON_NO_MCP=1 leaves buildMcpServer unset so the
38
+ // endpoint 404s (the `daemon start --no-mcp` flag sets this on the spawned process).
39
+ export const mcpEnabled = (env = process.env) => env['AGENTAGE_DAEMON_NO_MCP'] !== '1';
40
+ // Unset/empty/invalid -> undefined (the watcher's defaults apply); the watcher floors low values.
41
+ const envInt = (name) => {
42
+ const raw = process.env[name];
43
+ if (!raw)
44
+ return undefined;
45
+ const v = Number(raw);
46
+ return Number.isFinite(v) && v >= 0 ? v : undefined;
47
+ };
48
+ const state = createStateCleanup(() => {
49
+ removePidFile();
50
+ removePortFile();
51
+ removeTokenFile();
52
+ });
53
+ // The detached, long-lived engine host: one loopback HTTP server that owns a single in-process
54
+ // engine and serialises every vault mutation, avoiding concurrent git index.lock collisions. It
55
+ // runs the git sync loop and reschedules on config change.
56
+ const main = async () => {
57
+ const port = resolvePort();
58
+ const authToken = generateDaemonToken();
59
+ const git = createSyncManager();
60
+ const discover = createDiscoverWatcher({
61
+ log: (msg) => console.log(`[discover] ${msg}`),
62
+ debounceMs: envInt('AGENTAGE_DISCOVER_DEBOUNCE_MS'),
63
+ pollMs: envInt('AGENTAGE_DISCOVER_POLL_MS'),
64
+ });
65
+ const server = createDaemonServer({
66
+ getClient: createClientProvider(),
67
+ buildMcpServer: mcpEnabled() ? loadLocalMemoryServer : undefined,
68
+ sync: {
69
+ status: () => ({ ...git.status(), discover: discover.status() }),
70
+ runNow: (vault) => git.runNow(vault),
71
+ },
72
+ authToken,
73
+ version: VERSION,
74
+ });
75
+ await server.start(port);
76
+ writePidFile(process.pid);
77
+ writePortFile(port);
78
+ writeTokenFile(authToken);
79
+ state.markOwned();
80
+ const reschedule = () => safeReschedule([() => git.reschedule(), () => discover.reschedule()], (msg) => console.error(`[daemon] reschedule failed: ${msg}`));
81
+ reschedule();
82
+ const configPath = vaultsJsonPath();
83
+ watchFile(configPath, { interval: 2000 }, reschedule);
84
+ const shutdown = () => {
85
+ unwatchFile(configPath);
86
+ git.stop();
87
+ discover.stop();
88
+ server.stop().finally(() => {
89
+ state.cleanup();
90
+ process.exit(0);
91
+ });
92
+ };
93
+ process.on('SIGTERM', shutdown);
94
+ process.on('SIGINT', shutdown);
95
+ };
96
+ // Boot the real daemon. Invoked by the bin (src/daemon-entry.ts) once the observability bootstrap
97
+ // has run; importing this module for tests must never start a server.
98
+ export const boot = () => {
99
+ process.on('uncaughtException', (err) => {
100
+ console.error(`[daemon] uncaught: ${err instanceof Error ? err.message : String(err)}`);
101
+ state.cleanup();
102
+ process.exit(1);
103
+ });
104
+ main().catch((err) => {
105
+ if (isEaddrinuse(err)) {
106
+ // Another daemon owns the port + our state files: exit distinctly, touch nothing.
107
+ process.exit(EADDRINUSE_EXIT_CODE);
108
+ }
109
+ console.error(err instanceof Error ? err.message : String(err));
110
+ state.cleanup();
111
+ process.exit(1);
112
+ });
113
+ };
@@ -1,7 +1 @@
1
- export declare const isEaddrinuse: (err: unknown) => boolean;
2
- export declare const createStateCleanup: (remove: () => void) => {
3
- markOwned: () => void;
4
- cleanup: () => void;
5
- };
6
- export declare const safeReschedule: (steps: Array<() => void>, onError: (msg: string) => void) => void;
7
- export declare const mcpEnabled: (env?: NodeJS.ProcessEnv) => boolean;
1
+ export {};
@@ -1,118 +1,9 @@
1
- import { unwatchFile, watchFile } from 'node:fs';
2
- import { fileURLToPath } from 'node:url';
3
- import { createClientProvider } from './daemon/client-provider.js';
4
- import { EADDRINUSE_EXIT_CODE, generateDaemonToken, removePidFile, removePortFile, removeTokenFile, resolvePort, writePidFile, writePortFile, writeTokenFile, } from './daemon/lifecycle.js';
5
- import { createDaemonServer } from './daemon/server.js';
6
- import { loadLocalMemoryServer } from './mcp/local-server.js';
7
- import { vaultsJsonPath } from './lib/vault/vaults.js';
8
- import { createDiscoverWatcher } from './sync/discover/watcher.js';
9
- import { createSyncManager } from './sync/git/manager.js';
10
- import { VERSION } from './utils/version.js';
11
- export const isEaddrinuse = (err) => typeof err === 'object' && err !== null && err.code === 'EADDRINUSE';
12
- // Gate state-file cleanup on ownership: a loser of an autostart race must never wipe the winner's
13
- // pid/port/token files. Only the process that actually wrote them may remove them.
14
- export const createStateCleanup = (remove) => {
15
- let owned = false;
16
- return {
17
- markOwned: () => {
18
- owned = true;
19
- },
20
- cleanup: () => {
21
- if (owned)
22
- remove();
23
- },
24
- };
25
- };
26
- // Run each reschedule independently: a transiently-invalid config edit must not crash the daemon or
27
- // stop the others rescheduling; the throwing one keeps its last-good schedule.
28
- export const safeReschedule = (steps, onError) => {
29
- for (const step of steps) {
30
- try {
31
- step();
32
- }
33
- catch (err) {
34
- onError(err instanceof Error ? err.message : String(err));
35
- }
36
- }
37
- };
38
- // MCP is served at /mcp by default; AGENTAGE_DAEMON_NO_MCP=1 leaves buildMcpServer unset so the
39
- // endpoint 404s (the `daemon start --no-mcp` flag sets this on the spawned process).
40
- export const mcpEnabled = (env = process.env) => env['AGENTAGE_DAEMON_NO_MCP'] !== '1';
41
- // Unset/empty/invalid -> undefined (the watcher's defaults apply); the watcher floors low values.
42
- const envInt = (name) => {
43
- const raw = process.env[name];
44
- if (!raw)
45
- return undefined;
46
- const v = Number(raw);
47
- return Number.isFinite(v) && v >= 0 ? v : undefined;
48
- };
49
- const state = createStateCleanup(() => {
50
- removePidFile();
51
- removePortFile();
52
- removeTokenFile();
53
- });
54
- // The detached, long-lived engine host: one loopback HTTP server that owns a single in-process
55
- // engine and serialises every vault mutation, avoiding concurrent git index.lock collisions. It
56
- // runs the git sync loop and reschedules on config change.
57
- const main = async () => {
58
- const port = resolvePort();
59
- const authToken = generateDaemonToken();
60
- const git = createSyncManager();
61
- const discover = createDiscoverWatcher({
62
- log: (msg) => console.log(`[discover] ${msg}`),
63
- debounceMs: envInt('AGENTAGE_DISCOVER_DEBOUNCE_MS'),
64
- pollMs: envInt('AGENTAGE_DISCOVER_POLL_MS'),
65
- });
66
- const server = createDaemonServer({
67
- getClient: createClientProvider(),
68
- buildMcpServer: mcpEnabled() ? loadLocalMemoryServer : undefined,
69
- sync: {
70
- status: () => ({ ...git.status(), discover: discover.status() }),
71
- runNow: (vault) => git.runNow(vault),
72
- },
73
- authToken,
74
- version: VERSION,
75
- });
76
- await server.start(port);
77
- writePidFile(process.pid);
78
- writePortFile(port);
79
- writeTokenFile(authToken);
80
- state.markOwned();
81
- const reschedule = () => safeReschedule([() => git.reschedule(), () => discover.reschedule()], (msg) => console.error(`[daemon] reschedule failed: ${msg}`));
82
- reschedule();
83
- const configPath = vaultsJsonPath();
84
- watchFile(configPath, { interval: 2000 }, reschedule);
85
- const shutdown = () => {
86
- unwatchFile(configPath);
87
- git.stop();
88
- discover.stop();
89
- server.stop().finally(() => {
90
- state.cleanup();
91
- process.exit(0);
92
- });
93
- };
94
- process.on('SIGTERM', shutdown);
95
- process.on('SIGINT', shutdown);
96
- };
97
- // Only self-invoke when run directly (spawnDaemon's `node daemon-entry.js`); importing for tests
98
- // must not boot a daemon.
99
- const invokedDirectly = () => {
100
- const entry = process.argv[1];
101
- return !!entry && fileURLToPath(import.meta.url) === entry;
102
- };
103
- if (invokedDirectly()) {
104
- process.on('uncaughtException', (err) => {
105
- console.error(`[daemon] uncaught: ${err instanceof Error ? err.message : String(err)}`);
106
- state.cleanup();
107
- process.exit(1);
108
- });
109
- main().catch((err) => {
110
- if (isEaddrinuse(err)) {
111
- // Another daemon owns the port + our state files: exit distinctly, touch nothing.
112
- process.exit(EADDRINUSE_EXIT_CODE);
113
- }
114
- console.error(err instanceof Error ? err.message : String(err));
115
- state.cleanup();
116
- process.exit(1);
117
- });
118
- }
1
+ // The daemon's bin, a loader and nothing else: @agentage/observability instruments node:http and
2
+ // MCP tool registration through module hooks that only see modules imported after they register, so
3
+ // the bootstrap runs before the daemon itself is loaded. The path stays src/daemon-entry.ts because
4
+ // spawnDaemon resolves dist/daemon-entry.js.
5
+ const { DAEMON_SERVICE, startObservability } = await import('./observability.js');
6
+ await startObservability(DAEMON_SERVICE);
7
+ const { boot } = await import('./daemon/entry.js');
8
+ boot();
9
+ export {};
@@ -2,6 +2,7 @@ import { spawn as spawnChild } from 'node:child_process';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import chalk from 'chalk';
4
4
  import { EADDRINUSE_EXIT_CODE, readDaemonToken, resolvePort } from '../../daemon/lifecycle.js';
5
+ import { daemonServiceName } from '../../observability.js';
5
6
  import { VERSION } from '../../utils/version.js';
6
7
  const base = (port) => `http://127.0.0.1:${port}`;
7
8
  // The 0600 token file is the daemon's auth: read it fresh per call so a restart's new token is
@@ -97,6 +98,8 @@ export const spawnDaemon = async (port, opts = {}) => {
97
98
  env: {
98
99
  ...process.env,
99
100
  AGENTAGE_DAEMON_PORT: String(port),
101
+ // Its own service in telemetry: never inherit the name we defaulted into the CLI's env.
102
+ OTEL_SERVICE_NAME: daemonServiceName(),
100
103
  ...(opts.noMcp ? { AGENTAGE_DAEMON_NO_MCP: '1' } : {}),
101
104
  },
102
105
  });
@@ -0,0 +1,4 @@
1
+ export declare const CLI_SERVICE = "agentage-cli";
2
+ export declare const DAEMON_SERVICE = "agentage-daemon";
3
+ export declare const daemonServiceName: () => string;
4
+ export declare const startObservability: (service: string) => Promise<void>;
@@ -0,0 +1,35 @@
1
+ // Telemetry for the CLI and its daemon, through @agentage/observability. A real dependency here
2
+ // (unlike @agentage/server-memory, which must stay npx-light): `agentage` is installed once,
3
+ // globally. It is inert until OTEL_EXPORTER_OTLP_ENDPOINT names a collector, and every line it
4
+ // writes goes to stderr - stdout stays the machine-readable channel (`--json`, the daemon's
5
+ // JSON-RPC wire).
6
+ // Captured before startObservability() can overwrite it, so the daemon we spawn can tell a name the
7
+ // user chose from the default we injected.
8
+ const userServiceName = process.env['OTEL_SERVICE_NAME'];
9
+ export const CLI_SERVICE = 'agentage-cli';
10
+ export const DAEMON_SERVICE = 'agentage-daemon';
11
+ // What OTEL_SERVICE_NAME the spawned daemon should run under: the user's, else our own default.
12
+ export const daemonServiceName = () => userServiceName || DAEMON_SERVICE;
13
+ // Guarded even though the kit is a dependency: a half-installed global must degrade to a working
14
+ // CLI, never to a stack trace on every command.
15
+ export const startObservability = async (service) => {
16
+ // The kit (1.0.0) announces an enabled tracer with one console.log, which would land in a
17
+ // `--json` payload or on the daemon's JSON-RPC wire. Divert stdout to stderr while it boots; drop
18
+ // this once the kit writes that line to stderr itself.
19
+ const stdoutWrite = process.stdout.write;
20
+ process.stdout.write = process.stderr.write.bind(process.stderr);
21
+ try {
22
+ if (!process.env['OTEL_SERVICE_NAME'])
23
+ process.env['OTEL_SERVICE_NAME'] = service;
24
+ await import('@agentage/observability/bootstrap');
25
+ }
26
+ catch (err) {
27
+ if (process.env['AGENTAGE_DEBUG']) {
28
+ const reason = err instanceof Error ? err.message : String(err);
29
+ console.error(`[agentage] observability off: ${reason}`); // stderr: never corrupt stdout
30
+ }
31
+ }
32
+ finally {
33
+ process.stdout.write = stdoutWrite;
34
+ }
35
+ };
@@ -0,0 +1 @@
1
+ export declare const run: () => Promise<void>;
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+ import { Command } from 'commander';
3
+ import { registerDaemon } from './commands/daemon/daemon-cmd.js';
4
+ import { registerMemory } from './commands/memory/memory.js';
5
+ import { registerSetup } from './commands/auth/setup.js';
6
+ import { registerStatus } from './commands/status/status.js';
7
+ import { registerUpdate } from './commands/update/update.js';
8
+ import { registerVault } from './commands/vault/vault.js';
9
+ import { disableDaemon } from './lib/daemon/daemon-pref.js';
10
+ import { refreshUpdateCache, updateHint } from './lib/update/update-cache.js';
11
+ import { VERSION } from './utils/version.js';
12
+ const program = new Command();
13
+ program
14
+ .name('agentage')
15
+ .description('The offline-first terminal client for agentage Memory')
16
+ .version(VERSION)
17
+ .option('--no-daemon', 'run memory verbs in-process instead of via the daemon');
18
+ program.addHelpText('after', '\nNew here? Run: agentage vault add <name> --local, or agentage setup to connect an account.');
19
+ program.hook('preAction', () => {
20
+ if (program.opts().daemon === false)
21
+ disableDaemon();
22
+ });
23
+ // After any command (except the update-aware ones, which report richer state themselves), print a
24
+ // one-line hint from the cache, then kick a fire-and-forget refresh. Never awaited, never throws -
25
+ // the command output is already done and the cache-only read keeps this instant. The hint is
26
+ // suppressed under --json so it never corrupts machine-readable output.
27
+ program.hook('postAction', (_thisCommand, actionCommand) => {
28
+ const name = actionCommand.name();
29
+ if (name === 'status' || name === 'update')
30
+ return;
31
+ if (!actionCommand.opts()['json']) {
32
+ const hint = updateHint();
33
+ if (hint)
34
+ console.error(chalk.dim(hint)); // stderr: never corrupt piped stdout
35
+ }
36
+ void refreshUpdateCache();
37
+ });
38
+ registerSetup(program);
39
+ registerStatus(program);
40
+ registerVault(program);
41
+ registerMemory(program);
42
+ registerDaemon(program);
43
+ registerUpdate(program);
44
+ // Called by the bin (src/cli.ts) once the observability bootstrap has run.
45
+ export const run = async () => {
46
+ await program.parseAsync().catch((err) => {
47
+ console.error(err instanceof Error ? err.message : String(err));
48
+ process.exit(1);
49
+ });
50
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentage/cli",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "The agentage CLI - connect this machine to agentage from the terminal",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,14 +41,16 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@agentage/memory-core": "^0.5.0",
44
- "@agentage/server-memory": "^0.7.0",
44
+ "@agentage/observability": "^1.3.0",
45
+ "@agentage/server-memory": "^0.10.0",
45
46
  "@modelcontextprotocol/sdk": "^1.29.0",
47
+ "@opentelemetry/api": "^1.9.0",
46
48
  "chalk": "^6.0.0",
47
49
  "commander": "^15.0.0",
48
50
  "open": "^11.0.0"
49
51
  },
50
52
  "devDependencies": {
51
- "@anthropic-ai/sdk": "0.124.0",
53
+ "@anthropic-ai/sdk": "0.129.0",
52
54
  "@playwright/test": "latest",
53
55
  "@types/node": "latest",
54
56
  "@typescript-eslint/eslint-plugin": "latest",