@agentage/cli 0.35.0 → 0.38.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 (85) hide show
  1. package/README.md +53 -83
  2. package/dist/cli.js +0 -5
  3. package/dist/commands/auth/setup.d.ts +0 -2
  4. package/dist/commands/auth/setup.js +0 -18
  5. package/dist/commands/status/status.js +1 -25
  6. package/dist/commands/update/update.d.ts +0 -9
  7. package/dist/commands/update/update.js +0 -20
  8. package/dist/core/flag.d.ts +1 -0
  9. package/dist/core/flag.js +1 -0
  10. package/dist/lib/auth/api.js +1 -1
  11. package/dist/lib/auth/oauth.js +1 -1
  12. package/dist/lib/net/origins.d.ts +0 -2
  13. package/dist/lib/net/origins.js +1 -5
  14. package/dist/lib/net/user-agent.d.ts +1 -6
  15. package/dist/lib/net/user-agent.js +3 -4
  16. package/dist/lib/status/status-info.d.ts +0 -18
  17. package/dist/lib/status/status-info.js +1 -42
  18. package/dist/plugin.d.ts +1 -0
  19. package/dist/plugin.js +2 -0
  20. package/dist/program.js +4 -16
  21. package/package.json +17 -12
  22. package/dist/commands/daemon/daemon-cmd.d.ts +0 -2
  23. package/dist/commands/daemon/daemon-cmd.js +0 -107
  24. package/dist/commands/memory/memory-verbs.d.ts +0 -21
  25. package/dist/commands/memory/memory-verbs.js +0 -98
  26. package/dist/commands/memory/memory.d.ts +0 -2
  27. package/dist/commands/memory/memory.js +0 -54
  28. package/dist/commands/vault/vault-sync.d.ts +0 -13
  29. package/dist/commands/vault/vault-sync.js +0 -90
  30. package/dist/commands/vault/vault.d.ts +0 -22
  31. package/dist/commands/vault/vault.js +0 -129
  32. package/dist/daemon/actions.d.ts +0 -5
  33. package/dist/daemon/actions.js +0 -22
  34. package/dist/daemon/client-provider.d.ts +0 -2
  35. package/dist/daemon/client-provider.js +0 -16
  36. package/dist/daemon/entry.d.ts +0 -8
  37. package/dist/daemon/entry.js +0 -113
  38. package/dist/daemon/guards.d.ts +0 -3
  39. package/dist/daemon/guards.js +0 -13
  40. package/dist/daemon/lifecycle.d.ts +0 -17
  41. package/dist/daemon/lifecycle.js +0 -151
  42. package/dist/daemon/mcp-http.d.ts +0 -3
  43. package/dist/daemon/mcp-http.js +0 -34
  44. package/dist/daemon/server.d.ts +0 -23
  45. package/dist/daemon/server.js +0 -135
  46. package/dist/daemon-entry.d.ts +0 -1
  47. package/dist/daemon-entry.js +0 -9
  48. package/dist/lib/auth/provision.d.ts +0 -14
  49. package/dist/lib/auth/provision.js +0 -49
  50. package/dist/lib/daemon/daemon-client.d.ts +0 -37
  51. package/dist/lib/daemon/daemon-client.js +0 -136
  52. package/dist/lib/daemon/daemon-pref.d.ts +0 -2
  53. package/dist/lib/daemon/daemon-pref.js +0 -10
  54. package/dist/lib/memory/memory-client.d.ts +0 -30
  55. package/dist/lib/memory/memory-client.js +0 -98
  56. package/dist/lib/status/format.d.ts +0 -1
  57. package/dist/lib/status/format.js +0 -12
  58. package/dist/lib/status/vaults-format.d.ts +0 -2
  59. package/dist/lib/status/vaults-format.js +0 -49
  60. package/dist/lib/status/vaults-status.d.ts +0 -12
  61. package/dist/lib/status/vaults-status.js +0 -54
  62. package/dist/lib/vault/vault-registry.d.ts +0 -12
  63. package/dist/lib/vault/vault-registry.js +0 -81
  64. package/dist/lib/vault/vaults.d.ts +0 -10
  65. package/dist/lib/vault/vaults.js +0 -66
  66. package/dist/lib/vault/vaults.schema.d.ts +0 -2
  67. package/dist/lib/vault/vaults.schema.js +0 -10
  68. package/dist/mcp/local-server.d.ts +0 -4
  69. package/dist/mcp/local-server.js +0 -26
  70. package/dist/observability.d.ts +0 -4
  71. package/dist/observability.js +0 -35
  72. package/dist/sync/discover/watcher.d.ts +0 -26
  73. package/dist/sync/discover/watcher.js +0 -159
  74. package/dist/sync/git/conflict.d.ts +0 -1
  75. package/dist/sync/git/conflict.js +0 -16
  76. package/dist/sync/git/cycle.d.ts +0 -18
  77. package/dist/sync/git/cycle.js +0 -158
  78. package/dist/sync/git/git-exec.d.ts +0 -22
  79. package/dist/sync/git/git-exec.js +0 -56
  80. package/dist/sync/git/manager.d.ts +0 -28
  81. package/dist/sync/git/manager.js +0 -114
  82. package/dist/sync/git/planner.d.ts +0 -15
  83. package/dist/sync/git/planner.js +0 -46
  84. package/dist/sync/git/remote-url.d.ts +0 -3
  85. package/dist/sync/git/remote-url.js +0 -30
@@ -1,8 +0,0 @@
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;
@@ -1,113 +0,0 @@
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,3 +0,0 @@
1
- export declare const loopbackHosts: (port: number) => string[];
2
- export declare const isAllowedHost: (host: string | undefined, port: number) => boolean;
3
- export declare const isAllowedOrigin: (origin: string | undefined) => boolean;
@@ -1,13 +0,0 @@
1
- // Web-origin defense for the loopback daemon: a Host allow-list blocks DNS-rebinding (an attacker
2
- // domain that later resolves to 127.0.0.1 still sends its own Host), and an Origin allow-list blocks
3
- // a browser page from POSTing to the daemon. Both are pure so they unit-test without a socket.
4
- // The only Host values a genuine loopback client sends, pinned to the actually-bound port.
5
- export const loopbackHosts = (port) => [
6
- `127.0.0.1:${port}`,
7
- `localhost:${port}`,
8
- `[::1]:${port}`,
9
- ];
10
- export const isAllowedHost = (host, port) => host !== undefined && loopbackHosts(port).includes(host);
11
- // A missing Origin is fine (non-browser clients omit it); a present one must be loopback (any port).
12
- const LOOPBACK_ORIGIN = /^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/;
13
- export const isAllowedOrigin = (origin) => origin === undefined || LOOPBACK_ORIGIN.test(origin);
@@ -1,17 +0,0 @@
1
- export declare const DEFAULT_DAEMON_PORT = 4243;
2
- export declare const EADDRINUSE_EXIT_CODE = 3;
3
- export declare const writePidFile: (pid: number) => void;
4
- export declare const writePortFile: (port: number) => void;
5
- export declare const generateDaemonToken: () => string;
6
- export declare const writeTokenFile: (token: string) => void;
7
- export declare const readDaemonToken: () => string | null;
8
- export declare const removePidFile: () => void;
9
- export declare const removePortFile: () => void;
10
- export declare const removeTokenFile: () => void;
11
- export declare const resolvePort: () => number;
12
- export declare const isProcessAlive: (pid: number) => boolean;
13
- export declare const signalPid: (pid: number) => boolean;
14
- export declare const isDaemonRunning: () => boolean;
15
- export declare const stopDaemon: () => boolean;
16
- export declare const stopDaemonSafely: (probe?: (port: number) => Promise<number | null>) => Promise<boolean>;
17
- export declare const stopDaemonAndWait: (timeoutMs?: number) => Promise<boolean>;
@@ -1,151 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import chalk from 'chalk';
5
- import { getConfigDir } from '../lib/fs/config.js';
6
- // The daemon's on-disk state lives beside the config so an isolated AGENTAGE_CONFIG_DIR fully
7
- // isolates a daemon (pid, port, token) from any other - tests never collide with a real one.
8
- export const DEFAULT_DAEMON_PORT = 4243;
9
- // The daemon entry exits with this code on EADDRINUSE so a spawning CLI can short-circuit fast.
10
- export const EADDRINUSE_EXIT_CODE = 3;
11
- const pidPath = () => join(getConfigDir(), 'daemon.pid');
12
- const portPath = () => join(getConfigDir(), 'daemon.port');
13
- const tokenPath = () => join(getConfigDir(), 'daemon.token');
14
- export const writePidFile = (pid) => writeFileSync(pidPath(), String(pid), 'utf-8');
15
- export const writePortFile = (port) => writeFileSync(portPath(), String(port), 'utf-8');
16
- // A 256-bit per-daemon secret gating /api/*; only a same-machine client that can read the 0600
17
- // file (so, the same user) can call it, closing the loopback socket to any other local process.
18
- export const generateDaemonToken = () => randomBytes(32).toString('hex');
19
- export const writeTokenFile = (token) => writeFileSync(tokenPath(), token, { encoding: 'utf-8', mode: 0o600 });
20
- export const readDaemonToken = () => {
21
- if (!existsSync(tokenPath()))
22
- return null;
23
- const token = readFileSync(tokenPath(), 'utf-8').trim();
24
- return token.length > 0 ? token : null;
25
- };
26
- export const removePidFile = () => {
27
- if (existsSync(pidPath()))
28
- unlinkSync(pidPath());
29
- };
30
- export const removePortFile = () => {
31
- if (existsSync(portPath()))
32
- unlinkSync(portPath());
33
- };
34
- export const removeTokenFile = () => {
35
- if (existsSync(tokenPath()))
36
- unlinkSync(tokenPath());
37
- };
38
- const readNumberFile = (path) => {
39
- if (!existsSync(path))
40
- return null;
41
- const n = Number.parseInt(readFileSync(path, 'utf-8').trim(), 10);
42
- return Number.isNaN(n) ? null : n;
43
- };
44
- // Env override wins (test isolation); else the running daemon's recorded port; else the default.
45
- export const resolvePort = () => {
46
- const env = process.env['AGENTAGE_DAEMON_PORT'];
47
- if (env) {
48
- const n = Number.parseInt(env, 10);
49
- if (!Number.isNaN(n))
50
- return n;
51
- }
52
- return readNumberFile(portPath()) ?? DEFAULT_DAEMON_PORT;
53
- };
54
- const readPid = () => readNumberFile(pidPath());
55
- export const isProcessAlive = (pid) => {
56
- try {
57
- process.kill(pid, 0);
58
- return true;
59
- }
60
- catch {
61
- return false;
62
- }
63
- };
64
- // SIGTERM a known-live pid (a legacy daemon we learned about via /health, not our pidfile).
65
- // Returns whether the signal was delivered; false when the pid is already gone or unsignalable.
66
- export const signalPid = (pid) => {
67
- if (!isProcessAlive(pid))
68
- return false;
69
- try {
70
- process.kill(pid, 'SIGTERM');
71
- return true;
72
- }
73
- catch {
74
- return false;
75
- }
76
- };
77
- export const isDaemonRunning = () => {
78
- const pid = readPid();
79
- if (pid === null)
80
- return false;
81
- if (!isProcessAlive(pid)) {
82
- removePidFile();
83
- removePortFile();
84
- removeTokenFile();
85
- return false;
86
- }
87
- return true;
88
- };
89
- // SIGTERM the running daemon and clear its on-disk state; the daemon also self-cleans on exit.
90
- // Returns whether a live process was actually signalled.
91
- export const stopDaemon = () => {
92
- const pid = readPid();
93
- if (pid === null)
94
- return false;
95
- const alive = isProcessAlive(pid);
96
- if (alive)
97
- process.kill(pid, 'SIGTERM');
98
- removePidFile();
99
- removePortFile();
100
- removeTokenFile();
101
- return alive;
102
- };
103
- // Lightweight, cycle-free /health probe (daemon-client imports this module) returning the reported
104
- // pid; null when the port holds no reachable agentage daemon.
105
- const probeDaemonPid = async (port) => {
106
- try {
107
- const res = await fetch(`http://127.0.0.1:${port}/api/health`, {
108
- signal: AbortSignal.timeout(500),
109
- });
110
- if (!res.ok)
111
- return null;
112
- const h = (await res.json());
113
- return typeof h.pid === 'number' ? h.pid : null;
114
- }
115
- catch {
116
- return null;
117
- }
118
- };
119
- // stopDaemon signals a bare recorded pid, which the OS may have recycled onto an unrelated process.
120
- // Guard it: signal only when /health confirms the pid is our daemon, or - if no port was recorded -
121
- // fall back to the blind signal with a printed caveat. Refuse otherwise rather than kill a stranger.
122
- export const stopDaemonSafely = async (probe = probeDaemonPid) => {
123
- const pid = readPid();
124
- if (pid === null || !isProcessAlive(pid))
125
- return stopDaemon();
126
- const port = readNumberFile(portPath());
127
- const confirmed = port !== null && (await probe(port)) === pid;
128
- if (confirmed)
129
- return stopDaemon();
130
- if (port === null) {
131
- console.error(chalk.yellow(`No recorded port; signalling pid ${pid} without confirmation.`));
132
- return stopDaemon();
133
- }
134
- console.error(chalk.yellow(`Daemon on port ${port} did not confirm pid ${pid}; not signalling. Delete the pid file to force.`));
135
- return false;
136
- };
137
- // Stop, then wait (bounded) for the old process to actually exit so a restart can rebind the
138
- // port without an EADDRINUSE window. Returns whether the process is confirmed gone.
139
- export const stopDaemonAndWait = async (timeoutMs = 2000) => {
140
- const pid = readPid();
141
- const signalled = stopDaemon();
142
- if (!signalled || pid === null)
143
- return true;
144
- const deadline = Date.now() + timeoutMs;
145
- while (isProcessAlive(pid)) {
146
- if (Date.now() >= deadline)
147
- return false;
148
- await new Promise((r) => setTimeout(r, 50));
149
- }
150
- return true;
151
- };
@@ -1,3 +0,0 @@
1
- import { type IncomingMessage, type ServerResponse } from 'node:http';
2
- import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- export declare const handleMcp: (req: IncomingMessage, res: ServerResponse, buildServer: () => Promise<McpServer>, allowedHosts: string[]) => Promise<void>;
@@ -1,34 +0,0 @@
1
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
2
- const jsonRpcError = (res, status, message) => {
3
- res.writeHead(status, { 'Content-Type': 'application/json' });
4
- res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message }, id: null }));
5
- };
6
- // Stateless Streamable HTTP (JSON, no session), mirroring the cloud memory endpoint: a fresh MCP
7
- // server + transport per POST, torn down when the response closes. GET/DELETE are 405 - stateless
8
- // exposes no server-initiated SSE stream or session teardown. Tokenless (editor clients stay
9
- // simple) but DNS-rebinding-protected: allowedHosts pins the Host to the bound loopback port.
10
- export const handleMcp = async (req, res, buildServer, allowedHosts) => {
11
- if (req.method !== 'POST') {
12
- jsonRpcError(res, 405, 'Method not allowed: this endpoint is stateless (POST only).');
13
- return;
14
- }
15
- const server = await buildServer();
16
- const transport = new StreamableHTTPServerTransport({
17
- sessionIdGenerator: undefined,
18
- enableJsonResponse: true,
19
- enableDnsRebindingProtection: true,
20
- allowedHosts,
21
- });
22
- res.on('close', () => {
23
- void transport.close();
24
- void server.close();
25
- });
26
- try {
27
- await server.connect(transport);
28
- await transport.handleRequest(req, res);
29
- }
30
- catch {
31
- if (!res.headersSent)
32
- jsonRpcError(res, 500, 'Internal server error');
33
- }
34
- };
@@ -1,23 +0,0 @@
1
- import { type Server } from 'node:http';
2
- import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { type MemoryClient } from '../lib/memory/memory-client.js';
4
- import { type SyncResult } from '../sync/git/cycle.js';
5
- import { type SyncStatus } from '../sync/git/manager.js';
6
- export interface DaemonSyncApi {
7
- status: () => SyncStatus;
8
- runNow: (vault: string) => Promise<SyncResult>;
9
- }
10
- export interface DaemonServerOptions {
11
- getClient: () => MemoryClient | Promise<MemoryClient>;
12
- buildMcpServer?: () => Promise<McpServer>;
13
- sync?: DaemonSyncApi;
14
- authToken: string;
15
- version: string;
16
- startedAt?: number;
17
- }
18
- export interface DaemonServer {
19
- server: Server;
20
- start: (port: number, host?: string) => Promise<void>;
21
- stop: () => Promise<void>;
22
- }
23
- export declare const createDaemonServer: (opts: DaemonServerOptions) => DaemonServer;
@@ -1,135 +0,0 @@
1
- import { createServer } from 'node:http';
2
- import { dispatchMemory, isMemoryVerb } from './actions.js';
3
- import { isAllowedHost, isAllowedOrigin, loopbackHosts } from './guards.js';
4
- import { handleMcp } from './mcp-http.js';
5
- const LOOPBACK = '127.0.0.1';
6
- const AUTH_HEADER = 'x-agentage-token';
7
- const readBody = (req) => new Promise((resolve, reject) => {
8
- const chunks = [];
9
- req.on('data', (c) => chunks.push(c));
10
- req.on('end', () => {
11
- const raw = Buffer.concat(chunks).toString('utf-8');
12
- if (raw.length === 0)
13
- return resolve({});
14
- try {
15
- resolve(JSON.parse(raw));
16
- }
17
- catch {
18
- reject(new Error('invalid JSON body'));
19
- }
20
- });
21
- req.on('error', reject);
22
- });
23
- const send = (res, status, body) => {
24
- res.writeHead(status, { 'Content-Type': 'application/json' });
25
- res.end(JSON.stringify(body));
26
- };
27
- const isJsonContentType = (req) => (req.headers['content-type'] ?? '').split(';')[0]?.trim() === 'application/json';
28
- const header = (req, name) => {
29
- const v = req.headers[name];
30
- return Array.isArray(v) ? v[0] : v;
31
- };
32
- // Loopback-only JSON HTTP: GET /api/health + POST /api/memory/<verb> (the CLI verbs) + POST /mcp
33
- // (the frozen 6 tools for on-machine AI clients, stateless Streamable HTTP). Web-origin defense on
34
- // EVERY request (Host + Origin allow-lists + DNS-rebinding protection on /mcp); /api/* additionally
35
- // needs the per-daemon token, while /health and /mcp stay tokenless for probes and editor clients.
36
- export const createDaemonServer = (opts) => {
37
- const startedAt = opts.startedAt ?? Date.now();
38
- let served = 0;
39
- let boundPort = 0;
40
- // 403 unless the request looks like it came from a genuine same-machine loopback client.
41
- const originGuard = (req) => isAllowedHost(header(req, 'host'), boundPort) && isAllowedOrigin(header(req, 'origin'));
42
- const handle = async (req, res) => {
43
- const url = req.url ?? '/';
44
- if (!originGuard(req))
45
- return send(res, 403, { error: 'forbidden' });
46
- const path = url.split('?')[0] ?? url;
47
- const isApi = path.startsWith('/api/') && path !== '/api/health';
48
- if (isApi && header(req, AUTH_HEADER) !== opts.authToken) {
49
- return send(res, 401, { error: 'unauthorized' });
50
- }
51
- if (isApi && req.method === 'POST' && !isJsonContentType(req)) {
52
- return send(res, 415, { error: 'unsupported media type' });
53
- }
54
- if (req.method === 'GET' && url === '/api/health') {
55
- return send(res, 200, {
56
- ok: true,
57
- version: opts.version,
58
- pid: process.pid,
59
- uptime: Math.floor((Date.now() - startedAt) / 1000),
60
- served,
61
- mcp: !!opts.buildMcpServer,
62
- });
63
- }
64
- if (path === '/mcp') {
65
- if (!opts.buildMcpServer)
66
- return send(res, 404, { error: 'not found' });
67
- return handleMcp(req, res, opts.buildMcpServer, loopbackHosts(boundPort));
68
- }
69
- if (req.method === 'GET' && url === '/api/sync/status') {
70
- if (!opts.sync)
71
- return send(res, 404, { error: 'not found' });
72
- return send(res, 200, opts.sync.status());
73
- }
74
- if (req.method === 'POST' && url === '/api/sync/run') {
75
- if (!opts.sync)
76
- return send(res, 404, { error: 'not found' });
77
- const body = (await readBody(req));
78
- if (!body.vault)
79
- return send(res, 400, { error: 'vault is required' });
80
- try {
81
- return send(res, 200, await opts.sync.runNow(body.vault));
82
- }
83
- catch (err) {
84
- return send(res, 400, { error: err instanceof Error ? err.message : String(err) });
85
- }
86
- }
87
- const match = url.match(/^\/api\/memory\/([a-z]+)$/);
88
- if (req.method === 'POST' && match) {
89
- const verb = match[1];
90
- if (!isMemoryVerb(verb))
91
- return send(res, 404, { error: `unknown verb: ${verb}` });
92
- try {
93
- const body = await readBody(req);
94
- const result = await dispatchMemory(await opts.getClient(), verb, body);
95
- served += 1;
96
- return send(res, 200, result);
97
- }
98
- catch (err) {
99
- return send(res, 400, { error: err instanceof Error ? err.message : String(err) });
100
- }
101
- }
102
- send(res, 404, { error: 'not found' });
103
- };
104
- const server = createServer((req, res) => {
105
- handle(req, res).catch((err) => send(res, 500, { error: err instanceof Error ? err.message : String(err) }));
106
- });
107
- const start = (port, host = LOOPBACK) => new Promise((resolve, reject) => {
108
- const onError = (err) => {
109
- if (err.code !== 'EADDRINUSE')
110
- return reject(err);
111
- // Preserve the code so the entry point can exit distinctly on a busy port.
112
- const busy = new Error(`port ${port} already in use`);
113
- busy.code = 'EADDRINUSE';
114
- reject(busy);
115
- };
116
- server.once('error', onError);
117
- server.listen(port, host, () => {
118
- const addr = server.address();
119
- boundPort = typeof addr === 'object' && addr ? addr.port : port;
120
- server.removeListener('error', onError);
121
- resolve();
122
- });
123
- });
124
- // close() alone hangs on idle keep-alive sockets; drop idle ones now, force the rest after a grace.
125
- const stop = () => new Promise((resolve) => {
126
- server.closeIdleConnections();
127
- const grace = setTimeout(() => server.closeAllConnections(), 500);
128
- grace.unref();
129
- server.close(() => {
130
- clearTimeout(grace);
131
- resolve();
132
- });
133
- });
134
- return { server, start, stop };
135
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,9 +0,0 @@
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 {};
@@ -1,14 +0,0 @@
1
- import { type AuthState } from '../fs/config.js';
2
- import { type Links } from '../net/origins.js';
3
- export type ProvisionStatus = 'provisioned' | 'exists' | 'unauthenticated' | 'offline';
4
- export interface ProvisionResult {
5
- status: ProvisionStatus;
6
- message: string;
7
- }
8
- export interface ProvisionDeps {
9
- readAuth: () => AuthState | null;
10
- links: () => Links;
11
- post: (auth: AuthState, links: Links, url: string, body: unknown) => Promise<Response>;
12
- }
13
- export declare const defaultProvisionDeps: () => ProvisionDeps;
14
- export declare const provisionAccountVault: (name: string, deps?: ProvisionDeps) => Promise<ProvisionResult>;
@@ -1,49 +0,0 @@
1
- import { authedPost } from './api.js';
2
- import { readAuth } from '../fs/config.js';
3
- import { links as buildLinks, siteFqdn } from '../net/origins.js';
4
- export const defaultProvisionDeps = () => ({
5
- readAuth,
6
- links: () => buildLinks(siteFqdn()),
7
- post: authedPost,
8
- });
9
- const registeredLocally = (name, tail) => `Vault '${name}' registered locally${tail}`;
10
- export const provisionAccountVault = async (name, deps = defaultProvisionDeps()) => {
11
- const auth = deps.readAuth();
12
- if (!auth) {
13
- return {
14
- status: 'unauthenticated',
15
- message: registeredLocally(name, ' - run `agentage setup` to create it in your account.'),
16
- };
17
- }
18
- // A PAT is an MCP-surface credential; the backend REST provisioning endpoint rejects plain
19
- // bearers (only session cookies), so it cannot create the account memory. Fail clearly.
20
- if (auth.kind === 'pat') {
21
- return {
22
- status: 'unauthenticated',
23
- message: registeredLocally(name, ' - account provisioning needs an interactive session (run `agentage setup`); ' +
24
- 'a personal access token only authorizes memory (MCP) calls.'),
25
- };
26
- }
27
- const links = deps.links();
28
- let res;
29
- try {
30
- res = await deps.post(auth, links, `${links.api}/memories`, { name });
31
- }
32
- catch {
33
- return {
34
- status: 'offline',
35
- message: registeredLocally(name, ' - will provision when online.'),
36
- };
37
- }
38
- if (res.status === 201)
39
- return { status: 'provisioned', message: `Provisioned account vault '${name}'.` };
40
- if (res.status === 200)
41
- return { status: 'exists', message: `Account vault '${name}' already provisioned.` };
42
- if (res.status === 401)
43
- return {
44
- status: 'unauthenticated',
45
- message: registeredLocally(name, ' - run `agentage setup` to create it in your account.'),
46
- };
47
- // Any other status stays non-fatal: keep the local entry, let the daemon retry later.
48
- return { status: 'offline', message: registeredLocally(name, ' - will provision when online.') };
49
- };
@@ -1,37 +0,0 @@
1
- import { type SyncResult } from '../../sync/git/cycle.js';
2
- import { type SyncStatus } from '../../sync/git/manager.js';
3
- import { type MemoryClient } from '../memory/memory-client.js';
4
- export interface Health {
5
- ok: boolean;
6
- version: string;
7
- pid: number;
8
- uptime: number;
9
- served: number;
10
- mcp?: boolean;
11
- }
12
- export declare const health: (port: number, timeoutMs?: number) => Promise<Health | null>;
13
- export declare const syncStatus: (port: number, timeoutMs?: number) => Promise<SyncStatus | null>;
14
- export declare const syncRun: (port: number, vault: string) => Promise<SyncResult>;
15
- export declare const waitForHealth: (port: number, opts?: {
16
- timeoutMs?: number;
17
- intervalMs?: number;
18
- }) => Promise<boolean>;
19
- export declare const createDaemonClient: (port: number) => MemoryClient;
20
- export declare const mismatchNotice: (daemonVersion: string) => string | null;
21
- export type SpawnOutcome = {
22
- ok: true;
23
- } | {
24
- ok: false;
25
- reason: 'port-in-use' | 'unreachable' | 'blocked';
26
- };
27
- export declare const spawnDaemon: (port: number, opts?: {
28
- timeoutMs?: number;
29
- noMcp?: boolean;
30
- }) => Promise<SpawnOutcome>;
31
- export interface EnsureDeps {
32
- port?: number;
33
- probe?: (port: number) => Promise<Health | null>;
34
- spawn?: (port: number) => Promise<SpawnOutcome>;
35
- readToken?: () => string | null;
36
- }
37
- export declare const ensureDaemon: (deps?: EnsureDeps) => Promise<MemoryClient | null>;