@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,20 +1,16 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { dirname } from 'node:path';
4
- // A cross-process advisory lock keyed to a target file (`<target>.lock`). Config read-modify-writes
5
- // are sub-millisecond, so a lock past this age whose holder is gone is a crashed holder to take over.
6
- const LOCK_TTL_MS = 10_000;
7
- // A caller blocks up to this long for a contended lock. Past the TTL so a crashed holder is taken
8
- // over rather than wedging the caller; a throw only happens if the lock is genuinely stuck.
9
- const MAX_WAIT_MS = 15_000;
10
- // Past this age a holder is taken over even if its pid still resolves - the only explanation left is
11
- // pid reuse after a crash. Far beyond any real critical section or plausible scheduler pause, so a
12
- // live-but-CPU-starved holder is never mistaken for crashed below it.
13
- const STALE_HARD_MS = 60_000;
4
+ // Sized for sub-millisecond config read-modify-writes; the wait outlives the TTL on purpose.
5
+ const DEFAULTS = { ttlMs: 10_000, hardStaleMs: 60_000, maxWaitMs: 15_000 };
14
6
  const STEP_MS = 25;
7
+ const limitsOf = (opts) => ({
8
+ ttlMs: opts.ttlMs ?? DEFAULTS.ttlMs,
9
+ hardStaleMs: opts.hardStaleMs ?? DEFAULTS.hardStaleMs,
10
+ maxWaitMs: opts.maxWaitMs ?? DEFAULTS.maxWaitMs,
11
+ });
15
12
  const lockPath = (target) => `${target}.lock`;
16
- // The lock file holds "<pid> <timestamp>": pid gates crash-takeover, the trailing token is the
17
- // acquisition time used for staleness. A legacy single-token file parses as pid 0 (unknown holder).
13
+ // "<pid> <at>"; a legacy single-token file parses as pid 0 (unknown holder) and ages out on the TTL.
18
14
  const holderOf = (path) => {
19
15
  try {
20
16
  const parts = readFileSync(path, 'utf-8').trim().split(/\s+/);
@@ -28,8 +24,7 @@ const holderOf = (path) => {
28
24
  return null;
29
25
  }
30
26
  };
31
- // Same-host liveness probe. ESRCH = the holder is gone (crashed); EPERM = alive but owned by another
32
- // user. pid 0/unknown counts as gone so a legacy or malformed lock still ages out on the TTL.
27
+ // ESRCH = gone; EPERM = alive under another user; pid 0 (unknown) counts as gone.
33
28
  const holderAlive = (pid) => {
34
29
  if (pid <= 0)
35
30
  return false;
@@ -41,15 +36,12 @@ const holderAlive = (pid) => {
41
36
  return err.code === 'EPERM';
42
37
  }
43
38
  };
44
- // A holder is safe to take over only once it is past the TTL AND provably gone: a dead pid, our own
45
- // leftover pid (we cannot be inside our own synchronous section while here), or so old that pid reuse
46
- // is the only explanation. A live holder merely starved of CPU is NEVER stolen - stealing it would
47
- // put two writers in the critical section and silently drop an append (issue #249).
48
- const isCrashed = (holder, now) => {
39
+ // Never steal a live-but-CPU-starved holder: two writers in the section drop an append (#249).
40
+ const isCrashed = (holder, now, lim) => {
49
41
  const age = now - holder.at;
50
- if (age < LOCK_TTL_MS)
42
+ if (age < lim.ttlMs)
51
43
  return false;
52
- if (age >= STALE_HARD_MS)
44
+ if (age >= lim.hardStaleMs)
53
45
  return true;
54
46
  return holder.pid === process.pid || !holderAlive(holder.pid);
55
47
  };
@@ -61,10 +53,7 @@ const unlinkQuiet = (path) => {
61
53
  // already gone
62
54
  }
63
55
  };
64
- // Atomic exclusive create: writeFileSync('wx') is create-then-write, so a concurrent holderOf can
65
- // read the freshly created but still-empty lock and mistake a live holder for absent (issue #231
66
- // lost-update). Instead write the full "<pid> <at>" to a unique temp, then linkSync it into place:
67
- // link is atomic and fails EEXIST if held, so the content is always complete when the lock appears.
56
+ // Write-then-link: a reader can never see the lock before its "<pid> <at>" content (#231).
68
57
  const createLockExclusive = (path, content) => {
69
58
  const tmp = `${path}.${process.pid}.${randomBytes(4).toString('hex')}.new`;
70
59
  writeFileSync(tmp, content);
@@ -75,15 +64,12 @@ const createLockExclusive = (path, content) => {
75
64
  unlinkQuiet(tmp);
76
65
  }
77
66
  };
78
- // Serialize stale-lock takeover behind its own exclusive create so no taker can delete a lock that
79
- // turned FRESH mid-takeover (the ABA race: a reader sees the stale timestamp, a winner re-creates
80
- // the lock, the reader deletes the winner's file). Same O_EXCL-guard design proven in
81
- // update-lock.ts. Returns whether to re-race the create.
82
- const takeOverStale = (target, now) => {
67
+ // Takeover runs under its own exclusive guard so no taker deletes a lock that turned fresh (ABA).
68
+ const takeOverStale = (target, now, lim) => {
83
69
  const path = lockPath(target);
84
70
  const guard = `${path}.takeover`;
85
71
  const guardHeld = holderOf(guard);
86
- if (guardHeld !== null && isCrashed(guardHeld, now))
72
+ if (guardHeld !== null && isCrashed(guardHeld, now, lim))
87
73
  unlinkQuiet(guard);
88
74
  try {
89
75
  createLockExclusive(guard, `${process.pid} ${now}`);
@@ -93,10 +79,8 @@ const takeOverStale = (target, now) => {
93
79
  }
94
80
  try {
95
81
  const holder = holderOf(path);
96
- // A null holder means the lock file exists but its content did not read cleanly. Never treat that
97
- // as absent: clearing it here would steal a live holder still writing its bytes. Only a holder we
98
- // can positively prove crashed is taken over.
99
- if (holder === null || !isCrashed(holder, now))
82
+ // Unreadable content is not proof of a crash: only a provably crashed holder is cleared.
83
+ if (holder === null || !isCrashed(holder, now, lim))
100
84
  return false;
101
85
  unlinkQuiet(path);
102
86
  return true;
@@ -105,49 +89,56 @@ const takeOverStale = (target, now) => {
105
89
  unlinkQuiet(guard);
106
90
  }
107
91
  };
108
- // One-shot acquire: the atomic link IS the mutex - concurrent callers cannot both win it, and the
109
- // linked content is always complete so no reader sees an empty lock. A fresh or live holder refuses;
110
- // a crashed one (dead holder past the TTL) is cleared under the takeover guard and re-raced once.
111
- // `now` is injectable for tests.
112
- export const acquireFileLock = (target, now = Date.now()) => {
113
- mkdirSync(dirname(target), { recursive: true });
92
+ // Only EEXIST is contention; a permission error never clears, so it fails fast.
93
+ const lockWriteError = (err, path) => {
94
+ const code = err.code;
95
+ if (code === 'EEXIST')
96
+ return null;
97
+ if (code === 'EACCES' || code === 'EPERM' || code === 'EROFS')
98
+ return new Error(`permission denied: ${path}`);
99
+ return new Error(`could not write lock file ${path}${code ? ` (${code})` : ''}`);
100
+ };
101
+ const tryCreate = (path, now) => {
102
+ try {
103
+ createLockExclusive(path, `${process.pid} ${now}`);
104
+ return true;
105
+ }
106
+ catch (err) {
107
+ const fatal = lockWriteError(err, path);
108
+ if (fatal)
109
+ throw fatal;
110
+ return false;
111
+ }
112
+ };
113
+ // One-shot acquire: the atomic link IS the mutex; a crashed holder is cleared and re-raced once.
114
+ export const acquireFileLock = (target, now = Date.now(), opts = {}) => {
115
+ const lim = limitsOf(opts);
116
+ const path = lockPath(target);
117
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
114
118
  for (let attempt = 0; attempt < 2; attempt++) {
115
- try {
116
- createLockExclusive(lockPath(target), `${process.pid} ${now}`);
119
+ if (tryCreate(path, now))
117
120
  return true;
118
- }
119
- catch (err) {
120
- // Only EEXIST means contention. A permission/read-only error never clears, so failing fast
121
- // beats burning the full MAX_WAIT_MS before reporting a generic "could not acquire lock".
122
- const code = err.code;
123
- if (code !== 'EEXIST') {
124
- const path = lockPath(target);
125
- throw new Error(code === 'EACCES' || code === 'EPERM' || code === 'EROFS'
126
- ? `permission denied: ${path}`
127
- : `could not write lock file ${path}${code ? ` (${code})` : ''}`);
128
- }
129
- const holder = holderOf(lockPath(target));
130
- // Refuse unless the holder is positively crashed. A null holder (unreadable content) is NOT
131
- // proof of a crash: attempting takeover on it once stole a live holder's lock (issue #231).
132
- if (holder === null || !isCrashed(holder, now))
133
- return false;
134
- if (!takeOverStale(target, now))
135
- return false;
136
- }
121
+ const holder = holderOf(path);
122
+ if (holder === null || !isCrashed(holder, now, lim))
123
+ return false;
124
+ if (!takeOverStale(target, now, lim))
125
+ return false;
137
126
  }
138
127
  return false;
139
128
  };
140
- export const releaseFileLock = (target) => unlinkQuiet(lockPath(target));
129
+ // Only our own lock: a holder that took over after the hard-stale age keeps theirs.
130
+ export const releaseFileLock = (target) => {
131
+ const path = lockPath(target);
132
+ if (holderOf(path)?.pid === process.pid)
133
+ unlinkQuiet(path);
134
+ };
141
135
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
142
- // Run `fn` while holding the advisory lock for `target`, blocking with bounded backoff until the
143
- // lock is free (or a crashed holder is taken over past the TTL). `fn` MUST be synchronous: the lock
144
- // is held only across one synchronous critical section, so in-process callers can never both be
145
- // inside it. The whole read-modify-write belongs in `fn` - locking only the write still loses
146
- // updates across the read->write gap. Released in a finally, so a throw never wedges the lock.
147
- export const withFileLock = async (target, fn) => {
148
- const deadline = Date.now() + MAX_WAIT_MS;
136
+ // `fn` MUST be synchronous and hold the whole read-modify-write; released in a finally.
137
+ export const withFileLock = async (target, fn, opts = {}) => {
138
+ const { maxWaitMs } = limitsOf(opts);
139
+ const deadline = Date.now() + maxWaitMs;
149
140
  for (;;) {
150
- if (acquireFileLock(target)) {
141
+ if (acquireFileLock(target, Date.now(), opts)) {
151
142
  try {
152
143
  return fn();
153
144
  }
@@ -155,9 +146,8 @@ export const withFileLock = async (target, fn) => {
155
146
  releaseFileLock(target);
156
147
  }
157
148
  }
158
- if (Date.now() >= deadline) {
159
- throw new Error(`could not acquire lock for ${target} within ${MAX_WAIT_MS}ms`);
160
- }
149
+ if (Date.now() >= deadline)
150
+ throw new Error(`could not acquire lock for ${target} within ${maxWaitMs}ms`);
161
151
  await sleep(STEP_MS + Math.floor(Math.random() * STEP_MS));
162
152
  }
163
153
  };
@@ -0,0 +1,3 @@
1
+ export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
2
+ export declare const readJson: <T>(path: string, guard: (value: unknown) => value is T) => T | null;
3
+ export declare const writeJsonAtomic: (path: string, value: unknown, mode?: number) => void;
@@ -0,0 +1,43 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { chmodSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ // null on a missing, unparsable or wrong-shaped file: callers treat all three as "absent".
5
+ export const readJson = (path, guard) => {
6
+ let raw;
7
+ try {
8
+ raw = readFileSync(path, 'utf-8');
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ try {
14
+ const value = JSON.parse(raw);
15
+ return guard(value) ? value : null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ };
21
+ const unlinkQuiet = (path) => {
22
+ try {
23
+ unlinkSync(path);
24
+ }
25
+ catch {
26
+ // already gone
27
+ }
28
+ };
29
+ // Unique temp sibling + chmod before rename: never half-written, never briefly world-readable.
30
+ export const writeJsonAtomic = (path, value, mode = 0o600) => {
31
+ const tmp = `${path}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
32
+ let renamed = false;
33
+ try {
34
+ writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', { encoding: 'utf-8', mode });
35
+ chmodSync(tmp, mode);
36
+ renameSync(tmp, path);
37
+ renamed = true;
38
+ }
39
+ finally {
40
+ if (!renamed)
41
+ unlinkQuiet(tmp);
42
+ }
43
+ };
@@ -0,0 +1,10 @@
1
+ export declare const DEFAULT_TIMEOUT_MS = 10000;
2
+ export declare const AUTH_TIMEOUT_MS = 15000;
3
+ export type CliRequestInit = Omit<RequestInit, 'headers' | 'redirect' | 'signal'> & {
4
+ headers?: Record<string, string>;
5
+ };
6
+ export interface CliFetchOptions {
7
+ timeoutMs?: number;
8
+ }
9
+ export declare const cliFetch: (url: string, init?: CliRequestInit, opts?: CliFetchOptions) => Promise<Response>;
10
+ export declare const isRedirect: (res: Response) => boolean;
@@ -0,0 +1,13 @@
1
+ import { requestHeaders } from './user-agent.js';
2
+ export const DEFAULT_TIMEOUT_MS = 10_000;
3
+ export const AUTH_TIMEOUT_MS = 15_000;
4
+ // Captured at load: plugins loaded later must not be able to swap the fetch that carries tokens.
5
+ const baseFetch = globalThis.fetch;
6
+ // redirect: 'manual' so a bearer or form body is never replayed to a redirect target.
7
+ export const cliFetch = (url, init = {}, opts = {}) => baseFetch(url, {
8
+ ...init,
9
+ headers: { ...init.headers, ...requestHeaders() },
10
+ redirect: 'manual',
11
+ signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
12
+ });
13
+ export const isRedirect = (res) => res.type === 'opaqueredirect' || (res.status >= 300 && res.status < 400);
@@ -1,27 +1,55 @@
1
- import { get } from 'node:https';
2
- // node:https instead of global fetch: undici keeps a ref'd ~10s connect timer alive even after
3
- // its AbortSignal fires, stalling process exit on an unreachable network. req.destroy() from an
4
- // unref'd timer caps the whole attempt and frees the event loop the moment it settles. Never
5
- // throws: an unreachable host or malformed body resolves null (unreachable) or json:null.
6
- export const fetchJsonUnref = (url, timeoutMs, headers = {}) => new Promise((resolve) => {
7
- const req = get(url, { headers: { Accept: 'application/json', ...headers } }, (res) => {
8
- let body = '';
9
- res.setEncoding('utf8');
10
- res.on('data', (chunk) => {
11
- body += chunk;
12
- });
13
- res.on('end', () => {
14
- const status = res.statusCode ?? 0;
15
- let json = null;
16
- try {
17
- json = body ? JSON.parse(body) : null;
18
- }
19
- catch {
20
- json = null;
21
- }
22
- resolve({ ok: status >= 200 && status < 300, status, json });
23
- });
1
+ import { get as httpGet } from 'node:http';
2
+ import { get as httpsGet } from 'node:https';
3
+ // Loopback stubs (AGENTAGE_SITE_FQDN=127.0.0.1:<port>) are plain http; every real target is https.
4
+ const getterFor = (url) => {
5
+ try {
6
+ const { protocol } = new URL(url);
7
+ if (protocol === 'https:')
8
+ return httpsGet;
9
+ if (protocol === 'http:')
10
+ return httpGet;
11
+ }
12
+ catch {
13
+ // not a URL
14
+ }
15
+ return null;
16
+ };
17
+ const parseJson = (body) => {
18
+ try {
19
+ return body ? JSON.parse(body) : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ };
25
+ const collect = (res, resolve) => {
26
+ let body = '';
27
+ res.setEncoding('utf8');
28
+ res.on('data', (chunk) => {
29
+ body += chunk;
30
+ });
31
+ res.on('end', () => {
32
+ const status = res.statusCode ?? 0;
33
+ resolve({ ok: status >= 200 && status < 300, status, json: parseJson(body) });
24
34
  });
35
+ };
36
+ // A synchronous throw (e.g. an invalid header value) is a failed request, not a crash.
37
+ const start = (url, headers, resolve) => {
38
+ const get = getterFor(url);
39
+ if (!get)
40
+ return null;
41
+ try {
42
+ return get(url, { headers: { Accept: 'application/json', ...headers } }, (res) => collect(res, resolve));
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ };
48
+ // node:http(s), not fetch: undici's ref'd connect timer outlives an abort and stalls process exit.
49
+ export const fetchJsonUnref = (url, timeoutMs, headers = {}) => new Promise((resolve) => {
50
+ const req = start(url, headers, resolve);
51
+ if (!req)
52
+ return resolve(null);
25
53
  const timer = setTimeout(() => req.destroy(), timeoutMs);
26
54
  timer.unref();
27
55
  req.once('error', () => resolve(null));
@@ -3,8 +3,6 @@ export interface Links {
3
3
  site: string;
4
4
  api: string;
5
5
  auth: string;
6
- mcp: string;
7
- sync: string;
8
6
  }
9
7
  export declare const normalizeFqdn: (raw?: string) => string;
10
8
  export declare const siteFqdn: () => string;
@@ -20,20 +20,16 @@ export const environment = (fqdn) => isLocal(fqdn) || fqdn.startsWith('dev.') ?
20
20
  export const links = (fqdn) => {
21
21
  const stub = stubOrigin(fqdn);
22
22
  if (stub)
23
- return { site: stub, api: `${stub}/api`, auth: stub, mcp: `${stub}/mcp`, sync: stub };
23
+ return { site: stub, api: `${stub}/api`, auth: stub };
24
24
  return isLocal(fqdn)
25
25
  ? {
26
26
  site: 'http://localhost:3000',
27
27
  api: 'http://localhost:3001/api',
28
28
  auth: 'http://localhost:3010',
29
- mcp: 'http://localhost:3003/mcp',
30
- sync: 'http://localhost:3011',
31
29
  }
32
30
  : {
33
31
  site: `https://${fqdn}`,
34
32
  api: `https://api.${fqdn}/api`,
35
33
  auth: `https://auth.${fqdn}`,
36
- mcp: `https://memory.${fqdn}/mcp`,
37
- sync: `https://sync.${fqdn}`,
38
34
  };
39
35
  };
@@ -1,6 +1 @@
1
- export type RequestComponent = 'cli' | 'daemon';
2
- export interface RequestHeaderOptions {
3
- component: RequestComponent;
4
- daemonVersion?: string;
5
- }
6
- export declare const requestHeaders: (opts: RequestHeaderOptions) => Record<string, string>;
1
+ export declare const requestHeaders: () => Record<string, string>;
@@ -1,7 +1,6 @@
1
1
  import { VERSION } from '../../utils/version.js';
2
- // Identify the caller + versions so the server can observe which CLI + daemon connect.
3
- export const requestHeaders = (opts) => ({
4
- 'User-Agent': `agentage-${opts.component}/${VERSION}`,
2
+ // Identify the caller and its version so the server can observe which CLI connects.
3
+ export const requestHeaders = () => ({
4
+ 'User-Agent': `agentage-cli/${VERSION}`,
5
5
  'X-Agentage-CLI-Version': VERSION,
6
- 'X-Agentage-Daemon-Version': opts.component === 'daemon' ? VERSION : (opts.daemonVersion ?? 'none'),
7
6
  });
@@ -0,0 +1,17 @@
1
+ export interface NpmResult {
2
+ code: number;
3
+ stdout: string;
4
+ stderr: string;
5
+ }
6
+ export interface RunNpmOptions {
7
+ cwd?: string;
8
+ env?: NodeJS.ProcessEnv;
9
+ timeoutMs: number;
10
+ }
11
+ export interface ResolveNpmOptions {
12
+ execPath?: string;
13
+ env?: NodeJS.ProcessEnv;
14
+ }
15
+ export declare const resolveNpmCli: (opts?: ResolveNpmOptions) => string;
16
+ export declare const runNpm: (args: string[], opts: RunNpmOptions) => Promise<NpmResult>;
17
+ export declare const requireNpm10: (run?: typeof runNpm) => Promise<string>;
@@ -0,0 +1,77 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync, realpathSync } from 'node:fs';
3
+ import { delimiter, dirname, join } from 'node:path';
4
+ const NPM_CLI = join('npm', 'bin', 'npm-cli.js');
5
+ const NPM_MIN_MAJOR = 10;
6
+ const VERSION_TIMEOUT_MS = 30_000;
7
+ const MAX_BUFFER = 16 * 1024 * 1024;
8
+ // POSIX prefixes keep npm in <prefix>/lib/node_modules; the Windows installer puts it beside node.exe.
9
+ const bundledCandidates = (execPath) => [
10
+ join(dirname(execPath), '..', 'lib', 'node_modules', NPM_CLI),
11
+ join(dirname(execPath), 'node_modules', NPM_CLI),
12
+ ];
13
+ // Homebrew-style installs keep npm apart from node; their `npm` on PATH links to npm-cli.js.
14
+ const fromPath = (env) => {
15
+ for (const dir of (env['PATH'] ?? '').split(delimiter)) {
16
+ if (!dir)
17
+ continue;
18
+ try {
19
+ const real = realpathSync(join(dir, 'npm'));
20
+ if (real.endsWith('npm-cli.js'))
21
+ return real;
22
+ }
23
+ catch {
24
+ // no npm in this PATH entry
25
+ }
26
+ }
27
+ return undefined;
28
+ };
29
+ // npm runs as `node npm-cli.js`: no shell and no platform shim (npm.cmd) in between.
30
+ export const resolveNpmCli = (opts = {}) => {
31
+ const execPath = opts.execPath ?? process.execPath;
32
+ const env = opts.env ?? process.env;
33
+ const bundled = bundledCandidates(execPath).find((path) => existsSync(path));
34
+ if (bundled)
35
+ return bundled;
36
+ const fromEnv = env['npm_execpath'];
37
+ if (fromEnv?.endsWith('npm-cli.js'))
38
+ return fromEnv;
39
+ const onPath = fromPath(env);
40
+ if (onPath)
41
+ return onPath;
42
+ throw new Error(`npm-cli.js not found next to ${execPath}`);
43
+ };
44
+ const isScrubbed = (key) => /^npm_config_/i.test(key) || key.toUpperCase() === 'NODE_OPTIONS';
45
+ // Inherited npm_config_* (an `npx` or `npm run` parent) would redirect a global install.
46
+ const childEnv = (env = {}) => ({
47
+ ...Object.fromEntries(Object.entries(process.env).filter(([key]) => !isScrubbed(key))),
48
+ ...env,
49
+ });
50
+ const failureNote = (err, timeoutMs) => {
51
+ if (!err || typeof err.code === 'number')
52
+ return '';
53
+ if (err.killed)
54
+ return `\nnpm timed out after ${timeoutMs}ms`;
55
+ return `\nnpm could not run (${String(err.code ?? err.signal ?? 'unknown')})`;
56
+ };
57
+ export const runNpm = (args, opts) => new Promise((resolve) => {
58
+ const argv = [resolveNpmCli(), ...args, '--no-audit', '--no-fund'];
59
+ const options = {
60
+ cwd: opts.cwd,
61
+ env: childEnv(opts.env),
62
+ timeout: opts.timeoutMs,
63
+ maxBuffer: MAX_BUFFER,
64
+ windowsHide: true,
65
+ };
66
+ execFile(process.execPath, argv, options, (err, stdout, stderr) => {
67
+ const code = !err ? 0 : typeof err.code === 'number' ? err.code : 1;
68
+ resolve({ code, stdout, stderr: stderr + failureNote(err, opts.timeoutMs) });
69
+ });
70
+ });
71
+ export const requireNpm10 = async (run = runNpm) => {
72
+ const res = await run(['--version'], { timeoutMs: VERSION_TIMEOUT_MS });
73
+ const version = res.stdout.trim();
74
+ if (res.code !== 0 || !(Number.parseInt(version, 10) >= NPM_MIN_MAJOR))
75
+ throw new Error(`npm ${NPM_MIN_MAJOR} or newer is required (found ${version || 'none'})`);
76
+ return version;
77
+ };
@@ -2,22 +2,6 @@ import { type AuthEnvMismatch } from '../auth/env-match.js';
2
2
  import { type AuthState } from '../fs/config.js';
3
3
  import { type Env } from '../net/origins.js';
4
4
  import { type UpdateInfo } from '../update/update-check.js';
5
- import { type VaultStatus } from './vaults-status.js';
6
- export interface DaemonSyncSummary {
7
- vaults: number;
8
- state: 'ok' | 'error' | 'syncing';
9
- lastRun?: string;
10
- lastError?: string;
11
- }
12
- export interface DaemonStatus {
13
- running: boolean;
14
- pid?: number;
15
- port: number;
16
- uptimeSeconds?: number;
17
- mcp?: boolean;
18
- daemonVersion?: string;
19
- sync?: DaemonSyncSummary;
20
- }
21
5
  export interface StatusReport {
22
6
  version: string;
23
7
  fqdn: string;
@@ -40,7 +24,5 @@ export interface StatusReport {
40
24
  reachable: boolean;
41
25
  };
42
26
  update: UpdateInfo;
43
- daemon?: DaemonStatus;
44
- vaults: VaultStatus[];
45
27
  }
46
28
  export declare const gatherStatus: (auth: AuthState | null, fqdn: string) => Promise<StatusReport>;
@@ -1,13 +1,11 @@
1
1
  import { AuthRequiredError, introspectToken } from '../auth/api.js';
2
2
  import { detectEnvMismatch } from '../auth/env-match.js';
3
- import { health, syncStatus } from '../daemon/daemon-client.js';
3
+ import {} from '../fs/config.js';
4
4
  import { fetchJsonUnref } from '../net/http.js';
5
5
  import { requestHeaders } from '../net/user-agent.js';
6
6
  import { environment, links } from '../net/origins.js';
7
7
  import { checkForUpdate } from '../update/update-check.js';
8
8
  import { VERSION } from '../../utils/version.js';
9
- import { isDaemonRunning, resolvePort } from '../../daemon/lifecycle.js';
10
- import { buildVaultStatuses } from './vaults-status.js';
11
9
  // node:https via fetchJsonUnref, not global fetch: undici's ref'd connect timer keeps the process
12
10
  // alive ~10s after an aborted request on a packet-drop network, stalling `status` exit.
13
11
  const checkEndpoint = async (apiUrl, headers) => {
@@ -20,40 +18,6 @@ const checkSite = async (siteUrl, headers) => {
20
18
  const res = await fetchJsonUnref(siteUrl, 3000, headers);
21
19
  return res !== null;
22
20
  };
23
- // Fold the per-vault git states into one summary: any error wins, then any in-flight run, else ok.
24
- // lastRun is the freshest reported; lastError the first seen. Account vaults are absent by design -
25
- // they have no sync channel, so they are never counted as synced here (the vaults block names them).
26
- const summarizeSync = (sync) => {
27
- const git = Array.isArray(sync.vaults) ? sync.vaults : [];
28
- const error = git.find((v) => v.lastError)?.lastError;
29
- const running = git.some((v) => v.running);
30
- const state = error ? 'error' : running ? 'syncing' : 'ok';
31
- const lastRun = git
32
- .map((v) => v.lastRun)
33
- .filter((r) => Boolean(r))
34
- .sort()
35
- .at(-1);
36
- return { vaults: git.length, state, lastRun, lastError: error };
37
- };
38
- const probeDaemon = async () => {
39
- const port = resolvePort();
40
- const h = await health(port);
41
- if (!h)
42
- return { status: { running: isDaemonRunning(), port }, sync: null };
43
- const sync = await syncStatus(port);
44
- const summary = sync ? summarizeSync(sync) : undefined;
45
- const status = {
46
- running: true,
47
- pid: h.pid,
48
- port,
49
- uptimeSeconds: h.uptime,
50
- // Absent on a legacy daemon that predates the /mcp gate; treat undefined as serving (on).
51
- mcp: h.mcp !== false,
52
- daemonVersion: h.version,
53
- sync: summary && summary.vaults > 0 ? summary : undefined,
54
- };
55
- return { status, sync };
56
- };
57
21
  // AuthRequiredError = truly expired (terminal). TransientAuthError (or any non-terminal) = a blip
58
22
  // while we could not re-verify: never rendered as expired, never told to run setup, exit stays 0.
59
23
  const classifyAuthError = (err) => err instanceof AuthRequiredError
@@ -70,9 +34,7 @@ const mismatchAuth = (m) => ({
70
34
  export const gatherStatus = async (auth, fqdn) => {
71
35
  const target = links(fqdn);
72
36
  const env = environment(fqdn);
73
- // Probe the local daemon first (fast, 1s-capped /health) so its version can ride the probe headers.
74
- const probe = await probeDaemon();
75
- const headers = requestHeaders({ component: 'cli', daemonVersion: probe.status.daemonVersion });
37
+ const headers = requestHeaders();
76
38
  // Endpoint reachability and the (npm-registry) update check are independent - run them
77
39
  // together so `status` stays snappy.
78
40
  const [reachable, siteReachable, update] = await Promise.all([
@@ -88,8 +50,6 @@ export const gatherStatus = async (auth, fqdn) => {
88
50
  auth: { signedIn: false, note: 'not signed in - run: agentage setup' },
89
51
  endpoint: { url: target.api, reachable },
90
52
  update,
91
- daemon: probe.status,
92
- vaults: buildVaultStatuses(probe.sync, probe.status.running),
93
53
  };
94
54
  if (!auth)
95
55
  return report;
@@ -2,6 +2,7 @@ export interface UpdateCache {
2
2
  checkedAt: number;
3
3
  latest: string | null;
4
4
  }
5
+ export declare const isUpdateCache: (value: unknown) => value is UpdateCache;
5
6
  export declare const readUpdateCache: () => UpdateCache | null;
6
7
  export declare const updateHint: () => string | null;
7
8
  export declare const fetchLatestVersion: (timeoutMs: number, url?: string) => Promise<string | null>;