@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.
- package/README.md +53 -83
- package/dist/cli.js +5 -8
- package/dist/commands/auth/setup.d.ts +6 -16
- package/dist/commands/auth/setup.js +9 -139
- package/dist/commands/status/status.d.ts +4 -3
- package/dist/commands/status/status.js +4 -27
- package/dist/commands/update/update.d.ts +3 -10
- package/dist/commands/update/update.js +24 -41
- package/dist/core/flag.d.ts +1 -0
- package/dist/core/flag.js +1 -0
- package/dist/lib/auth/api.d.ts +3 -2
- package/dist/lib/auth/api.js +50 -56
- package/dist/lib/auth/env-match.js +1 -0
- package/dist/lib/auth/introspect.js +10 -11
- package/dist/lib/auth/oauth.d.ts +2 -0
- package/dist/lib/auth/oauth.js +30 -28
- package/dist/lib/auth/sign-in.d.ts +20 -0
- package/dist/lib/auth/sign-in.js +95 -0
- package/dist/lib/fs/config.d.ts +1 -0
- package/dist/lib/fs/config.js +14 -21
- package/dist/lib/fs/file-lock.d.ts +7 -2
- package/dist/lib/fs/file-lock.js +64 -74
- package/dist/lib/fs/json-file.d.ts +3 -0
- package/dist/lib/fs/json-file.js +43 -0
- package/dist/lib/net/fetch.d.ts +10 -0
- package/dist/lib/net/fetch.js +13 -0
- package/dist/lib/net/http.js +51 -23
- package/dist/lib/net/origins.d.ts +0 -2
- package/dist/lib/net/origins.js +1 -5
- package/dist/lib/net/user-agent.d.ts +1 -6
- package/dist/lib/net/user-agent.js +3 -4
- package/dist/lib/npm/npm-cli.d.ts +17 -0
- package/dist/lib/npm/npm-cli.js +77 -0
- package/dist/lib/status/status-info.d.ts +0 -18
- package/dist/lib/status/status-info.js +2 -42
- package/dist/lib/update/update-cache.d.ts +1 -0
- package/dist/lib/update/update-cache.js +7 -11
- package/dist/lib/update/update-check.js +1 -4
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +2 -0
- package/dist/program.d.ts +18 -1
- package/dist/program.js +75 -44
- package/package.json +17 -12
- package/dist/commands/daemon/daemon-cmd.d.ts +0 -2
- package/dist/commands/daemon/daemon-cmd.js +0 -107
- package/dist/commands/memory/memory-verbs.d.ts +0 -21
- package/dist/commands/memory/memory-verbs.js +0 -98
- package/dist/commands/memory/memory.d.ts +0 -2
- package/dist/commands/memory/memory.js +0 -54
- package/dist/commands/vault/vault-sync.d.ts +0 -13
- package/dist/commands/vault/vault-sync.js +0 -90
- package/dist/commands/vault/vault.d.ts +0 -22
- package/dist/commands/vault/vault.js +0 -129
- package/dist/daemon/actions.d.ts +0 -5
- package/dist/daemon/actions.js +0 -22
- package/dist/daemon/client-provider.d.ts +0 -2
- package/dist/daemon/client-provider.js +0 -16
- package/dist/daemon/entry.d.ts +0 -8
- package/dist/daemon/entry.js +0 -113
- package/dist/daemon/guards.d.ts +0 -3
- package/dist/daemon/guards.js +0 -13
- package/dist/daemon/lifecycle.d.ts +0 -17
- package/dist/daemon/lifecycle.js +0 -151
- package/dist/daemon/mcp-http.d.ts +0 -3
- package/dist/daemon/mcp-http.js +0 -34
- package/dist/daemon/server.d.ts +0 -23
- package/dist/daemon/server.js +0 -135
- package/dist/daemon-entry.d.ts +0 -1
- package/dist/daemon-entry.js +0 -9
- package/dist/lib/auth/provision.d.ts +0 -14
- package/dist/lib/auth/provision.js +0 -49
- package/dist/lib/daemon/daemon-client.d.ts +0 -37
- package/dist/lib/daemon/daemon-client.js +0 -136
- package/dist/lib/daemon/daemon-pref.d.ts +0 -2
- package/dist/lib/daemon/daemon-pref.js +0 -10
- package/dist/lib/memory/memory-client.d.ts +0 -30
- package/dist/lib/memory/memory-client.js +0 -98
- package/dist/lib/status/format.d.ts +0 -1
- package/dist/lib/status/format.js +0 -12
- package/dist/lib/status/vaults-format.d.ts +0 -2
- package/dist/lib/status/vaults-format.js +0 -49
- package/dist/lib/status/vaults-status.d.ts +0 -12
- package/dist/lib/status/vaults-status.js +0 -54
- package/dist/lib/update/update-lock.d.ts +0 -2
- package/dist/lib/update/update-lock.js +0 -109
- package/dist/lib/vault/vault-registry.d.ts +0 -12
- package/dist/lib/vault/vault-registry.js +0 -81
- package/dist/lib/vault/vaults.d.ts +0 -10
- package/dist/lib/vault/vaults.js +0 -66
- package/dist/lib/vault/vaults.schema.d.ts +0 -2
- package/dist/lib/vault/vaults.schema.js +0 -10
- package/dist/mcp/local-server.d.ts +0 -4
- package/dist/mcp/local-server.js +0 -26
- package/dist/observability.d.ts +0 -4
- package/dist/observability.js +0 -35
- package/dist/sync/discover/watcher.d.ts +0 -26
- package/dist/sync/discover/watcher.js +0 -159
- package/dist/sync/git/conflict.d.ts +0 -1
- package/dist/sync/git/conflict.js +0 -16
- package/dist/sync/git/cycle.d.ts +0 -18
- package/dist/sync/git/cycle.js +0 -158
- package/dist/sync/git/git-exec.d.ts +0 -22
- package/dist/sync/git/git-exec.js +0 -56
- package/dist/sync/git/manager.d.ts +0 -28
- package/dist/sync/git/manager.js +0 -114
- package/dist/sync/git/planner.d.ts +0 -15
- package/dist/sync/git/planner.js +0 -46
- package/dist/sync/git/remote-url.d.ts +0 -3
- package/dist/sync/git/remote-url.js +0 -30
package/dist/lib/fs/file-lock.js
CHANGED
|
@@ -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
|
-
//
|
|
5
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
45
|
-
|
|
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 <
|
|
42
|
+
if (age < lim.ttlMs)
|
|
51
43
|
return false;
|
|
52
|
-
if (age >=
|
|
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
|
-
//
|
|
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
|
-
//
|
|
79
|
-
|
|
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
|
-
//
|
|
97
|
-
|
|
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
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
createLockExclusive(lockPath(target), `${process.pid} ${now}`);
|
|
119
|
+
if (tryCreate(path, now))
|
|
117
120
|
return true;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 ${
|
|
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);
|
package/dist/lib/net/http.js
CHANGED
|
@@ -1,27 +1,55 @@
|
|
|
1
|
-
import { get } from 'node:
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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));
|
package/dist/lib/net/origins.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
3
|
-
export const requestHeaders = (
|
|
4
|
-
'User-Agent': `agentage
|
|
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 {
|
|
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
|
-
|
|
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>;
|