@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/daemon/server.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import { dispatchMemory, isMemoryVerb } from './actions.js';
|
|
3
|
-
import { isAllowedHost, isAllowedOrigin, loopbackHosts } from './guards.js';
|
|
4
|
-
import { handleMcp } from './mcp-http.js';
|
|
5
|
-
const LOOPBACK = '127.0.0.1';
|
|
6
|
-
const AUTH_HEADER = 'x-agentage-token';
|
|
7
|
-
const readBody = (req) => new Promise((resolve, reject) => {
|
|
8
|
-
const chunks = [];
|
|
9
|
-
req.on('data', (c) => chunks.push(c));
|
|
10
|
-
req.on('end', () => {
|
|
11
|
-
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
12
|
-
if (raw.length === 0)
|
|
13
|
-
return resolve({});
|
|
14
|
-
try {
|
|
15
|
-
resolve(JSON.parse(raw));
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
reject(new Error('invalid JSON body'));
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
req.on('error', reject);
|
|
22
|
-
});
|
|
23
|
-
const send = (res, status, body) => {
|
|
24
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
25
|
-
res.end(JSON.stringify(body));
|
|
26
|
-
};
|
|
27
|
-
const isJsonContentType = (req) => (req.headers['content-type'] ?? '').split(';')[0]?.trim() === 'application/json';
|
|
28
|
-
const header = (req, name) => {
|
|
29
|
-
const v = req.headers[name];
|
|
30
|
-
return Array.isArray(v) ? v[0] : v;
|
|
31
|
-
};
|
|
32
|
-
// Loopback-only JSON HTTP: GET /api/health + POST /api/memory/<verb> (the CLI verbs) + POST /mcp
|
|
33
|
-
// (the frozen 6 tools for on-machine AI clients, stateless Streamable HTTP). Web-origin defense on
|
|
34
|
-
// EVERY request (Host + Origin allow-lists + DNS-rebinding protection on /mcp); /api/* additionally
|
|
35
|
-
// needs the per-daemon token, while /health and /mcp stay tokenless for probes and editor clients.
|
|
36
|
-
export const createDaemonServer = (opts) => {
|
|
37
|
-
const startedAt = opts.startedAt ?? Date.now();
|
|
38
|
-
let served = 0;
|
|
39
|
-
let boundPort = 0;
|
|
40
|
-
// 403 unless the request looks like it came from a genuine same-machine loopback client.
|
|
41
|
-
const originGuard = (req) => isAllowedHost(header(req, 'host'), boundPort) && isAllowedOrigin(header(req, 'origin'));
|
|
42
|
-
const handle = async (req, res) => {
|
|
43
|
-
const url = req.url ?? '/';
|
|
44
|
-
if (!originGuard(req))
|
|
45
|
-
return send(res, 403, { error: 'forbidden' });
|
|
46
|
-
const path = url.split('?')[0] ?? url;
|
|
47
|
-
const isApi = path.startsWith('/api/') && path !== '/api/health';
|
|
48
|
-
if (isApi && header(req, AUTH_HEADER) !== opts.authToken) {
|
|
49
|
-
return send(res, 401, { error: 'unauthorized' });
|
|
50
|
-
}
|
|
51
|
-
if (isApi && req.method === 'POST' && !isJsonContentType(req)) {
|
|
52
|
-
return send(res, 415, { error: 'unsupported media type' });
|
|
53
|
-
}
|
|
54
|
-
if (req.method === 'GET' && url === '/api/health') {
|
|
55
|
-
return send(res, 200, {
|
|
56
|
-
ok: true,
|
|
57
|
-
version: opts.version,
|
|
58
|
-
pid: process.pid,
|
|
59
|
-
uptime: Math.floor((Date.now() - startedAt) / 1000),
|
|
60
|
-
served,
|
|
61
|
-
mcp: !!opts.buildMcpServer,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
if (path === '/mcp') {
|
|
65
|
-
if (!opts.buildMcpServer)
|
|
66
|
-
return send(res, 404, { error: 'not found' });
|
|
67
|
-
return handleMcp(req, res, opts.buildMcpServer, loopbackHosts(boundPort));
|
|
68
|
-
}
|
|
69
|
-
if (req.method === 'GET' && url === '/api/sync/status') {
|
|
70
|
-
if (!opts.sync)
|
|
71
|
-
return send(res, 404, { error: 'not found' });
|
|
72
|
-
return send(res, 200, opts.sync.status());
|
|
73
|
-
}
|
|
74
|
-
if (req.method === 'POST' && url === '/api/sync/run') {
|
|
75
|
-
if (!opts.sync)
|
|
76
|
-
return send(res, 404, { error: 'not found' });
|
|
77
|
-
const body = (await readBody(req));
|
|
78
|
-
if (!body.vault)
|
|
79
|
-
return send(res, 400, { error: 'vault is required' });
|
|
80
|
-
try {
|
|
81
|
-
return send(res, 200, await opts.sync.runNow(body.vault));
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
return send(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const match = url.match(/^\/api\/memory\/([a-z]+)$/);
|
|
88
|
-
if (req.method === 'POST' && match) {
|
|
89
|
-
const verb = match[1];
|
|
90
|
-
if (!isMemoryVerb(verb))
|
|
91
|
-
return send(res, 404, { error: `unknown verb: ${verb}` });
|
|
92
|
-
try {
|
|
93
|
-
const body = await readBody(req);
|
|
94
|
-
const result = await dispatchMemory(await opts.getClient(), verb, body);
|
|
95
|
-
served += 1;
|
|
96
|
-
return send(res, 200, result);
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
return send(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
send(res, 404, { error: 'not found' });
|
|
103
|
-
};
|
|
104
|
-
const server = createServer((req, res) => {
|
|
105
|
-
handle(req, res).catch((err) => send(res, 500, { error: err instanceof Error ? err.message : String(err) }));
|
|
106
|
-
});
|
|
107
|
-
const start = (port, host = LOOPBACK) => new Promise((resolve, reject) => {
|
|
108
|
-
const onError = (err) => {
|
|
109
|
-
if (err.code !== 'EADDRINUSE')
|
|
110
|
-
return reject(err);
|
|
111
|
-
// Preserve the code so the entry point can exit distinctly on a busy port.
|
|
112
|
-
const busy = new Error(`port ${port} already in use`);
|
|
113
|
-
busy.code = 'EADDRINUSE';
|
|
114
|
-
reject(busy);
|
|
115
|
-
};
|
|
116
|
-
server.once('error', onError);
|
|
117
|
-
server.listen(port, host, () => {
|
|
118
|
-
const addr = server.address();
|
|
119
|
-
boundPort = typeof addr === 'object' && addr ? addr.port : port;
|
|
120
|
-
server.removeListener('error', onError);
|
|
121
|
-
resolve();
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
// close() alone hangs on idle keep-alive sockets; drop idle ones now, force the rest after a grace.
|
|
125
|
-
const stop = () => new Promise((resolve) => {
|
|
126
|
-
server.closeIdleConnections();
|
|
127
|
-
const grace = setTimeout(() => server.closeAllConnections(), 500);
|
|
128
|
-
grace.unref();
|
|
129
|
-
server.close(() => {
|
|
130
|
-
clearTimeout(grace);
|
|
131
|
-
resolve();
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
return { server, start, stop };
|
|
135
|
-
};
|
package/dist/daemon-entry.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/daemon-entry.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
// The daemon's bin, a loader and nothing else: @agentage/observability instruments node:http and
|
|
2
|
-
// MCP tool registration through module hooks that only see modules imported after they register, so
|
|
3
|
-
// the bootstrap runs before the daemon itself is loaded. The path stays src/daemon-entry.ts because
|
|
4
|
-
// spawnDaemon resolves dist/daemon-entry.js.
|
|
5
|
-
const { DAEMON_SERVICE, startObservability } = await import('./observability.js');
|
|
6
|
-
await startObservability(DAEMON_SERVICE);
|
|
7
|
-
const { boot } = await import('./daemon/entry.js');
|
|
8
|
-
boot();
|
|
9
|
-
export {};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type AuthState } from '../fs/config.js';
|
|
2
|
-
import { type Links } from '../net/origins.js';
|
|
3
|
-
export type ProvisionStatus = 'provisioned' | 'exists' | 'unauthenticated' | 'offline';
|
|
4
|
-
export interface ProvisionResult {
|
|
5
|
-
status: ProvisionStatus;
|
|
6
|
-
message: string;
|
|
7
|
-
}
|
|
8
|
-
export interface ProvisionDeps {
|
|
9
|
-
readAuth: () => AuthState | null;
|
|
10
|
-
links: () => Links;
|
|
11
|
-
post: (auth: AuthState, links: Links, url: string, body: unknown) => Promise<Response>;
|
|
12
|
-
}
|
|
13
|
-
export declare const defaultProvisionDeps: () => ProvisionDeps;
|
|
14
|
-
export declare const provisionAccountVault: (name: string, deps?: ProvisionDeps) => Promise<ProvisionResult>;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { authedPost } from './api.js';
|
|
2
|
-
import { readAuth } from '../fs/config.js';
|
|
3
|
-
import { links as buildLinks, siteFqdn } from '../net/origins.js';
|
|
4
|
-
export const defaultProvisionDeps = () => ({
|
|
5
|
-
readAuth,
|
|
6
|
-
links: () => buildLinks(siteFqdn()),
|
|
7
|
-
post: authedPost,
|
|
8
|
-
});
|
|
9
|
-
const registeredLocally = (name, tail) => `Vault '${name}' registered locally${tail}`;
|
|
10
|
-
export const provisionAccountVault = async (name, deps = defaultProvisionDeps()) => {
|
|
11
|
-
const auth = deps.readAuth();
|
|
12
|
-
if (!auth) {
|
|
13
|
-
return {
|
|
14
|
-
status: 'unauthenticated',
|
|
15
|
-
message: registeredLocally(name, ' - run `agentage setup` to create it in your account.'),
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
// A PAT is an MCP-surface credential; the backend REST provisioning endpoint rejects plain
|
|
19
|
-
// bearers (only session cookies), so it cannot create the account memory. Fail clearly.
|
|
20
|
-
if (auth.kind === 'pat') {
|
|
21
|
-
return {
|
|
22
|
-
status: 'unauthenticated',
|
|
23
|
-
message: registeredLocally(name, ' - account provisioning needs an interactive session (run `agentage setup`); ' +
|
|
24
|
-
'a personal access token only authorizes memory (MCP) calls.'),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
const links = deps.links();
|
|
28
|
-
let res;
|
|
29
|
-
try {
|
|
30
|
-
res = await deps.post(auth, links, `${links.api}/memories`, { name });
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return {
|
|
34
|
-
status: 'offline',
|
|
35
|
-
message: registeredLocally(name, ' - will provision when online.'),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
if (res.status === 201)
|
|
39
|
-
return { status: 'provisioned', message: `Provisioned account vault '${name}'.` };
|
|
40
|
-
if (res.status === 200)
|
|
41
|
-
return { status: 'exists', message: `Account vault '${name}' already provisioned.` };
|
|
42
|
-
if (res.status === 401)
|
|
43
|
-
return {
|
|
44
|
-
status: 'unauthenticated',
|
|
45
|
-
message: registeredLocally(name, ' - run `agentage setup` to create it in your account.'),
|
|
46
|
-
};
|
|
47
|
-
// Any other status stays non-fatal: keep the local entry, let the daemon retry later.
|
|
48
|
-
return { status: 'offline', message: registeredLocally(name, ' - will provision when online.') };
|
|
49
|
-
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { type SyncResult } from '../../sync/git/cycle.js';
|
|
2
|
-
import { type SyncStatus } from '../../sync/git/manager.js';
|
|
3
|
-
import { type MemoryClient } from '../memory/memory-client.js';
|
|
4
|
-
export interface Health {
|
|
5
|
-
ok: boolean;
|
|
6
|
-
version: string;
|
|
7
|
-
pid: number;
|
|
8
|
-
uptime: number;
|
|
9
|
-
served: number;
|
|
10
|
-
mcp?: boolean;
|
|
11
|
-
}
|
|
12
|
-
export declare const health: (port: number, timeoutMs?: number) => Promise<Health | null>;
|
|
13
|
-
export declare const syncStatus: (port: number, timeoutMs?: number) => Promise<SyncStatus | null>;
|
|
14
|
-
export declare const syncRun: (port: number, vault: string) => Promise<SyncResult>;
|
|
15
|
-
export declare const waitForHealth: (port: number, opts?: {
|
|
16
|
-
timeoutMs?: number;
|
|
17
|
-
intervalMs?: number;
|
|
18
|
-
}) => Promise<boolean>;
|
|
19
|
-
export declare const createDaemonClient: (port: number) => MemoryClient;
|
|
20
|
-
export declare const mismatchNotice: (daemonVersion: string) => string | null;
|
|
21
|
-
export type SpawnOutcome = {
|
|
22
|
-
ok: true;
|
|
23
|
-
} | {
|
|
24
|
-
ok: false;
|
|
25
|
-
reason: 'port-in-use' | 'unreachable' | 'blocked';
|
|
26
|
-
};
|
|
27
|
-
export declare const spawnDaemon: (port: number, opts?: {
|
|
28
|
-
timeoutMs?: number;
|
|
29
|
-
noMcp?: boolean;
|
|
30
|
-
}) => Promise<SpawnOutcome>;
|
|
31
|
-
export interface EnsureDeps {
|
|
32
|
-
port?: number;
|
|
33
|
-
probe?: (port: number) => Promise<Health | null>;
|
|
34
|
-
spawn?: (port: number) => Promise<SpawnOutcome>;
|
|
35
|
-
readToken?: () => string | null;
|
|
36
|
-
}
|
|
37
|
-
export declare const ensureDaemon: (deps?: EnsureDeps) => Promise<MemoryClient | null>;
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { spawn as spawnChild } from 'node:child_process';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { EADDRINUSE_EXIT_CODE, readDaemonToken, resolvePort } from '../../daemon/lifecycle.js';
|
|
5
|
-
import { daemonServiceName } from '../../observability.js';
|
|
6
|
-
import { VERSION } from '../../utils/version.js';
|
|
7
|
-
const base = (port) => `http://127.0.0.1:${port}`;
|
|
8
|
-
// The 0600 token file is the daemon's auth: read it fresh per call so a restart's new token is
|
|
9
|
-
// picked up; absent means we cannot authenticate, so /api/* will 401 and callers fall back.
|
|
10
|
-
const apiHeaders = () => {
|
|
11
|
-
const token = readDaemonToken();
|
|
12
|
-
return { 'Content-Type': 'application/json', ...(token ? { 'X-Agentage-Token': token } : {}) };
|
|
13
|
-
};
|
|
14
|
-
const post = async (port, verb, body) => {
|
|
15
|
-
const res = await fetch(`${base(port)}/api/memory/${verb}`, {
|
|
16
|
-
method: 'POST',
|
|
17
|
-
headers: apiHeaders(),
|
|
18
|
-
body: JSON.stringify(body),
|
|
19
|
-
});
|
|
20
|
-
if (!res.ok) {
|
|
21
|
-
const data = (await res.json().catch(() => ({})));
|
|
22
|
-
throw new Error(data.error || `daemon request failed: ${res.status}`);
|
|
23
|
-
}
|
|
24
|
-
return res.json();
|
|
25
|
-
};
|
|
26
|
-
export const health = async (port, timeoutMs = 1000) => {
|
|
27
|
-
try {
|
|
28
|
-
const res = await fetch(`${base(port)}/api/health`, {
|
|
29
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
30
|
-
});
|
|
31
|
-
return res.ok ? (await res.json()) : null;
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
// Read the daemon's per-vault sync state (last run, last error); null when the endpoint is absent
|
|
38
|
-
// (an older daemon) or unreachable.
|
|
39
|
-
export const syncStatus = async (port, timeoutMs = 1000) => {
|
|
40
|
-
try {
|
|
41
|
-
const res = await fetch(`${base(port)}/api/sync/status`, {
|
|
42
|
-
headers: apiHeaders(),
|
|
43
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
44
|
-
});
|
|
45
|
-
return res.ok ? (await res.json()) : null;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
// Ask the daemon to sync one vault now; the daemon runs the git cycle in its own process.
|
|
52
|
-
export const syncRun = async (port, vault) => {
|
|
53
|
-
const res = await fetch(`${base(port)}/api/sync/run`, {
|
|
54
|
-
method: 'POST',
|
|
55
|
-
headers: apiHeaders(),
|
|
56
|
-
body: JSON.stringify({ vault }),
|
|
57
|
-
});
|
|
58
|
-
if (!res.ok) {
|
|
59
|
-
const data = (await res.json().catch(() => ({})));
|
|
60
|
-
throw new Error(data.error || `sync request failed: ${res.status}`);
|
|
61
|
-
}
|
|
62
|
-
return res.json();
|
|
63
|
-
};
|
|
64
|
-
export const waitForHealth = async (port, opts = {}) => {
|
|
65
|
-
const intervalMs = opts.intervalMs ?? 100;
|
|
66
|
-
const deadline = Date.now() + (opts.timeoutMs ?? 4000);
|
|
67
|
-
while (Date.now() < deadline) {
|
|
68
|
-
if (await health(port, intervalMs + 200))
|
|
69
|
-
return true;
|
|
70
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
};
|
|
74
|
-
// A thin MemoryClient over HTTP: every method is a pass-through POST; the daemon's in-process
|
|
75
|
-
// DirectClient does the vault scoping, so the wire carries the raw ref + opts unchanged.
|
|
76
|
-
export const createDaemonClient = (port) => ({
|
|
77
|
-
search: (query, opts) => post(port, 'search', { query, opts }),
|
|
78
|
-
read: (ref, opts) => post(port, 'read', { ref, opts }),
|
|
79
|
-
write: (ref, body, opts) => post(port, 'write', { ref, body, opts }),
|
|
80
|
-
edit: (ref, op, opts) => post(port, 'edit', { ref, op, opts }),
|
|
81
|
-
list: (folder, opts) => post(port, 'list', { folder, opts }),
|
|
82
|
-
delete: (ref, opts) => post(port, 'delete', { ref, opts }),
|
|
83
|
-
});
|
|
84
|
-
// A restart hint when the running daemon predates the current binary.
|
|
85
|
-
export const mismatchNotice = (daemonVersion) => daemonVersion === VERSION
|
|
86
|
-
? null
|
|
87
|
-
: `daemon version ${daemonVersion} != cli ${VERSION}; restart with: agentage daemon stop && agentage daemon start`;
|
|
88
|
-
const entryPath = () => fileURLToPath(new URL('../../daemon-entry.js', import.meta.url));
|
|
89
|
-
// Detached spawn (not fork: fork's IPC channel keeps the parent event loop alive past unref) so
|
|
90
|
-
// the daemon outlives this CLI; ignore stdio + unref so the CLI can exit. Watches the child's exit
|
|
91
|
-
// so a fast EADDRINUSE death short-circuits the health wait instead of burning the full timeout.
|
|
92
|
-
export const spawnDaemon = async (port, opts = {}) => {
|
|
93
|
-
let child;
|
|
94
|
-
try {
|
|
95
|
-
child = spawnChild(process.execPath, [entryPath()], {
|
|
96
|
-
detached: true,
|
|
97
|
-
stdio: 'ignore',
|
|
98
|
-
env: {
|
|
99
|
-
...process.env,
|
|
100
|
-
AGENTAGE_DAEMON_PORT: String(port),
|
|
101
|
-
// Its own service in telemetry: never inherit the name we defaulted into the CLI's env.
|
|
102
|
-
OTEL_SERVICE_NAME: daemonServiceName(),
|
|
103
|
-
...(opts.noMcp ? { AGENTAGE_DAEMON_NO_MCP: '1' } : {}),
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return { ok: false, reason: 'blocked' };
|
|
109
|
-
}
|
|
110
|
-
const exited = new Promise((resolve) => {
|
|
111
|
-
child.once('exit', (code) => resolve({ ok: false, reason: code === EADDRINUSE_EXIT_CODE ? 'port-in-use' : 'unreachable' }));
|
|
112
|
-
});
|
|
113
|
-
child.unref();
|
|
114
|
-
const healthy = waitForHealth(port, { timeoutMs: opts.timeoutMs ?? 4000 }).then((ok) => (ok ? { ok: true } : { ok: false, reason: 'unreachable' }));
|
|
115
|
-
return Promise.race([healthy, exited]);
|
|
116
|
-
};
|
|
117
|
-
// DO3/DO4/DO9: prefer a live daemon, autostart one if absent, and return null when it is
|
|
118
|
-
// unreachable, cannot be spawned, or has no readable token (nothing to authenticate with) so the
|
|
119
|
-
// caller falls back to the in-process DirectClient.
|
|
120
|
-
export const ensureDaemon = async (deps = {}) => {
|
|
121
|
-
const port = deps.port ?? resolvePort();
|
|
122
|
-
const probe = deps.probe ?? health;
|
|
123
|
-
const spawn = deps.spawn ?? spawnDaemon;
|
|
124
|
-
const readToken = deps.readToken ?? readDaemonToken;
|
|
125
|
-
const existing = await probe(port);
|
|
126
|
-
if (existing) {
|
|
127
|
-
if (!readToken())
|
|
128
|
-
return null;
|
|
129
|
-
const notice = mismatchNotice(existing.version);
|
|
130
|
-
if (notice)
|
|
131
|
-
console.error(chalk.yellow(notice));
|
|
132
|
-
return createDaemonClient(port);
|
|
133
|
-
}
|
|
134
|
-
const outcome = await spawn(port).catch(() => ({ ok: false, reason: 'blocked' }));
|
|
135
|
-
return outcome.ok && readToken() ? createDaemonClient(port) : null;
|
|
136
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// Whether the memory verbs must run in-process (DirectClient) instead of via the daemon.
|
|
2
|
-
// Set by the `--no-daemon` global flag or AGENTAGE_NO_DAEMON=1 (tests/sandbox/CI).
|
|
3
|
-
let forced = false;
|
|
4
|
-
export const disableDaemon = () => {
|
|
5
|
-
forced = true;
|
|
6
|
-
};
|
|
7
|
-
export const daemonDisabled = () => {
|
|
8
|
-
const env = process.env['AGENTAGE_NO_DAEMON'];
|
|
9
|
-
return forced || env === '1' || env === 'true';
|
|
10
|
-
};
|
|
@@ -1,30 +0,0 @@
|
|
|
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;
|
|
@@ -1,98 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const formatUptime: (seconds: number) => string;
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,49 +0,0 @@
|
|
|
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 'unsynced':
|
|
25
|
-
return `${mark(false)} not synced - account vaults have no sync channel`;
|
|
26
|
-
case 'idle':
|
|
27
|
-
return v.channel === 'local' ? chalk.dim('- local only') : chalk.dim('- idle');
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
// Only git vaults are "connected": an account vault has no sync channel, so counting it would
|
|
31
|
-
// re-tell the lie the per-vault row exists to correct.
|
|
32
|
-
const countLabel = (vaults) => {
|
|
33
|
-
const connected = vaults.filter((v) => v.channel === 'git').length;
|
|
34
|
-
if (connected > 0)
|
|
35
|
-
return `${connected} connected`;
|
|
36
|
-
const n = vaults.length;
|
|
37
|
-
return `${n} ${n === 1 ? 'vault' : 'vaults'}`;
|
|
38
|
-
};
|
|
39
|
-
// The `vaults` block: a header count line then one aligned `<name> <channel> <status>` row per vault.
|
|
40
|
-
// Zero vaults prints a single actionable hint instead of an empty block.
|
|
41
|
-
export const vaultLines = (vaults) => {
|
|
42
|
-
if (vaults.length === 0)
|
|
43
|
-
return [`${'vaults'.padEnd(10)} none - run: agentage vault add <name> --local`];
|
|
44
|
-
const nameW = Math.max(...vaults.map((v) => v.name.length));
|
|
45
|
-
const chanW = Math.max(...vaults.map((v) => v.channel.length));
|
|
46
|
-
const header = `${'vaults'.padEnd(10)} ${countLabel(vaults)}`;
|
|
47
|
-
const rows = vaults.map((v) => ` ${v.name.padEnd(nameW)} ${v.channel.padEnd(chanW)} ${statusCell(v)}`);
|
|
48
|
-
return [header, ...rows];
|
|
49
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type VaultsConfig } from '@agentage/memory-core';
|
|
2
|
-
import { type SyncStatus } from '../../sync/git/manager.js';
|
|
3
|
-
export type VaultChannel = 'local' | 'git' | 'account';
|
|
4
|
-
export type VaultSyncState = 'ok' | 'syncing' | 'error' | 'idle' | 'unknown' | 'unsynced';
|
|
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[];
|