@agentage/cli 0.37.2 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +53 -83
  2. package/dist/cli.js +5 -8
  3. package/dist/commands/auth/setup.d.ts +6 -16
  4. package/dist/commands/auth/setup.js +9 -139
  5. package/dist/commands/status/status.d.ts +4 -3
  6. package/dist/commands/status/status.js +4 -27
  7. package/dist/commands/update/update.d.ts +3 -10
  8. package/dist/commands/update/update.js +24 -41
  9. package/dist/core/flag.d.ts +1 -0
  10. package/dist/core/flag.js +1 -0
  11. package/dist/lib/auth/api.d.ts +3 -2
  12. package/dist/lib/auth/api.js +50 -56
  13. package/dist/lib/auth/env-match.js +1 -0
  14. package/dist/lib/auth/introspect.js +10 -11
  15. package/dist/lib/auth/oauth.d.ts +2 -0
  16. package/dist/lib/auth/oauth.js +30 -28
  17. package/dist/lib/auth/sign-in.d.ts +20 -0
  18. package/dist/lib/auth/sign-in.js +95 -0
  19. package/dist/lib/fs/config.d.ts +1 -0
  20. package/dist/lib/fs/config.js +14 -21
  21. package/dist/lib/fs/file-lock.d.ts +7 -2
  22. package/dist/lib/fs/file-lock.js +64 -74
  23. package/dist/lib/fs/json-file.d.ts +3 -0
  24. package/dist/lib/fs/json-file.js +43 -0
  25. package/dist/lib/net/fetch.d.ts +10 -0
  26. package/dist/lib/net/fetch.js +13 -0
  27. package/dist/lib/net/http.js +51 -23
  28. package/dist/lib/net/origins.d.ts +0 -2
  29. package/dist/lib/net/origins.js +1 -5
  30. package/dist/lib/net/user-agent.d.ts +1 -6
  31. package/dist/lib/net/user-agent.js +3 -4
  32. package/dist/lib/npm/npm-cli.d.ts +17 -0
  33. package/dist/lib/npm/npm-cli.js +77 -0
  34. package/dist/lib/status/status-info.d.ts +0 -18
  35. package/dist/lib/status/status-info.js +2 -42
  36. package/dist/lib/update/update-cache.d.ts +1 -0
  37. package/dist/lib/update/update-cache.js +7 -11
  38. package/dist/lib/update/update-check.js +1 -4
  39. package/dist/plugin.d.ts +1 -0
  40. package/dist/plugin.js +2 -0
  41. package/dist/program.d.ts +18 -1
  42. package/dist/program.js +75 -44
  43. package/package.json +17 -12
  44. package/dist/commands/daemon/daemon-cmd.d.ts +0 -2
  45. package/dist/commands/daemon/daemon-cmd.js +0 -107
  46. package/dist/commands/memory/memory-verbs.d.ts +0 -21
  47. package/dist/commands/memory/memory-verbs.js +0 -98
  48. package/dist/commands/memory/memory.d.ts +0 -2
  49. package/dist/commands/memory/memory.js +0 -54
  50. package/dist/commands/vault/vault-sync.d.ts +0 -13
  51. package/dist/commands/vault/vault-sync.js +0 -90
  52. package/dist/commands/vault/vault.d.ts +0 -22
  53. package/dist/commands/vault/vault.js +0 -129
  54. package/dist/daemon/actions.d.ts +0 -5
  55. package/dist/daemon/actions.js +0 -22
  56. package/dist/daemon/client-provider.d.ts +0 -2
  57. package/dist/daemon/client-provider.js +0 -16
  58. package/dist/daemon/entry.d.ts +0 -8
  59. package/dist/daemon/entry.js +0 -113
  60. package/dist/daemon/guards.d.ts +0 -3
  61. package/dist/daemon/guards.js +0 -13
  62. package/dist/daemon/lifecycle.d.ts +0 -17
  63. package/dist/daemon/lifecycle.js +0 -151
  64. package/dist/daemon/mcp-http.d.ts +0 -3
  65. package/dist/daemon/mcp-http.js +0 -34
  66. package/dist/daemon/server.d.ts +0 -23
  67. package/dist/daemon/server.js +0 -135
  68. package/dist/daemon-entry.d.ts +0 -1
  69. package/dist/daemon-entry.js +0 -9
  70. package/dist/lib/auth/provision.d.ts +0 -14
  71. package/dist/lib/auth/provision.js +0 -49
  72. package/dist/lib/daemon/daemon-client.d.ts +0 -37
  73. package/dist/lib/daemon/daemon-client.js +0 -136
  74. package/dist/lib/daemon/daemon-pref.d.ts +0 -2
  75. package/dist/lib/daemon/daemon-pref.js +0 -10
  76. package/dist/lib/memory/memory-client.d.ts +0 -30
  77. package/dist/lib/memory/memory-client.js +0 -98
  78. package/dist/lib/status/format.d.ts +0 -1
  79. package/dist/lib/status/format.js +0 -12
  80. package/dist/lib/status/vaults-format.d.ts +0 -2
  81. package/dist/lib/status/vaults-format.js +0 -49
  82. package/dist/lib/status/vaults-status.d.ts +0 -12
  83. package/dist/lib/status/vaults-status.js +0 -54
  84. package/dist/lib/update/update-lock.d.ts +0 -2
  85. package/dist/lib/update/update-lock.js +0 -109
  86. package/dist/lib/vault/vault-registry.d.ts +0 -12
  87. package/dist/lib/vault/vault-registry.js +0 -81
  88. package/dist/lib/vault/vaults.d.ts +0 -10
  89. package/dist/lib/vault/vaults.js +0 -66
  90. package/dist/lib/vault/vaults.schema.d.ts +0 -2
  91. package/dist/lib/vault/vaults.schema.js +0 -10
  92. package/dist/mcp/local-server.d.ts +0 -4
  93. package/dist/mcp/local-server.js +0 -26
  94. package/dist/observability.d.ts +0 -4
  95. package/dist/observability.js +0 -35
  96. package/dist/sync/discover/watcher.d.ts +0 -26
  97. package/dist/sync/discover/watcher.js +0 -159
  98. package/dist/sync/git/conflict.d.ts +0 -1
  99. package/dist/sync/git/conflict.js +0 -16
  100. package/dist/sync/git/cycle.d.ts +0 -18
  101. package/dist/sync/git/cycle.js +0 -158
  102. package/dist/sync/git/git-exec.d.ts +0 -22
  103. package/dist/sync/git/git-exec.js +0 -56
  104. package/dist/sync/git/manager.d.ts +0 -28
  105. package/dist/sync/git/manager.js +0 -114
  106. package/dist/sync/git/planner.d.ts +0 -15
  107. package/dist/sync/git/planner.js +0 -46
  108. package/dist/sync/git/remote-url.d.ts +0 -3
  109. package/dist/sync/git/remote-url.js +0 -30
@@ -1,22 +0,0 @@
1
- import { type Command } from 'commander';
2
- import { type VaultsConfig } from '@agentage/memory-core';
3
- import { type ProvisionResult } from '../../lib/auth/provision.js';
4
- import { type LoadedVaults } from '../../lib/vault/vaults.js';
5
- export interface VaultDeps {
6
- load: () => LoadedVaults;
7
- mutate: (fn: (config: VaultsConfig) => VaultsConfig | null | void) => Promise<VaultsConfig>;
8
- ensureDir: (path: string) => void;
9
- provision: (name: string) => Promise<ProvisionResult>;
10
- log: (msg: string) => void;
11
- }
12
- export interface VaultAddOptions {
13
- local?: string | boolean;
14
- git?: string;
15
- path?: string;
16
- }
17
- export declare const runVaultAdd: (name: string, opts: VaultAddOptions, deps?: VaultDeps) => Promise<void>;
18
- export declare const runVaultRemove: (name: string, deps?: VaultDeps) => Promise<void>;
19
- export declare const runVaultList: (opts: {
20
- json?: boolean;
21
- }, deps?: VaultDeps) => void;
22
- export declare const registerVault: (program: Command) => void;
@@ -1,129 +0,0 @@
1
- import chalk from 'chalk';
2
- import { isAccountVault } from '@agentage/memory-core';
3
- import { addVault, appendDiscoverIgnore, ensureVaultDir, formatVaultLine, redactEntry, removeVault, vaultType, } from '../../lib/vault/vault-registry.js';
4
- import { defaultProvisionDeps, provisionAccountVault, } from '../../lib/auth/provision.js';
5
- import { loadVaultsConfig, mutateVaultsConfig } from '../../lib/vault/vaults.js';
6
- import { assertSafeRemoteUrl, redactRemoteUrl } from '../../sync/git/remote-url.js';
7
- import { defaultVaultSyncDeps, runVaultSync } from './vault-sync.js';
8
- const defaultDeps = {
9
- load: loadVaultsConfig,
10
- mutate: mutateVaultsConfig,
11
- ensureDir: ensureVaultDir,
12
- provision: (name) => provisionAccountVault(name, defaultProvisionDeps()),
13
- log: (msg) => console.log(msg),
14
- };
15
- const buildEntry = (name, opts) => {
16
- const hasLocal = opts.local !== undefined;
17
- if (hasLocal && opts.git)
18
- throw new Error('choose one of --local or --git, not both');
19
- if (opts.path !== undefined && (hasLocal || opts.git))
20
- throw new Error('--path applies only to an account vault (drop --local and --git)');
21
- // A --git vault is a local working copy that syncs to an external git remote (path + origin);
22
- // the daemon commits/pushes and pulls it per its interval.
23
- if (opts.git) {
24
- assertSafeRemoteUrl(opts.git);
25
- return { path: `~/vaults/${name}`, origin: [{ remote: opts.git }], mcp: ['local'] };
26
- }
27
- if (hasLocal) {
28
- const path = typeof opts.local === 'string' ? opts.local : `~/vaults/${name}`;
29
- return { path, mcp: ['local'] };
30
- }
31
- // No --local/--git: an account vault - a local folder plus a memory in your account; this CLI
32
- // has no channel that syncs the two.
33
- return { path: opts.path ?? `~/vaults/${name}`, origin: [{ remote: 'agentage' }] };
34
- };
35
- export const runVaultAdd = async (name, opts, deps = defaultDeps) => {
36
- const entry = buildEntry(name, opts);
37
- // The dup check + save run together under the lock, against the FRESH on-disk config.
38
- await deps.mutate((config) => addVault(config, name, entry));
39
- if (entry.path)
40
- deps.ensureDir(entry.path);
41
- const kind = vaultType(entry);
42
- const where = entry.path ?? entry.origin?.[0]?.remote ?? '';
43
- const via = kind === 'git' && entry.origin?.length ? ` <- ${redactRemoteUrl(entry.origin[0].remote)}` : '';
44
- deps.log(chalk.green(`Added vault '${name}' (${kind}) -> ${where}${via}`));
45
- // Offline-first: the local entry is saved first; provisioning the account channel is never fatal.
46
- if (isAccountVault(entry))
47
- deps.log((await deps.provision(name)).message);
48
- };
49
- export const runVaultRemove = async (name, deps = defaultDeps) => {
50
- let wasDefault = false;
51
- let ignoredRoot = null;
52
- // The removal and the discover-ignore append run as one locked read-modify-write, against the
53
- // FRESH on-disk config, else a concurrent writer's update is lost or the next scan re-adds the folder.
54
- const next = await deps.mutate((config) => {
55
- wasDefault = config.default === name;
56
- const path = config.vaults?.[name]?.path;
57
- let updated = removeVault(config, name);
58
- const ignored = path ? appendDiscoverIgnore(updated, name, path) : null;
59
- if (ignored) {
60
- updated = ignored.config;
61
- ignoredRoot = ignored.root;
62
- }
63
- return updated;
64
- });
65
- deps.log(`Removed vault '${name}' (files left on disk).`);
66
- if (ignoredRoot)
67
- deps.log(`Added '${name}' to ${ignoredRoot} ignore so it is not re-discovered.`);
68
- if (wasDefault && next.default)
69
- deps.log(`Default vault is now '${next.default}'.`);
70
- };
71
- export const runVaultList = (opts, deps = defaultDeps) => {
72
- const { config } = deps.load();
73
- const vaults = config.vaults ?? {};
74
- const names = Object.keys(vaults);
75
- if (opts.json) {
76
- // Backward-compatible: still the name-keyed map, each entry annotated with its honest type.
77
- // Origin remotes are redacted so a credentialed URL never leaks into machine output.
78
- const out = Object.fromEntries(names.map((n) => [n, { ...redactEntry(vaults[n]), type: vaultType(vaults[n]) }]));
79
- deps.log(JSON.stringify(out, null, 2));
80
- return;
81
- }
82
- if (names.length === 0) {
83
- deps.log('No vaults registered. Add one with `agentage vault add <name> --local`.');
84
- return;
85
- }
86
- for (const name of names) {
87
- const suffix = name === config.default ? ' (default)' : '';
88
- deps.log(formatVaultLine(name, vaults[name]) + suffix);
89
- }
90
- };
91
- const guard = (fn) => {
92
- try {
93
- fn();
94
- }
95
- catch (err) {
96
- console.error(chalk.red(err instanceof Error ? err.message : String(err)));
97
- process.exitCode = 1;
98
- }
99
- };
100
- const guardAsync = (fn) => fn().catch((err) => {
101
- console.error(chalk.red(err instanceof Error ? err.message : String(err)));
102
- process.exitCode = 1;
103
- });
104
- export const registerVault = (program) => {
105
- const vault = program.command('vault').description('Manage local memory vaults');
106
- vault
107
- .command('list')
108
- .description('List registered vaults')
109
- .option('--json', 'machine-readable output')
110
- .action((opts) => guard(() => runVaultList(opts)));
111
- vault
112
- .command('add <name>')
113
- .description('Register a vault (account by default; --local or --git for a self-hosted one)')
114
- .option('--local [path]', 'a local folder (default ~/vaults/<name>)')
115
- .option('--git <remote>', 'a vault synced to an external git remote')
116
- .option('--path <dir>', 'account vault local mirror dir (default ~/vaults/<name>)')
117
- .action((name, opts) => guardAsync(() => runVaultAdd(name, opts)));
118
- vault
119
- .command('remove <name>')
120
- .description('Unregister a vault (files stay on disk)')
121
- .action((name) => guardAsync(() => runVaultRemove(name)));
122
- vault
123
- .command('sync [name]')
124
- .description('Sync vaults now (git commit/push/pull)')
125
- .action((name) => runVaultSync(name, defaultVaultSyncDeps()).catch((err) => {
126
- console.error(chalk.red(err instanceof Error ? err.message : String(err)));
127
- process.exitCode = 1;
128
- }));
129
- };
@@ -1,5 +0,0 @@
1
- import { type MemoryClient } from '../lib/memory/memory-client.js';
2
- export declare const MEMORY_VERBS: readonly ["search", "read", "write", "edit", "list", "delete"];
3
- export type MemoryVerb = (typeof MEMORY_VERBS)[number];
4
- export declare const isMemoryVerb: (v: string) => v is MemoryVerb;
5
- export declare const dispatchMemory: (client: MemoryClient, verb: MemoryVerb, body: unknown) => Promise<unknown>;
@@ -1,22 +0,0 @@
1
- export const MEMORY_VERBS = ['search', 'read', 'write', 'edit', 'list', 'delete'];
2
- export const isMemoryVerb = (v) => MEMORY_VERBS.includes(v);
3
- // Map one wire payload to the matching MemoryClient call. The daemon and the CLI share this verb
4
- // set so a daemon request runs the exact method a DirectClient would run in-process (DO2).
5
- export const dispatchMemory = (client, verb, body) => {
6
- const p = (body ?? {});
7
- switch (verb) {
8
- case 'search':
9
- return client.search(p['query'], p['opts']);
10
- case 'read':
11
- return client.read(p['ref'], p['opts']);
12
- case 'write':
13
- return client.write(p['ref'], p['body'], p['opts']);
14
- case 'edit':
15
- return client.edit(p['ref'], p['op'], p['opts']);
16
- case 'list':
17
- return client.list(p['folder'], p['opts']);
18
- case 'delete':
19
- return client.delete(p['ref'], p['opts']);
20
- }
21
- throw new Error(`unknown verb: ${verb}`);
22
- };
@@ -1,2 +0,0 @@
1
- import { type MemoryClient } from '../lib/memory/memory-client.js';
2
- export declare const createClientProvider: () => (() => MemoryClient);
@@ -1,16 +0,0 @@
1
- import { existsSync, statSync } from 'node:fs';
2
- import { createDirectClient } from '../lib/memory/memory-client.js';
3
- import { loadVaultsConfig, vaultsJsonPath } from '../lib/vault/vaults.js';
4
- // One in-process engine per daemon, rebuilt only when vaults.json changes on disk, so a
5
- // `vault add` between requests is picked up without a daemon restart.
6
- export const createClientProvider = () => {
7
- let cached;
8
- return () => {
9
- const path = vaultsJsonPath();
10
- const mtime = existsSync(path) ? statSync(path).mtimeMs : 0;
11
- if (!cached || cached.mtime !== mtime) {
12
- cached = { client: createDirectClient(loadVaultsConfig().config), mtime };
13
- }
14
- return cached.client;
15
- };
16
- };
@@ -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;