@agentage/cli 0.37.2 → 0.38.0

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 (85) hide show
  1. package/README.md +53 -83
  2. package/dist/cli.js +0 -5
  3. package/dist/commands/auth/setup.d.ts +0 -2
  4. package/dist/commands/auth/setup.js +0 -18
  5. package/dist/commands/status/status.js +1 -25
  6. package/dist/commands/update/update.d.ts +0 -9
  7. package/dist/commands/update/update.js +0 -20
  8. package/dist/core/flag.d.ts +1 -0
  9. package/dist/core/flag.js +1 -0
  10. package/dist/lib/auth/api.js +1 -1
  11. package/dist/lib/auth/oauth.js +1 -1
  12. package/dist/lib/net/origins.d.ts +0 -2
  13. package/dist/lib/net/origins.js +1 -5
  14. package/dist/lib/net/user-agent.d.ts +1 -6
  15. package/dist/lib/net/user-agent.js +3 -4
  16. package/dist/lib/status/status-info.d.ts +0 -18
  17. package/dist/lib/status/status-info.js +1 -42
  18. package/dist/plugin.d.ts +1 -0
  19. package/dist/plugin.js +2 -0
  20. package/dist/program.js +4 -16
  21. package/package.json +17 -12
  22. package/dist/commands/daemon/daemon-cmd.d.ts +0 -2
  23. package/dist/commands/daemon/daemon-cmd.js +0 -107
  24. package/dist/commands/memory/memory-verbs.d.ts +0 -21
  25. package/dist/commands/memory/memory-verbs.js +0 -98
  26. package/dist/commands/memory/memory.d.ts +0 -2
  27. package/dist/commands/memory/memory.js +0 -54
  28. package/dist/commands/vault/vault-sync.d.ts +0 -13
  29. package/dist/commands/vault/vault-sync.js +0 -90
  30. package/dist/commands/vault/vault.d.ts +0 -22
  31. package/dist/commands/vault/vault.js +0 -129
  32. package/dist/daemon/actions.d.ts +0 -5
  33. package/dist/daemon/actions.js +0 -22
  34. package/dist/daemon/client-provider.d.ts +0 -2
  35. package/dist/daemon/client-provider.js +0 -16
  36. package/dist/daemon/entry.d.ts +0 -8
  37. package/dist/daemon/entry.js +0 -113
  38. package/dist/daemon/guards.d.ts +0 -3
  39. package/dist/daemon/guards.js +0 -13
  40. package/dist/daemon/lifecycle.d.ts +0 -17
  41. package/dist/daemon/lifecycle.js +0 -151
  42. package/dist/daemon/mcp-http.d.ts +0 -3
  43. package/dist/daemon/mcp-http.js +0 -34
  44. package/dist/daemon/server.d.ts +0 -23
  45. package/dist/daemon/server.js +0 -135
  46. package/dist/daemon-entry.d.ts +0 -1
  47. package/dist/daemon-entry.js +0 -9
  48. package/dist/lib/auth/provision.d.ts +0 -14
  49. package/dist/lib/auth/provision.js +0 -49
  50. package/dist/lib/daemon/daemon-client.d.ts +0 -37
  51. package/dist/lib/daemon/daemon-client.js +0 -136
  52. package/dist/lib/daemon/daemon-pref.d.ts +0 -2
  53. package/dist/lib/daemon/daemon-pref.js +0 -10
  54. package/dist/lib/memory/memory-client.d.ts +0 -30
  55. package/dist/lib/memory/memory-client.js +0 -98
  56. package/dist/lib/status/format.d.ts +0 -1
  57. package/dist/lib/status/format.js +0 -12
  58. package/dist/lib/status/vaults-format.d.ts +0 -2
  59. package/dist/lib/status/vaults-format.js +0 -49
  60. package/dist/lib/status/vaults-status.d.ts +0 -12
  61. package/dist/lib/status/vaults-status.js +0 -54
  62. package/dist/lib/vault/vault-registry.d.ts +0 -12
  63. package/dist/lib/vault/vault-registry.js +0 -81
  64. package/dist/lib/vault/vaults.d.ts +0 -10
  65. package/dist/lib/vault/vaults.js +0 -66
  66. package/dist/lib/vault/vaults.schema.d.ts +0 -2
  67. package/dist/lib/vault/vaults.schema.js +0 -10
  68. package/dist/mcp/local-server.d.ts +0 -4
  69. package/dist/mcp/local-server.js +0 -26
  70. package/dist/observability.d.ts +0 -4
  71. package/dist/observability.js +0 -35
  72. package/dist/sync/discover/watcher.d.ts +0 -26
  73. package/dist/sync/discover/watcher.js +0 -159
  74. package/dist/sync/git/conflict.d.ts +0 -1
  75. package/dist/sync/git/conflict.js +0 -16
  76. package/dist/sync/git/cycle.d.ts +0 -18
  77. package/dist/sync/git/cycle.js +0 -158
  78. package/dist/sync/git/git-exec.d.ts +0 -22
  79. package/dist/sync/git/git-exec.js +0 -56
  80. package/dist/sync/git/manager.d.ts +0 -28
  81. package/dist/sync/git/manager.js +0 -114
  82. package/dist/sync/git/planner.d.ts +0 -15
  83. package/dist/sync/git/planner.js +0 -46
  84. package/dist/sync/git/remote-url.d.ts +0 -3
  85. package/dist/sync/git/remote-url.js +0 -30
@@ -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,2 +0,0 @@
1
- export declare const disableDaemon: () => void;
2
- export declare const daemonDisabled: () => boolean;
@@ -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,2 +0,0 @@
1
- import { type VaultStatus } from './vaults-status.js';
2
- export declare const vaultLines: (vaults: VaultStatus[]) => string[];
@@ -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[];
@@ -1,54 +0,0 @@
1
- import { isAccountVault } from '@agentage/memory-core';
2
- import { loadVaultsConfig } from '../vault/vaults.js';
3
- // Config alone decides the channel: an external remote means git (it really syncs), else an
4
- // `agentage` origin means account, else local-only. External wins so a hand-edited entry carrying
5
- // both is reported by the channel that actually moves bytes.
6
- const channelOf = (entry) => {
7
- const external = entry.origin?.some((o) => o.remote.trim() && o.remote.trim() !== 'agentage');
8
- if (external)
9
- return 'git';
10
- return isAccountVault(entry) ? 'account' : 'local';
11
- };
12
- // Live state from the daemon wins; a local-only vault is `idle` (nothing to sync), an account vault
13
- // is always `unsynced` (it has no sync channel, so no daemon report can make it healthy), and any
14
- // git vault with no daemon report is `unknown` (daemon down or the vault not yet scheduled).
15
- const stateFrom = (channel, live, daemonUp) => {
16
- if (channel === 'local')
17
- return 'idle';
18
- if (channel === 'account')
19
- return 'unsynced';
20
- if (!daemonUp)
21
- return 'unknown';
22
- if (!live)
23
- return 'unknown';
24
- if (live.lastError)
25
- return 'error';
26
- if (live.running)
27
- return 'syncing';
28
- return live.lastRun ? 'ok' : 'idle';
29
- };
30
- // Index the daemon's per-vault git reports by name.
31
- const indexLive = (sync) => {
32
- const map = new Map();
33
- for (const v of sync?.vaults ?? [])
34
- map.set(v.vault, { running: v.running, lastError: v.lastError, lastRun: v.lastRun });
35
- return map;
36
- };
37
- // The full per-vault picture: every configured vault (so local-only vaults are never hidden),
38
- // classified by channel and annotated with the daemon's live sync state when available.
39
- export const buildVaultStatuses = (sync, daemonUp, config = loadVaultsConfig().config) => {
40
- const live = indexLive(sync);
41
- return Object.entries(config.vaults ?? {})
42
- .map(([name, entry]) => {
43
- const channel = channelOf(entry);
44
- const l = live.get(name);
45
- return {
46
- name,
47
- channel,
48
- status: stateFrom(channel, l, daemonUp),
49
- lastRun: l?.lastRun,
50
- lastError: l?.lastError,
51
- };
52
- })
53
- .sort((a, b) => a.name.localeCompare(b.name));
54
- };
@@ -1,12 +0,0 @@
1
- import { type VaultEntry, type VaultsConfig } from '@agentage/memory-core';
2
- export type VaultType = 'account' | 'git' | 'local' | 'remote';
3
- export declare const vaultType: (entry: VaultEntry) => VaultType;
4
- export declare const ensureVaultDir: (path: string) => void;
5
- export declare const formatVaultLine: (name: string, entry: VaultEntry) => string;
6
- export declare const redactEntry: (entry: VaultEntry) => VaultEntry;
7
- export declare const addVault: (config: VaultsConfig, name: string, entry: VaultEntry) => VaultsConfig;
8
- export declare const removeVault: (config: VaultsConfig, name: string) => VaultsConfig;
9
- export declare const appendDiscoverIgnore: (config: VaultsConfig, name: string, vaultPath: string) => {
10
- config: VaultsConfig;
11
- root: string;
12
- } | null;
@@ -1,81 +0,0 @@
1
- import { existsSync, mkdirSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
- import { expandPath, isAccountVault, validateConfig, } from '@agentage/memory-core';
4
- import { redactRemoteUrl } from '../../sync/git/remote-url.js';
5
- import { isValidVaultName } from './vaults.schema.js';
6
- // The human-facing type of an entry: an agentage origin is an account vault; otherwise a path with
7
- // an external origin is git, a bare path is local, and an origin without a path is remote.
8
- export const vaultType = (entry) => {
9
- if (isAccountVault(entry))
10
- return 'account';
11
- if (entry.path)
12
- return entry.origin?.length ? 'git' : 'local';
13
- return 'remote';
14
- };
15
- // A local vault's markdown directory is created on `vault add` (~ is expanded).
16
- export const ensureVaultDir = (path) => {
17
- const dir = expandPath(path);
18
- if (!existsSync(dir))
19
- mkdirSync(dir, { recursive: true });
20
- };
21
- export const formatVaultLine = (name, entry) => {
22
- const kind = vaultType(entry);
23
- const rawWhere = entry.path ? expandPath(entry.path) : (entry.origin?.[0]?.remote ?? '');
24
- const where = entry.path ? rawWhere : redactRemoteUrl(rawWhere);
25
- // Only an external git remote is worth echoing; the account channel is implied by the type.
26
- const remote = kind === 'git' && entry.origin?.length
27
- ? ` <- ${redactRemoteUrl(entry.origin[0].remote)}`
28
- : '';
29
- return `${name.padEnd(16)} ${kind.padEnd(8)} ${where}${remote}`;
30
- };
31
- // A copy of `entry` with every origin remote redacted (credentials stripped) for display/JSON;
32
- // the on-disk value is never mutated.
33
- export const redactEntry = (entry) => entry.origin?.length
34
- ? { ...entry, origin: entry.origin.map((o) => ({ ...o, remote: redactRemoteUrl(o.remote) })) }
35
- : entry;
36
- // Add an entry under `name`; the first vault added also becomes the `default`.
37
- export const addVault = (config, name, entry) => {
38
- if (!isValidVaultName(name))
39
- throw new Error(`invalid vault name: ${JSON.stringify(name)}`);
40
- const vaults = config.vaults ?? {};
41
- if (vaults[name])
42
- throw new Error(`vault '${name}' already exists`);
43
- return validateConfig({
44
- ...config,
45
- default: config.default ?? name,
46
- vaults: { ...vaults, [name]: entry },
47
- });
48
- };
49
- // Remove an entry; if it was the `default`, reassign to a remaining vault or drop the field.
50
- export const removeVault = (config, name) => {
51
- const vaults = config.vaults ?? {};
52
- if (!vaults[name])
53
- throw new Error(`vault '${name}' not found`);
54
- const rest = { ...vaults };
55
- delete rest[name];
56
- const next = { ...config, vaults: rest };
57
- if (config.default === name) {
58
- const fallback = Object.keys(rest)[0];
59
- if (fallback)
60
- next.default = fallback;
61
- else
62
- delete next.default;
63
- }
64
- return validateConfig(next);
65
- };
66
- // V8: when a removed vault sat directly under a discover root, append its name to that root's
67
- // `ignore` so the next scan does not re-add it. Returns the updated config plus the matched root
68
- // path (for the message), or null when the path is not directly under any discover root.
69
- export const appendDiscoverIgnore = (config, name, vaultPath) => {
70
- const parent = resolve(expandPath(vaultPath), '..');
71
- const roots = config.discover ?? [];
72
- const idx = roots.findIndex((r) => resolve(expandPath(r.path)) === parent);
73
- if (idx === -1)
74
- return null;
75
- const root = roots[idx];
76
- if (root.ignore?.includes(name))
77
- return { config, root: root.path };
78
- const next = [...roots];
79
- next[idx] = { ...root, ignore: [...(root.ignore ?? []), name] };
80
- return { config: validateConfig({ ...config, discover: next }), root: root.path };
81
- };
@@ -1,10 +0,0 @@
1
- import { type VaultsConfig } from '@agentage/memory-core';
2
- export declare const vaultsJsonPath: () => string;
3
- export interface LoadedVaults {
4
- config: VaultsConfig;
5
- source: string | null;
6
- }
7
- export declare const loadVaultsConfig: () => LoadedVaults;
8
- export declare const ensureVaultsConfig: () => void;
9
- export declare const saveVaultsConfig: (config: VaultsConfig) => string;
10
- export declare const mutateVaultsConfig: (fn: (config: VaultsConfig) => VaultsConfig | null | void) => Promise<VaultsConfig>;
@@ -1,66 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { validateConfig } from '@agentage/memory-core';
5
- import { ensureConfigDir, getConfigDir } from '../fs/config.js';
6
- import { withFileLock } from '../fs/file-lock.js';
7
- import { VAULTS_SCHEMA_URL } from './vaults.schema.js';
8
- export const vaultsJsonPath = () => join(getConfigDir(), 'vaults.json');
9
- const EMPTY = { version: 1, vaults: {} };
10
- // JSON only (one format everywhere; the standalone @agentage/server-memory reads the same file
11
- // and parses only JSON). A missing file yields an empty config; a bad shape throws ConfigError.
12
- export const loadVaultsConfig = () => {
13
- const path = vaultsJsonPath();
14
- if (!existsSync(path))
15
- return { config: EMPTY, source: null };
16
- const raw = JSON.parse(readFileSync(path, 'utf-8'));
17
- return { config: validateConfig(raw), source: path };
18
- };
19
- // Scaffold an empty, $schema-linked vaults.json when none exists yet, so a fresh machine always
20
- // has an editable file with editor autocomplete. Leaves any existing file untouched.
21
- export const ensureVaultsConfig = () => {
22
- if (existsSync(vaultsJsonPath()))
23
- return;
24
- saveVaultsConfig(EMPTY);
25
- };
26
- // Atomic 0600 write. Re-injects the canonical $schema first and preserves every other field.
27
- export const saveVaultsConfig = (config) => {
28
- ensureConfigDir();
29
- const path = vaultsJsonPath();
30
- const out = { $schema: VAULTS_SCHEMA_URL, version: config.version };
31
- if (config.vaultsDir !== undefined)
32
- out.vaultsDir = config.vaultsDir;
33
- if (config.autodiscover !== undefined)
34
- out.autodiscover = config.autodiscover;
35
- if (config.autoInit !== undefined)
36
- out.autoInit = config.autoInit;
37
- if (config.default !== undefined)
38
- out.default = config.default;
39
- if (config.discover !== undefined)
40
- out.discover = config.discover;
41
- out.vaults = config.vaults ?? {};
42
- // A per-save tmp name: two concurrent savers can never clobber each other's tmp file.
43
- const tmp = `${path}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
44
- writeFileSync(tmp, JSON.stringify(out, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
45
- renameSync(tmp, path);
46
- chmodSync(path, 0o600);
47
- return path;
48
- };
49
- // Cross-process-safe read-modify-write on vaults.json (issue #231). Under the advisory lock, re-read
50
- // FRESH from disk, apply `fn`, then atomic-save - so a concurrent `vault add` or the daemon's
51
- // discovery write-back can never clobber each other's update. `fn` may return a new config, mutate
52
- // the fresh one in place and return void, or return null to skip the write entirely (a no-op scan
53
- // must not re-touch the file, else the daemon's config watcher would loop). Returns the config on
54
- // disk after the call. A plain read stays lock-free via loadVaultsConfig.
55
- export const mutateVaultsConfig = async (fn) => {
56
- ensureConfigDir();
57
- return withFileLock(vaultsJsonPath(), () => {
58
- const fresh = loadVaultsConfig().config;
59
- const next = fn(fresh);
60
- if (next === null)
61
- return fresh; // explicit no-op: leave the file untouched
62
- const result = next ?? fresh;
63
- saveVaultsConfig(result);
64
- return result;
65
- });
66
- };
@@ -1,2 +0,0 @@
1
- export declare const VAULTS_SCHEMA_URL = "https://agentage.io/schemas/vaults.schema.json";
2
- export { isValidVaultName, VAULT_NAME_PATTERN } from '@agentage/memory-core';
@@ -1,10 +0,0 @@
1
- // The vaults.json format is owned by @agentage/memory-core (validateConfig / VaultsConfig).
2
- // This module keeps the one CLI-surface piece memory-core does not: the editor-tooling $schema
3
- // pointer written on create. Name validation re-exports memory-core's single source of truth.
4
- // The public JSON Schema location (served by the landing). Written into the file on create so
5
- // editors get autocomplete + inline validation; accepted-and-ignored on load. A fixed
6
- // production URL by design: editor tooling fetches it regardless of the CLI's target FQDN.
7
- export const VAULTS_SCHEMA_URL = 'https://agentage.io/schemas/vaults.schema.json';
8
- // ADR-013 AA-4: every name stays a valid cloud path segment (`<sub>/<name>.git`). memory-core's
9
- // schema accepts any string key, so the CLI guards the name at the add surface with its allowlist.
10
- export { isValidVaultName, VAULT_NAME_PATTERN } from '@agentage/memory-core';
@@ -1,4 +0,0 @@
1
- import { type VaultRegistry } from '@agentage/memory-core';
2
- import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- export declare const createLocalMemoryServer: (registry: VaultRegistry) => McpServer;
4
- export declare const loadLocalMemoryServer: () => Promise<McpServer>;
@@ -1,26 +0,0 @@
1
- import { createRegistry } from '@agentage/memory-core';
2
- import { createMemoryServer } from '@agentage/server-memory';
3
- import { loadVaultsConfig } from '../lib/vault/vaults.js';
4
- import { VERSION } from '../utils/version.js';
5
- // Restrict a registry to its offline (git working-copy) vaults, so the local MCP surface exposes
6
- // exactly what the DirectClient does - never a network-backed vault over the no-auth loopback.
7
- const localOnly = (registry) => {
8
- const local = registry.list().filter((h) => h.backend.capabilities().kind === 'local');
9
- const byId = new Map(local.map((h) => [h.id, h]));
10
- const def = registry.default();
11
- const defaultHandle = def && byId.has(def.id) ? def : undefined;
12
- return {
13
- list: () => local,
14
- get: (id) => byId.get(id),
15
- default: () => defaultHandle,
16
- surfaced: () => local,
17
- watch: () => () => { },
18
- close: async () => { },
19
- };
20
- };
21
- // Build the frozen 6-tool MCP server over one registry, reusing @agentage/server-memory's contract
22
- // layer verbatim (descriptions, schemas, annotations, per-connection instructions) - zero drift.
23
- export const createLocalMemoryServer = (registry) => createMemoryServer(localOnly(registry), { scope: 'local', version: VERSION });
24
- // The whole local stack: read this machine's vaults.json (the same config the daemon + CLI verbs
25
- // load) -> registry -> an MCP server over the surfaced local vaults.
26
- export const loadLocalMemoryServer = async () => createLocalMemoryServer(await createRegistry(loadVaultsConfig().config));
@@ -1,4 +0,0 @@
1
- export declare const CLI_SERVICE = "agentage-cli";
2
- export declare const DAEMON_SERVICE = "agentage-daemon";
3
- export declare const daemonServiceName: () => string;
4
- export declare const startObservability: (service: string) => Promise<void>;