@agentage/cli 0.0.3 → 0.0.4

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 (158) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +67 -25
  3. package/dist/cli.d.ts +0 -1
  4. package/dist/cli.js +44 -4
  5. package/dist/commands/{setup.d.ts → auth/setup.d.ts} +6 -3
  6. package/dist/commands/{setup.js → auth/setup.js} +56 -11
  7. package/dist/commands/daemon/daemon-cmd.d.ts +2 -0
  8. package/dist/commands/daemon/daemon-cmd.js +124 -0
  9. package/dist/commands/memory/memory-verbs.d.ts +21 -0
  10. package/dist/commands/memory/memory-verbs.js +98 -0
  11. package/dist/commands/memory/memory.d.ts +2 -0
  12. package/dist/commands/memory/memory.js +54 -0
  13. package/dist/commands/{status.d.ts → status/status.d.ts} +1 -2
  14. package/dist/commands/status/status.js +86 -0
  15. package/dist/commands/update/update.d.ts +23 -0
  16. package/dist/commands/update/update.js +89 -0
  17. package/dist/commands/vault/vault-sync.d.ts +15 -0
  18. package/dist/commands/vault/vault-sync.js +100 -0
  19. package/dist/commands/vault/vault.d.ts +22 -0
  20. package/dist/commands/vault/vault.js +128 -0
  21. package/dist/daemon/actions.d.ts +5 -0
  22. package/dist/daemon/actions.js +22 -0
  23. package/dist/daemon/client-provider.d.ts +2 -0
  24. package/dist/daemon/client-provider.js +16 -0
  25. package/dist/daemon/guards.d.ts +3 -0
  26. package/dist/daemon/guards.js +13 -0
  27. package/dist/daemon/lifecycle.d.ts +17 -0
  28. package/dist/daemon/lifecycle.js +151 -0
  29. package/dist/daemon/mcp-http.d.ts +3 -0
  30. package/dist/daemon/mcp-http.js +34 -0
  31. package/dist/daemon/server.d.ts +26 -0
  32. package/dist/daemon/server.js +136 -0
  33. package/dist/daemon-entry.d.ts +7 -0
  34. package/dist/daemon-entry.js +128 -0
  35. package/dist/index.d.ts +0 -1
  36. package/dist/index.js +0 -1
  37. package/dist/lib/auth/api.d.ts +8 -0
  38. package/dist/lib/auth/api.js +97 -0
  39. package/dist/lib/auth/auth-errors.d.ts +7 -0
  40. package/dist/lib/auth/auth-errors.js +22 -0
  41. package/dist/lib/{callback-server.d.ts → auth/callback-server.d.ts} +0 -1
  42. package/dist/lib/{callback-server.js → auth/callback-server.js} +2 -2
  43. package/dist/lib/auth/env-match.d.ts +9 -0
  44. package/dist/lib/auth/env-match.js +17 -0
  45. package/dist/lib/auth/introspect.d.ts +7 -0
  46. package/dist/lib/auth/introspect.js +89 -0
  47. package/dist/lib/{oauth.d.ts → auth/oauth.d.ts} +6 -2
  48. package/dist/lib/{oauth.js → auth/oauth.js} +26 -7
  49. package/dist/lib/auth/provision.d.ts +14 -0
  50. package/dist/lib/auth/provision.js +62 -0
  51. package/dist/lib/daemon/daemon-client.d.ts +39 -0
  52. package/dist/lib/daemon/daemon-client.js +134 -0
  53. package/dist/lib/daemon/daemon-pref.d.ts +2 -0
  54. package/dist/lib/daemon/daemon-pref.js +10 -0
  55. package/dist/lib/{config.d.ts → fs/config.d.ts} +1 -1
  56. package/dist/lib/fs/config.js +55 -0
  57. package/dist/lib/fs/file-lock.d.ts +3 -0
  58. package/dist/lib/fs/file-lock.js +163 -0
  59. package/dist/lib/memory/memory-client.d.ts +30 -0
  60. package/dist/lib/memory/memory-client.js +98 -0
  61. package/dist/lib/net/http.d.ts +6 -0
  62. package/dist/lib/net/http.js +32 -0
  63. package/dist/lib/{origins.d.ts → net/origins.d.ts} +1 -1
  64. package/dist/lib/net/origins.js +39 -0
  65. package/dist/lib/net/user-agent.d.ts +6 -0
  66. package/dist/lib/net/user-agent.js +7 -0
  67. package/dist/lib/status/format.d.ts +1 -0
  68. package/dist/lib/status/format.js +12 -0
  69. package/dist/lib/status/status-info.d.ts +45 -0
  70. package/dist/lib/status/status-info.js +107 -0
  71. package/dist/lib/status/vaults-format.d.ts +2 -0
  72. package/dist/lib/status/vaults-format.js +44 -0
  73. package/dist/lib/status/vaults-status.d.ts +12 -0
  74. package/dist/lib/status/vaults-status.js +53 -0
  75. package/dist/lib/update/update-cache.d.ts +14 -0
  76. package/dist/lib/update/update-cache.js +63 -0
  77. package/dist/lib/update/update-check.d.ts +27 -0
  78. package/dist/lib/update/update-check.js +50 -0
  79. package/dist/lib/update/update-lock.d.ts +2 -0
  80. package/dist/lib/update/update-lock.js +109 -0
  81. package/dist/lib/vault/vault-registry.d.ts +12 -0
  82. package/dist/lib/vault/vault-registry.js +82 -0
  83. package/dist/lib/vault/vaults.d.ts +10 -0
  84. package/dist/lib/vault/vaults.js +66 -0
  85. package/dist/lib/vault/vaults.schema.d.ts +2 -0
  86. package/dist/lib/vault/vaults.schema.js +10 -0
  87. package/dist/mcp/local-server.d.ts +4 -0
  88. package/dist/mcp/local-server.js +26 -0
  89. package/dist/sync/couch/cycle.d.ts +2 -0
  90. package/dist/sync/couch/cycle.js +58 -0
  91. package/dist/sync/couch/discovery.d.ts +22 -0
  92. package/dist/sync/couch/discovery.js +34 -0
  93. package/dist/sync/couch/file-store.d.ts +2 -0
  94. package/dist/sync/couch/file-store.js +47 -0
  95. package/dist/sync/couch/local-commit.d.ts +2 -0
  96. package/dist/sync/couch/local-commit.js +24 -0
  97. package/dist/sync/couch/manager.d.ts +4 -0
  98. package/dist/sync/couch/manager.fixtures.d.ts +20 -0
  99. package/dist/sync/couch/manager.fixtures.js +56 -0
  100. package/dist/sync/couch/manager.js +126 -0
  101. package/dist/sync/couch/manager.types.d.ts +81 -0
  102. package/dist/sync/couch/manager.types.js +1 -0
  103. package/dist/sync/couch/mutation-target.d.ts +5 -0
  104. package/dist/sync/couch/mutation-target.js +33 -0
  105. package/dist/sync/couch/push-on-write.d.ts +3 -0
  106. package/dist/sync/couch/push-on-write.js +33 -0
  107. package/dist/sync/couch/state-store.d.ts +3 -0
  108. package/dist/sync/couch/state-store.js +28 -0
  109. package/dist/sync/couch/targets.d.ts +9 -0
  110. package/dist/sync/couch/targets.js +23 -0
  111. package/dist/sync/couch/wire.d.ts +6 -0
  112. package/dist/sync/couch/wire.js +16 -0
  113. package/dist/sync/discover/watcher.d.ts +26 -0
  114. package/dist/sync/discover/watcher.js +159 -0
  115. package/dist/sync/git/conflict.d.ts +1 -0
  116. package/dist/sync/git/conflict.js +16 -0
  117. package/dist/sync/git/cycle.d.ts +18 -0
  118. package/dist/sync/git/cycle.js +158 -0
  119. package/dist/sync/git/git-exec.d.ts +22 -0
  120. package/dist/sync/git/git-exec.js +56 -0
  121. package/dist/sync/git/manager.d.ts +30 -0
  122. package/dist/sync/git/manager.js +114 -0
  123. package/dist/sync/git/planner.d.ts +15 -0
  124. package/dist/sync/git/planner.js +46 -0
  125. package/dist/sync/git/remote-url.d.ts +3 -0
  126. package/dist/sync/git/remote-url.js +30 -0
  127. package/dist/utils/version.d.ts +0 -1
  128. package/dist/utils/version.js +0 -1
  129. package/package.json +20 -7
  130. package/dist/cli.d.ts.map +0 -1
  131. package/dist/cli.js.map +0 -1
  132. package/dist/commands/setup.d.ts.map +0 -1
  133. package/dist/commands/setup.js.map +0 -1
  134. package/dist/commands/status.d.ts.map +0 -1
  135. package/dist/commands/status.js +0 -36
  136. package/dist/commands/status.js.map +0 -1
  137. package/dist/index.d.ts.map +0 -1
  138. package/dist/index.js.map +0 -1
  139. package/dist/lib/api.d.ts +0 -12
  140. package/dist/lib/api.d.ts.map +0 -1
  141. package/dist/lib/api.js +0 -46
  142. package/dist/lib/api.js.map +0 -1
  143. package/dist/lib/callback-server.d.ts.map +0 -1
  144. package/dist/lib/callback-server.js.map +0 -1
  145. package/dist/lib/config.d.ts.map +0 -1
  146. package/dist/lib/config.js +0 -34
  147. package/dist/lib/config.js.map +0 -1
  148. package/dist/lib/oauth.d.ts.map +0 -1
  149. package/dist/lib/oauth.js.map +0 -1
  150. package/dist/lib/origins.d.ts.map +0 -1
  151. package/dist/lib/origins.js +0 -26
  152. package/dist/lib/origins.js.map +0 -1
  153. package/dist/lib/status-info.d.ts +0 -18
  154. package/dist/lib/status-info.d.ts.map +0 -1
  155. package/dist/lib/status-info.js +0 -39
  156. package/dist/lib/status-info.js.map +0 -1
  157. package/dist/utils/version.d.ts.map +0 -1
  158. package/dist/utils/version.js.map +0 -1
@@ -0,0 +1,163 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
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;
14
+ const STEP_MS = 25;
15
+ 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).
18
+ const holderOf = (path) => {
19
+ try {
20
+ const parts = readFileSync(path, 'utf-8').trim().split(/\s+/);
21
+ const at = Number.parseInt(parts[parts.length - 1] ?? '', 10);
22
+ if (Number.isNaN(at))
23
+ return null;
24
+ const pid = parts.length >= 2 ? Number.parseInt(parts[0] ?? '', 10) : 0;
25
+ return { pid: Number.isNaN(pid) ? 0 : pid, at };
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ };
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.
33
+ const holderAlive = (pid) => {
34
+ if (pid <= 0)
35
+ return false;
36
+ try {
37
+ process.kill(pid, 0);
38
+ return true;
39
+ }
40
+ catch (err) {
41
+ return err.code === 'EPERM';
42
+ }
43
+ };
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) => {
49
+ const age = now - holder.at;
50
+ if (age < LOCK_TTL_MS)
51
+ return false;
52
+ if (age >= STALE_HARD_MS)
53
+ return true;
54
+ return holder.pid === process.pid || !holderAlive(holder.pid);
55
+ };
56
+ const unlinkQuiet = (path) => {
57
+ try {
58
+ unlinkSync(path);
59
+ }
60
+ catch {
61
+ // already gone
62
+ }
63
+ };
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.
68
+ const createLockExclusive = (path, content) => {
69
+ const tmp = `${path}.${process.pid}.${randomBytes(4).toString('hex')}.new`;
70
+ writeFileSync(tmp, content);
71
+ try {
72
+ linkSync(tmp, path);
73
+ }
74
+ finally {
75
+ unlinkQuiet(tmp);
76
+ }
77
+ };
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) => {
83
+ const path = lockPath(target);
84
+ const guard = `${path}.takeover`;
85
+ const guardHeld = holderOf(guard);
86
+ if (guardHeld !== null && isCrashed(guardHeld, now))
87
+ unlinkQuiet(guard);
88
+ try {
89
+ createLockExclusive(guard, `${process.pid} ${now}`);
90
+ }
91
+ catch {
92
+ return false; // another taker is mid-takeover; let it win
93
+ }
94
+ try {
95
+ 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))
100
+ return false;
101
+ unlinkQuiet(path);
102
+ return true;
103
+ }
104
+ finally {
105
+ unlinkQuiet(guard);
106
+ }
107
+ };
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 });
114
+ for (let attempt = 0; attempt < 2; attempt++) {
115
+ try {
116
+ createLockExclusive(lockPath(target), `${process.pid} ${now}`);
117
+ 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
+ }
137
+ }
138
+ return false;
139
+ };
140
+ export const releaseFileLock = (target) => unlinkQuiet(lockPath(target));
141
+ 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;
149
+ for (;;) {
150
+ if (acquireFileLock(target)) {
151
+ try {
152
+ return fn();
153
+ }
154
+ finally {
155
+ releaseFileLock(target);
156
+ }
157
+ }
158
+ if (Date.now() >= deadline) {
159
+ throw new Error(`could not acquire lock for ${target} within ${MAX_WAIT_MS}ms`);
160
+ }
161
+ await sleep(STEP_MS + Math.floor(Math.random() * STEP_MS));
162
+ }
163
+ };
@@ -0,0 +1,30 @@
1
+ import { type EditInput, type ListResult, type MemoryView, type SearchResult, type VaultsConfig, type WriteResult } from '@agentage/memory-core';
2
+ export interface DeleteResult {
3
+ path: string;
4
+ deleted: boolean;
5
+ }
6
+ export declare const translateEngineMessage: (message: string) => string;
7
+ export interface VerbOptions {
8
+ vault?: string;
9
+ }
10
+ export interface SearchOptions extends VerbOptions {
11
+ folder?: string;
12
+ limit?: number;
13
+ tags?: string[];
14
+ cursor?: string;
15
+ }
16
+ export interface ListOptions extends VerbOptions {
17
+ depth?: 1 | 2;
18
+ tags?: string[];
19
+ }
20
+ export interface MemoryClient {
21
+ search(query: string, opts?: SearchOptions): Promise<SearchResult>;
22
+ read(ref: string, opts?: VerbOptions): Promise<MemoryView>;
23
+ write(ref: string, body: string, opts?: VerbOptions & {
24
+ frontmatter?: Record<string, unknown>;
25
+ }): Promise<WriteResult>;
26
+ edit(ref: string, op: Omit<EditInput, 'path'>, opts?: VerbOptions): Promise<WriteResult>;
27
+ list(folder: string | undefined, opts?: ListOptions): Promise<ListResult>;
28
+ delete(ref: string, opts?: VerbOptions): Promise<DeleteResult>;
29
+ }
30
+ export declare const createDirectClient: (config: VaultsConfig) => MemoryClient;
@@ -0,0 +1,98 @@
1
+ import { createRegistry, createRouter, } from '@agentage/memory-core';
2
+ // Engine (memory-core) messages leak MCP tool vocabulary (`memory__list`, ...) unfit for a terminal.
3
+ // Rewrite the known patterns to CLI vocabulary at this seam; do not fork the engine.
4
+ const ENGINE_MESSAGE_MAP = [
5
+ [
6
+ /Use memory__list with no folder to see available vaults\.?/g,
7
+ 'Run `agentage vault list` to see available vaults.',
8
+ ],
9
+ ];
10
+ export const translateEngineMessage = (message) => ENGINE_MESSAGE_MAP.reduce((m, [pattern, replacement]) => m.replace(pattern, replacement), message);
11
+ // --vault becomes an @-prefix so the router resolves it; an explicit @<vault>/... passes through.
12
+ const scopeRef = (ref, vault) => ref.startsWith('@') ? ref : vault ? `@${vault}/${ref}` : ref;
13
+ const scopeFolder = (folder, vault) => {
14
+ if (vault)
15
+ return folder ? `@${vault}/${folder}` : `@${vault}`;
16
+ return folder || undefined;
17
+ };
18
+ export const createDirectClient = (config) => {
19
+ let ctxP;
20
+ const ctx = () => {
21
+ if (!ctxP) {
22
+ ctxP = (async () => {
23
+ const registry = await createRegistry(config);
24
+ // Offline engine: only local (git working-copy) backends are usable here.
25
+ const local = registry.list().filter((h) => h.backend.capabilities().kind === 'local');
26
+ if (local.length === 0)
27
+ throw new Error('no local vaults registered - run `agentage vault add <name> --local`');
28
+ const def = registry.default();
29
+ const defaultHandle = def && local.some((h) => h.id === def.id) ? def : undefined;
30
+ return {
31
+ router: createRouter(local, defaultHandle),
32
+ multi: local.length > 1,
33
+ hasDefault: !!defaultHandle,
34
+ };
35
+ })();
36
+ ctxP.catch(() => (ctxP = undefined));
37
+ }
38
+ return ctxP;
39
+ };
40
+ // A single-target verb on a bare ref needs a vault when >1 is registered and none is default.
41
+ const requireTarget = (ref, c) => {
42
+ if (c.multi && !c.hasDefault && !ref.startsWith('@'))
43
+ throw new Error('multiple vaults - use --vault <name>, @<vault>/<path>, or set "default" in vaults.json');
44
+ };
45
+ return {
46
+ async search(query, opts = {}) {
47
+ const { router } = await ctx();
48
+ return router.search({
49
+ query,
50
+ folder: scopeFolder(opts.folder, opts.vault),
51
+ tags: opts.tags,
52
+ limit: opts.limit,
53
+ cursor: opts.cursor,
54
+ });
55
+ },
56
+ async read(ref, opts = {}) {
57
+ const c = await ctx();
58
+ const target = scopeRef(ref, opts.vault);
59
+ requireTarget(target, c);
60
+ const view = await c.router.read(target);
61
+ if (!view)
62
+ throw new Error(`not found: ${ref}`);
63
+ return view;
64
+ },
65
+ async write(ref, body, opts = {}) {
66
+ const c = await ctx();
67
+ const target = scopeRef(ref, opts.vault);
68
+ requireTarget(target, c);
69
+ return c.router.write({ path: target, body, frontmatter: opts.frontmatter });
70
+ },
71
+ async edit(ref, op, opts = {}) {
72
+ const c = await ctx();
73
+ const target = scopeRef(ref, opts.vault);
74
+ requireTarget(target, c);
75
+ const receipt = await c.router.edit({ ...op, path: target });
76
+ if (!receipt)
77
+ throw new Error(`not found: ${ref}`);
78
+ return receipt;
79
+ },
80
+ async list(folder, opts = {}) {
81
+ const { router } = await ctx();
82
+ return router.list({
83
+ folder: scopeFolder(folder, opts.vault),
84
+ depth: opts.depth,
85
+ tags: opts.tags,
86
+ });
87
+ },
88
+ async delete(ref, opts = {}) {
89
+ const c = await ctx();
90
+ const target = scopeRef(ref, opts.vault);
91
+ requireTarget(target, c);
92
+ const deleted = await c.router.delete(target);
93
+ if (!deleted)
94
+ throw new Error(`not found: ${ref}`);
95
+ return { path: target, deleted };
96
+ },
97
+ };
98
+ };
@@ -0,0 +1,6 @@
1
+ export interface UnrefResponse {
2
+ ok: boolean;
3
+ status: number;
4
+ json: unknown;
5
+ }
6
+ export declare const fetchJsonUnref: (url: string, timeoutMs: number, headers?: Record<string, string>) => Promise<UnrefResponse | null>;
@@ -0,0 +1,32 @@
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
+ });
24
+ });
25
+ const timer = setTimeout(() => req.destroy(), timeoutMs);
26
+ timer.unref();
27
+ req.once('error', () => resolve(null));
28
+ req.once('close', () => {
29
+ clearTimeout(timer);
30
+ resolve(null); // no-op when already resolved
31
+ });
32
+ });
@@ -4,9 +4,9 @@ export interface Links {
4
4
  api: string;
5
5
  auth: string;
6
6
  mcp: string;
7
+ sync: string;
7
8
  }
8
9
  export declare const normalizeFqdn: (raw?: string) => string;
9
10
  export declare const siteFqdn: () => string;
10
11
  export declare const environment: (fqdn: string) => Env;
11
12
  export declare const links: (fqdn: string) => Links;
12
- //# sourceMappingURL=origins.d.ts.map
@@ -0,0 +1,39 @@
1
+ const DEFAULT_FQDN = 'agentage.io';
2
+ export const normalizeFqdn = (raw) => {
3
+ const value = (raw ?? '')
4
+ .trim()
5
+ .toLowerCase()
6
+ .replace(/^https?:\/\//, '')
7
+ .replace(/\/+$/, '');
8
+ return value || DEFAULT_FQDN;
9
+ };
10
+ export const siteFqdn = () => normalizeFqdn(process.env['AGENTAGE_SITE_FQDN']);
11
+ const isLocal = (fqdn) => fqdn === 'localhost' || fqdn.startsWith('localhost:') || fqdn.startsWith('127.0.0.1');
12
+ // A loopback-IP-with-port fqdn (`127.0.0.1:<port>`) collapses every service onto that one
13
+ // origin, path-prefixed - the single-server stub convention the hermetic e2e drives. The bare
14
+ // `localhost` / `localhost:<port>` dev fqdns keep the multi-port dev mapping below untouched.
15
+ const stubOrigin = (fqdn) => {
16
+ const m = fqdn.match(/^127\.0\.0\.1:(\d+)$/);
17
+ return m ? `http://127.0.0.1:${m[1]}` : null;
18
+ };
19
+ export const environment = (fqdn) => isLocal(fqdn) || fqdn.startsWith('dev.') ? 'development' : 'production';
20
+ export const links = (fqdn) => {
21
+ const stub = stubOrigin(fqdn);
22
+ if (stub)
23
+ return { site: stub, api: `${stub}/api`, auth: stub, mcp: `${stub}/mcp`, sync: stub };
24
+ return isLocal(fqdn)
25
+ ? {
26
+ site: 'http://localhost:3000',
27
+ api: 'http://localhost:3001/api',
28
+ auth: 'http://localhost:3010',
29
+ mcp: 'http://localhost:3003/mcp',
30
+ sync: 'http://localhost:3011',
31
+ }
32
+ : {
33
+ site: `https://${fqdn}`,
34
+ api: `https://api.${fqdn}/api`,
35
+ auth: `https://auth.${fqdn}`,
36
+ mcp: `https://memory.${fqdn}/mcp`,
37
+ sync: `https://sync.${fqdn}`,
38
+ };
39
+ };
@@ -0,0 +1,6 @@
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>;
@@ -0,0 +1,7 @@
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}`,
5
+ 'X-Agentage-CLI-Version': VERSION,
6
+ 'X-Agentage-Daemon-Version': opts.component === 'daemon' ? VERSION : (opts.daemonVersion ?? 'none'),
7
+ });
@@ -0,0 +1 @@
1
+ export declare const formatUptime: (seconds: number) => string;
@@ -0,0 +1,12 @@
1
+ // Compact "Hh Mm" / "Mm Ss" uptime, matching the daemon and status rows. Seconds only under a
2
+ // minute; hours dropped once zero so short-lived daemons read cleanly.
3
+ export const formatUptime = (seconds) => {
4
+ const s = Math.max(0, Math.floor(seconds));
5
+ if (s < 60)
6
+ return `${s}s`;
7
+ const h = Math.floor(s / 3600);
8
+ const m = Math.floor((s % 3600) / 60);
9
+ if (h > 0)
10
+ return `${h}h ${m}m`;
11
+ return `${m}m ${s % 60}s`;
12
+ };
@@ -0,0 +1,45 @@
1
+ import { type AuthEnvMismatch } from '../auth/env-match.js';
2
+ import { type AuthState } from '../fs/config.js';
3
+ import { type Env } from '../net/origins.js';
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
+ export interface StatusReport {
22
+ version: string;
23
+ fqdn: string;
24
+ env: Env;
25
+ target: {
26
+ fqdn: string;
27
+ env: Env;
28
+ reachable: boolean;
29
+ };
30
+ auth: {
31
+ signedIn: boolean;
32
+ tokenExpiresAt?: string;
33
+ note?: string;
34
+ transient?: boolean;
35
+ mismatch?: AuthEnvMismatch;
36
+ };
37
+ endpoint: {
38
+ url: string;
39
+ reachable: boolean;
40
+ };
41
+ update: UpdateInfo;
42
+ daemon?: DaemonStatus;
43
+ vaults: VaultStatus[];
44
+ }
45
+ export declare const gatherStatus: (auth: AuthState | null, fqdn: string) => Promise<StatusReport>;
@@ -0,0 +1,107 @@
1
+ import { AuthRequiredError, introspectToken } from '../auth/api.js';
2
+ import { detectEnvMismatch } from '../auth/env-match.js';
3
+ import { health, syncStatus } from '../daemon/daemon-client.js';
4
+ import { fetchJsonUnref } from '../net/http.js';
5
+ import { requestHeaders } from '../net/user-agent.js';
6
+ import { environment, links } from '../net/origins.js';
7
+ import { checkForUpdate } from '../update/update-check.js';
8
+ import { VERSION } from '../../utils/version.js';
9
+ import { isDaemonRunning, resolvePort } from '../../daemon/lifecycle.js';
10
+ import { buildVaultStatuses } from './vaults-status.js';
11
+ // node:https via fetchJsonUnref, not global fetch: undici's ref'd connect timer keeps the process
12
+ // alive ~10s after an aborted request on a packet-drop network, stalling `status` exit.
13
+ const checkEndpoint = async (apiUrl, headers) => {
14
+ const res = await fetchJsonUnref(`${apiUrl}/health`, 3000, headers);
15
+ return res?.ok ?? false;
16
+ };
17
+ // Host reachability, not app health: the site root answers any HTTP status (200/3xx/4xx) when the
18
+ // host is up, so any non-null response counts as reachable; only a refused/timed-out connect fails.
19
+ const checkSite = async (siteUrl, headers) => {
20
+ const res = await fetchJsonUnref(siteUrl, 3000, headers);
21
+ return res !== null;
22
+ };
23
+ // Fold the git + couch per-vault states into one summary: any error wins, then any in-flight
24
+ // run, else ok. lastRun is the freshest reported; lastError the first seen.
25
+ const summarizeSync = (sync) => {
26
+ const git = Array.isArray(sync.vaults) ? sync.vaults : [];
27
+ const couch = Array.isArray(sync.couch) ? sync.couch : [];
28
+ const vaults = git.length + couch.length;
29
+ const error = git.find((v) => v.lastError)?.lastError ?? couch.find((v) => v.lastError)?.lastError;
30
+ const running = git.some((v) => v.running) || couch.some((v) => v.running);
31
+ const state = error ? 'error' : running ? 'syncing' : 'ok';
32
+ const runs = [...git.map((v) => v.lastRun), ...couch.map((v) => v.lastSync)].filter((r) => Boolean(r));
33
+ const lastRun = runs.sort().at(-1);
34
+ return { vaults, state, lastRun, lastError: error };
35
+ };
36
+ const probeDaemon = async () => {
37
+ const port = resolvePort();
38
+ const h = await health(port);
39
+ if (!h)
40
+ return { status: { running: isDaemonRunning(), port }, sync: null };
41
+ const sync = await syncStatus(port);
42
+ const summary = sync ? summarizeSync(sync) : undefined;
43
+ const status = {
44
+ running: true,
45
+ pid: h.pid,
46
+ port,
47
+ uptimeSeconds: h.uptime,
48
+ // Absent on a legacy daemon that predates the /mcp gate; treat undefined as serving (on).
49
+ mcp: h.mcp !== false,
50
+ daemonVersion: h.version,
51
+ sync: summary && summary.vaults > 0 ? summary : undefined,
52
+ };
53
+ return { status, sync };
54
+ };
55
+ // AuthRequiredError = truly expired (terminal). TransientAuthError (or any non-terminal) = a blip
56
+ // while we could not re-verify: never rendered as expired, never told to run setup, exit stays 0.
57
+ const classifyAuthError = (err) => err instanceof AuthRequiredError
58
+ ? { signedIn: false, note: 'session expired - run: agentage setup' }
59
+ : { signedIn: true, transient: true, note: 'signed in (could not re-verify - temporary)' };
60
+ // A credential issued for one environment, checked against another, is not expired - it belongs
61
+ // elsewhere. Name both sides and hint the two fixes, without introspecting cross-environment.
62
+ const mismatchAuth = (m) => ({
63
+ signedIn: false,
64
+ mismatch: m,
65
+ note: `signed in to ${m.credentialFqdn} - CLI targets ${m.targetFqdn} (${m.targetEnv}); ` +
66
+ `run: agentage setup to sign into ${m.targetEnv}, or set AGENTAGE_SITE_FQDN=${m.credentialFqdn}`,
67
+ });
68
+ export const gatherStatus = async (auth, fqdn) => {
69
+ const target = links(fqdn);
70
+ const env = environment(fqdn);
71
+ // Probe the local daemon first (fast, 1s-capped /health) so its version can ride the probe headers.
72
+ const probe = await probeDaemon();
73
+ const headers = requestHeaders({ component: 'cli', daemonVersion: probe.status.daemonVersion });
74
+ // Endpoint reachability and the (npm-registry) update check are independent - run them
75
+ // together so `status` stays snappy.
76
+ const [reachable, siteReachable, update] = await Promise.all([
77
+ checkEndpoint(target.api, headers),
78
+ checkSite(target.site, headers),
79
+ checkForUpdate(VERSION),
80
+ ]);
81
+ const report = {
82
+ version: VERSION,
83
+ fqdn,
84
+ env,
85
+ target: { fqdn, env, reachable: siteReachable },
86
+ auth: { signedIn: false, note: 'not signed in - run: agentage setup' },
87
+ endpoint: { url: target.api, reachable },
88
+ update,
89
+ daemon: probe.status,
90
+ vaults: buildVaultStatuses(probe.sync, probe.status.running),
91
+ };
92
+ if (!auth)
93
+ return report;
94
+ const mismatch = detectEnvMismatch(auth, fqdn);
95
+ if (mismatch) {
96
+ report.auth = mismatchAuth(mismatch);
97
+ return report;
98
+ }
99
+ try {
100
+ const session = await introspectToken(auth, target);
101
+ report.auth = { signedIn: true, tokenExpiresAt: session.expiresAt };
102
+ }
103
+ catch (err) {
104
+ report.auth = classifyAuthError(err);
105
+ }
106
+ return report;
107
+ };
@@ -0,0 +1,2 @@
1
+ import { type VaultStatus } from './vaults-status.js';
2
+ export declare const vaultLines: (vaults: VaultStatus[]) => string[];
@@ -0,0 +1,44 @@
1
+ import chalk from 'chalk';
2
+ const mark = (good) => (good ? chalk.green('✓') : chalk.red('✗'));
3
+ // HH:MM in local time; a malformed timestamp falls back to the raw string so we never crash.
4
+ const shortTime = (iso) => {
5
+ if (!iso)
6
+ return '';
7
+ const d = new Date(iso);
8
+ return Number.isNaN(d.getTime())
9
+ ? iso
10
+ : `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
11
+ };
12
+ const shortError = (err) => (err ?? 'sync failed').split('\n')[0]?.slice(0, 40) ?? 'sync failed';
13
+ // One right-hand status cell per vault, sharing the check/cross marks with the other status rows.
14
+ const statusCell = (v) => {
15
+ switch (v.status) {
16
+ case 'ok':
17
+ return `${mark(true)} last ok ${shortTime(v.lastRun)}`.trimEnd();
18
+ case 'syncing':
19
+ return `${chalk.yellow('⋯')} syncing`;
20
+ case 'error':
21
+ return `${mark(false)} error (${shortError(v.lastError)})`;
22
+ case 'unknown':
23
+ return chalk.dim('- unknown (daemon stopped)');
24
+ case 'idle':
25
+ return v.channel === 'local' ? chalk.dim('- local only') : chalk.dim('- idle');
26
+ }
27
+ };
28
+ const countLabel = (vaults) => {
29
+ const connected = vaults.filter((v) => v.channel !== 'local').length;
30
+ if (connected > 0)
31
+ return `${connected} connected`;
32
+ const n = vaults.length;
33
+ return `${n} ${n === 1 ? 'vault' : 'vaults'}`;
34
+ };
35
+ // The `vaults` block: a header count line then one aligned `<name> <channel> <status>` row per vault.
36
+ // Zero vaults prints a single actionable hint instead of an empty block.
37
+ export const vaultLines = (vaults) => {
38
+ if (vaults.length === 0)
39
+ return [`${'vaults'.padEnd(10)} none - run: agentage vault add <name> --local`];
40
+ const nameW = Math.max(...vaults.map((v) => v.name.length));
41
+ const header = `${'vaults'.padEnd(10)} ${countLabel(vaults)}`;
42
+ const rows = vaults.map((v) => ` ${v.name.padEnd(nameW)} ${v.channel.padEnd(6)} ${statusCell(v)}`);
43
+ return [header, ...rows];
44
+ };
@@ -0,0 +1,12 @@
1
+ import { type VaultsConfig } from '@agentage/memory-core';
2
+ import { type SyncStatus } from '../../sync/git/manager.js';
3
+ export type VaultChannel = 'local' | 'git' | 'cloud';
4
+ export type VaultSyncState = 'ok' | 'syncing' | 'error' | 'idle' | 'unknown';
5
+ export interface VaultStatus {
6
+ name: string;
7
+ channel: VaultChannel;
8
+ status: VaultSyncState;
9
+ lastRun?: string;
10
+ lastError?: string;
11
+ }
12
+ export declare const buildVaultStatuses: (sync: SyncStatus | null, daemonUp: boolean, config?: VaultsConfig) => VaultStatus[];