@agentage/cli 0.26.0 → 0.27.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.
- package/dist/commands/daemon/daemon-cmd.js +0 -17
- package/dist/commands/vault/vault-sync.d.ts +2 -4
- package/dist/commands/vault/vault-sync.js +32 -42
- package/dist/commands/vault/vault.js +3 -2
- package/dist/daemon/server.d.ts +1 -4
- package/dist/daemon/server.js +0 -1
- package/dist/daemon-entry.js +6 -16
- package/dist/lib/auth/api.d.ts +0 -1
- package/dist/lib/auth/api.js +0 -12
- package/dist/lib/auth/provision.d.ts +1 -1
- package/dist/lib/auth/provision.js +5 -27
- package/dist/lib/daemon/daemon-client.d.ts +1 -3
- package/dist/lib/daemon/daemon-client.js +1 -2
- package/dist/lib/status/status-info.js +11 -9
- package/dist/lib/status/vaults-format.js +7 -2
- package/dist/lib/status/vaults-status.d.ts +2 -2
- package/dist/lib/status/vaults-status.js +13 -12
- package/dist/lib/vault/vault-registry.js +2 -3
- package/dist/sync/discover/watcher.js +1 -1
- package/dist/sync/git/manager.d.ts +0 -2
- package/dist/sync/git/planner.js +3 -3
- package/package.json +4 -4
- package/dist/sync/couch/cycle.d.ts +0 -2
- package/dist/sync/couch/cycle.js +0 -58
- package/dist/sync/couch/discovery.d.ts +0 -22
- package/dist/sync/couch/discovery.js +0 -34
- package/dist/sync/couch/file-store.d.ts +0 -2
- package/dist/sync/couch/file-store.js +0 -47
- package/dist/sync/couch/local-commit.d.ts +0 -2
- package/dist/sync/couch/local-commit.js +0 -24
- package/dist/sync/couch/manager.d.ts +0 -4
- package/dist/sync/couch/manager.fixtures.d.ts +0 -20
- package/dist/sync/couch/manager.fixtures.js +0 -56
- package/dist/sync/couch/manager.js +0 -126
- package/dist/sync/couch/manager.types.d.ts +0 -81
- package/dist/sync/couch/manager.types.js +0 -1
- package/dist/sync/couch/mutation-target.d.ts +0 -5
- package/dist/sync/couch/mutation-target.js +0 -33
- package/dist/sync/couch/push-on-write.d.ts +0 -3
- package/dist/sync/couch/push-on-write.js +0 -33
- package/dist/sync/couch/state-store.d.ts +0 -3
- package/dist/sync/couch/state-store.js +0 -28
- package/dist/sync/couch/targets.d.ts +0 -9
- package/dist/sync/couch/targets.js +0 -23
- package/dist/sync/couch/wire.d.ts +0 -6
- package/dist/sync/couch/wire.js +0 -16
|
@@ -83,23 +83,6 @@ const statusAction = async () => {
|
|
|
83
83
|
console.log(` ${v.vault.padEnd(16)} ${cadence.padEnd(12)} ${state}`);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
if (sync?.couch && sync.couch.length > 0) {
|
|
87
|
-
console.log('couch sync');
|
|
88
|
-
for (const v of sync.couch) {
|
|
89
|
-
const cadence = v.intervalSeconds > 0 ? `every ${v.intervalSeconds}s` : 'manual';
|
|
90
|
-
const state = v.paused
|
|
91
|
-
? `paused: ${v.paused}`
|
|
92
|
-
: v.running
|
|
93
|
-
? 'running'
|
|
94
|
-
: v.lastError
|
|
95
|
-
? `error: ${v.lastError}`
|
|
96
|
-
: v.lastSync
|
|
97
|
-
? `ok ${v.lastSync}`
|
|
98
|
-
: 'scheduled';
|
|
99
|
-
const pending = v.pendingCount > 0 ? ` ${v.pendingCount} pending` : '';
|
|
100
|
-
console.log(` ${v.vault.padEnd(16)} ${cadence.padEnd(12)} ${state}${pending}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
86
|
if (sync?.discover && sync.discover.roots.length > 0) {
|
|
104
87
|
console.log(`discover roots (${sync.discover.roots.length})`);
|
|
105
88
|
for (const root of sync.discover.roots)
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { type VaultsConfig } from '@agentage/memory-core';
|
|
2
|
-
import { type SyncRunResult } from '../../lib/daemon/daemon-client.js';
|
|
3
2
|
import { type SyncResult } from '../../sync/git/cycle.js';
|
|
4
|
-
import { type CouchSyncResult } from '../../sync/couch/manager.js';
|
|
5
3
|
import { type SyncTarget } from '../../sync/git/planner.js';
|
|
6
4
|
export interface VaultSyncDeps {
|
|
7
5
|
loadConfig: () => VaultsConfig;
|
|
8
6
|
daemonPort: () => Promise<number | null>;
|
|
9
|
-
runViaDaemon: (port: number, vault: string) => Promise<
|
|
7
|
+
runViaDaemon: (port: number, vault: string) => Promise<SyncResult>;
|
|
10
8
|
runGitInProcess: (target: SyncTarget) => Promise<SyncResult>;
|
|
11
|
-
runCouchInProcess: (vault: string) => Promise<CouchSyncResult>;
|
|
12
9
|
log: (msg: string) => void;
|
|
13
10
|
}
|
|
11
|
+
export declare const ACCOUNT_NO_CHANNEL = "not synced - account vaults have no sync channel";
|
|
14
12
|
export declare const runVaultSync: (name: string | undefined, deps: VaultSyncDeps) => Promise<void>;
|
|
15
13
|
export declare const defaultVaultSyncDeps: () => VaultSyncDeps;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { isAccountVault } from '@agentage/memory-core';
|
|
2
3
|
import { health, syncRun } from '../../lib/daemon/daemon-client.js';
|
|
3
4
|
import { daemonDisabled } from '../../lib/daemon/daemon-pref.js';
|
|
4
5
|
import { loadVaultsConfig } from '../../lib/vault/vaults.js';
|
|
5
6
|
import { resolvePort } from '../../daemon/lifecycle.js';
|
|
6
7
|
import { runSyncCycle } from '../../sync/git/cycle.js';
|
|
7
|
-
import { createCouchSyncManager } from '../../sync/couch/manager.js';
|
|
8
|
-
import { couchTargets } from '../../sync/couch/targets.js';
|
|
9
8
|
import { syncTargets } from '../../sync/git/planner.js';
|
|
10
9
|
import { redactRemoteUrl } from '../../sync/git/remote-url.js';
|
|
11
|
-
const
|
|
10
|
+
export const ACCOUNT_NO_CHANNEL = 'not synced - account vaults have no sync channel';
|
|
12
11
|
const describeGit = (r) => {
|
|
13
12
|
if (!r.ok)
|
|
14
13
|
return chalk.red(`failed (${r.reason ?? 'error'})${r.error ? `: ${r.error}` : ''}`);
|
|
@@ -23,49 +22,43 @@ const describeGit = (r) => {
|
|
|
23
22
|
bits.push('pushed');
|
|
24
23
|
return chalk.green(bits.length ? bits.join(', ') : 'up to date');
|
|
25
24
|
};
|
|
26
|
-
const describeCouch = (r) => {
|
|
27
|
-
if (r.paused)
|
|
28
|
-
return chalk.yellow(`paused (${r.paused})`);
|
|
29
|
-
if (!r.ok)
|
|
30
|
-
return chalk.red(`failed${r.error ? `: ${r.error}` : ''}`);
|
|
31
|
-
const bits = [];
|
|
32
|
-
if (r.committed)
|
|
33
|
-
bits.push('committed');
|
|
34
|
-
if (r.pulled)
|
|
35
|
-
bits.push('pulled');
|
|
36
|
-
if (r.pendingCount)
|
|
37
|
-
bits.push(`${r.pendingCount} pending`);
|
|
38
|
-
return chalk.green(bits.length ? bits.join(', ') : 'up to date');
|
|
39
|
-
};
|
|
40
25
|
const report = (log, r) => {
|
|
41
|
-
if (isCouch(r)) {
|
|
42
|
-
log(`${r.vault} (account): ${describeCouch(r)}`);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
26
|
log(`${r.vault} -> ${redactRemoteUrl(r.remote)}: ${describeGit(r)}`);
|
|
46
27
|
for (const c of r.conflicts)
|
|
47
28
|
log(` kept remote copy: ${c}`);
|
|
48
29
|
};
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
30
|
+
// Account vaults whose only origin is the reserved `agentage` remote: nothing syncs them. They are
|
|
31
|
+
// named, never silently skipped, so `vault sync` cannot read as "everything is up to date".
|
|
32
|
+
const unsyncableAccountVaults = (config, gitTargets) => {
|
|
33
|
+
const git = new Set(gitTargets.map((t) => t.vault));
|
|
34
|
+
return Object.entries(config.vaults ?? {})
|
|
35
|
+
.filter(([vault, entry]) => isAccountVault(entry) && !git.has(vault))
|
|
36
|
+
.map(([vault]) => vault);
|
|
37
|
+
};
|
|
38
|
+
const reportAccounts = (log, vaults) => {
|
|
39
|
+
for (const vault of vaults)
|
|
40
|
+
log(`${vault} (account): ${chalk.yellow(ACCOUNT_NO_CHANNEL)}`);
|
|
41
|
+
};
|
|
42
|
+
// `agentage vault sync [name]`: sync one vault (or every syncable vault). Git-origin vaults commit
|
|
43
|
+
// + push + pull-rebase; account (agentage) vaults have no sync channel and are reported as such.
|
|
44
|
+
// Prefers a running daemon (single writer), else runs the cycle in-process. Works for interval-0
|
|
45
|
+
// (manual-only) vaults and with the daemon down. Failures are surfaced, not thrown (V6: never a crash).
|
|
53
46
|
export const runVaultSync = async (name, deps) => {
|
|
54
47
|
const config = deps.loadConfig();
|
|
55
48
|
// A named vault that is not registered is an error (mirrors `vault remove`), not a silent no-op.
|
|
56
49
|
if (name && !(config.vaults ?? {})[name])
|
|
57
50
|
throw new Error(`vault '${name}' not found`);
|
|
58
51
|
const gitTargets = syncTargets(config).filter((t) => !name || t.vault === name);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
52
|
+
const accounts = unsyncableAccountVaults(config, gitTargets).filter((v) => !name || v === name);
|
|
53
|
+
if (gitTargets.length === 0) {
|
|
54
|
+
reportAccounts(deps.log, accounts);
|
|
55
|
+
if (accounts.length === 0)
|
|
56
|
+
deps.log(name
|
|
57
|
+
? `No syncable origin configured for vault '${name}'.`
|
|
58
|
+
: 'No syncable vaults. Add one with `agentage vault add <name>`.');
|
|
66
59
|
return;
|
|
67
60
|
}
|
|
68
|
-
const vaults = [...new Set(
|
|
61
|
+
const vaults = [...new Set(gitTargets.map((t) => t.vault))];
|
|
69
62
|
deps.log(`Syncing ${vaults.length} vault(s)...`);
|
|
70
63
|
const port = await deps.daemonPort();
|
|
71
64
|
if (port !== null) {
|
|
@@ -73,16 +66,14 @@ export const runVaultSync = async (name, deps) => {
|
|
|
73
66
|
deps.log(`${vault}...`);
|
|
74
67
|
report(deps.log, await deps.runViaDaemon(port, vault));
|
|
75
68
|
}
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
for (const target of gitTargets) {
|
|
79
|
-
deps.log(`${target.vault}...`);
|
|
80
|
-
report(deps.log, await deps.runGitInProcess(target));
|
|
81
69
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
70
|
+
else {
|
|
71
|
+
for (const target of gitTargets) {
|
|
72
|
+
deps.log(`${target.vault}...`);
|
|
73
|
+
report(deps.log, await deps.runGitInProcess(target));
|
|
74
|
+
}
|
|
85
75
|
}
|
|
76
|
+
reportAccounts(deps.log, accounts);
|
|
86
77
|
};
|
|
87
78
|
const resolveDaemonPort = async () => {
|
|
88
79
|
if (daemonDisabled())
|
|
@@ -95,6 +86,5 @@ export const defaultVaultSyncDeps = () => ({
|
|
|
95
86
|
daemonPort: resolveDaemonPort,
|
|
96
87
|
runViaDaemon: syncRun,
|
|
97
88
|
runGitInProcess: runSyncCycle,
|
|
98
|
-
runCouchInProcess: (vault) => createCouchSyncManager().runNow(vault),
|
|
99
89
|
log: (msg) => console.log(msg),
|
|
100
90
|
});
|
|
@@ -28,7 +28,8 @@ const buildEntry = (name, opts) => {
|
|
|
28
28
|
const path = typeof opts.local === 'string' ? opts.local : `~/vaults/${name}`;
|
|
29
29
|
return { path, mcp: ['local'] };
|
|
30
30
|
}
|
|
31
|
-
// No --local/--git: an account vault - a local
|
|
31
|
+
// No --local/--git: an account vault - a local folder plus a memory in your account; this CLI
|
|
32
|
+
// has no channel that syncs the two.
|
|
32
33
|
return { path: opts.path ?? `~/vaults/${name}`, origin: [{ remote: 'agentage' }] };
|
|
33
34
|
};
|
|
34
35
|
export const runVaultAdd = async (name, opts, deps = defaultDeps) => {
|
|
@@ -120,7 +121,7 @@ export const registerVault = (program) => {
|
|
|
120
121
|
.action((name) => guardAsync(() => runVaultRemove(name)));
|
|
121
122
|
vault
|
|
122
123
|
.command('sync [name]')
|
|
123
|
-
.description('Sync vaults now (git commit/push/pull
|
|
124
|
+
.description('Sync vaults now (git commit/push/pull)')
|
|
124
125
|
.action((name) => runVaultSync(name, defaultVaultSyncDeps()).catch((err) => {
|
|
125
126
|
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
126
127
|
process.exitCode = 1;
|
package/dist/daemon/server.d.ts
CHANGED
|
@@ -2,18 +2,15 @@ import { type Server } from 'node:http';
|
|
|
2
2
|
import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { type MemoryClient } from '../lib/memory/memory-client.js';
|
|
4
4
|
import { type SyncResult } from '../sync/git/cycle.js';
|
|
5
|
-
import { type CouchSyncResult } from '../sync/couch/manager.js';
|
|
6
5
|
import { type SyncStatus } from '../sync/git/manager.js';
|
|
7
|
-
import { type MemoryVerb } from './actions.js';
|
|
8
6
|
export interface DaemonSyncApi {
|
|
9
7
|
status: () => SyncStatus;
|
|
10
|
-
runNow: (vault: string) => Promise<SyncResult
|
|
8
|
+
runNow: (vault: string) => Promise<SyncResult>;
|
|
11
9
|
}
|
|
12
10
|
export interface DaemonServerOptions {
|
|
13
11
|
getClient: () => MemoryClient | Promise<MemoryClient>;
|
|
14
12
|
buildMcpServer?: () => Promise<McpServer>;
|
|
15
13
|
sync?: DaemonSyncApi;
|
|
16
|
-
onMutation?: (verb: MemoryVerb, body: unknown) => void;
|
|
17
14
|
authToken: string;
|
|
18
15
|
version: string;
|
|
19
16
|
startedAt?: number;
|
package/dist/daemon/server.js
CHANGED
|
@@ -93,7 +93,6 @@ export const createDaemonServer = (opts) => {
|
|
|
93
93
|
const body = await readBody(req);
|
|
94
94
|
const result = await dispatchMemory(await opts.getClient(), verb, body);
|
|
95
95
|
served += 1;
|
|
96
|
-
opts.onMutation?.(verb, body); // fire-and-forget account push-on-save
|
|
97
96
|
return send(res, 200, result);
|
|
98
97
|
}
|
|
99
98
|
catch (err) {
|
package/dist/daemon-entry.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { unwatchFile, watchFile } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { isAccountVault } from '@agentage/memory-core';
|
|
4
3
|
import { createClientProvider } from './daemon/client-provider.js';
|
|
5
4
|
import { EADDRINUSE_EXIT_CODE, generateDaemonToken, removePidFile, removePortFile, removeTokenFile, resolvePort, writePidFile, writePortFile, writeTokenFile, } from './daemon/lifecycle.js';
|
|
6
5
|
import { createDaemonServer } from './daemon/server.js';
|
|
7
6
|
import { loadLocalMemoryServer } from './mcp/local-server.js';
|
|
8
|
-
import {
|
|
9
|
-
import { createCouchSyncManager } from './sync/couch/manager.js';
|
|
7
|
+
import { vaultsJsonPath } from './lib/vault/vaults.js';
|
|
10
8
|
import { createDiscoverWatcher } from './sync/discover/watcher.js';
|
|
11
9
|
import { createSyncManager } from './sync/git/manager.js';
|
|
12
10
|
import { VERSION } from './utils/version.js';
|
|
@@ -26,7 +24,7 @@ export const createStateCleanup = (remove) => {
|
|
|
26
24
|
};
|
|
27
25
|
};
|
|
28
26
|
// Run each reschedule independently: a transiently-invalid config edit must not crash the daemon or
|
|
29
|
-
// stop the
|
|
27
|
+
// stop the others rescheduling; the throwing one keeps its last-good schedule.
|
|
30
28
|
export const safeReschedule = (steps, onError) => {
|
|
31
29
|
for (const step of steps) {
|
|
32
30
|
try {
|
|
@@ -55,30 +53,23 @@ const state = createStateCleanup(() => {
|
|
|
55
53
|
});
|
|
56
54
|
// The detached, long-lived engine host: one loopback HTTP server that owns a single in-process
|
|
57
55
|
// engine and serialises every vault mutation, avoiding concurrent git index.lock collisions. It
|
|
58
|
-
// runs
|
|
56
|
+
// runs the git sync loop and reschedules on config change.
|
|
59
57
|
const main = async () => {
|
|
60
58
|
const port = resolvePort();
|
|
61
59
|
const authToken = generateDaemonToken();
|
|
62
60
|
const git = createSyncManager();
|
|
63
|
-
const couch = createCouchSyncManager();
|
|
64
61
|
const discover = createDiscoverWatcher({
|
|
65
62
|
log: (msg) => console.log(`[discover] ${msg}`),
|
|
66
63
|
debounceMs: envInt('AGENTAGE_DISCOVER_DEBOUNCE_MS'),
|
|
67
64
|
pollMs: envInt('AGENTAGE_DISCOVER_POLL_MS'),
|
|
68
65
|
});
|
|
69
|
-
// A vault is on exactly one channel: an account (agentage) vault syncs over couch, else git.
|
|
70
|
-
const runNow = (vault) => {
|
|
71
|
-
const entry = loadVaultsConfig().config.vaults?.[vault];
|
|
72
|
-
return entry && isAccountVault(entry) ? couch.runNow(vault) : git.runNow(vault);
|
|
73
|
-
};
|
|
74
66
|
const server = createDaemonServer({
|
|
75
67
|
getClient: createClientProvider(),
|
|
76
68
|
buildMcpServer: mcpEnabled() ? loadLocalMemoryServer : undefined,
|
|
77
69
|
sync: {
|
|
78
|
-
status: () => ({ ...git.status(),
|
|
79
|
-
runNow,
|
|
70
|
+
status: () => ({ ...git.status(), discover: discover.status() }),
|
|
71
|
+
runNow: (vault) => git.runNow(vault),
|
|
80
72
|
},
|
|
81
|
-
onMutation: (verb, body) => couch.onWrite(verb, body),
|
|
82
73
|
authToken,
|
|
83
74
|
version: VERSION,
|
|
84
75
|
});
|
|
@@ -87,14 +78,13 @@ const main = async () => {
|
|
|
87
78
|
writePortFile(port);
|
|
88
79
|
writeTokenFile(authToken);
|
|
89
80
|
state.markOwned();
|
|
90
|
-
const reschedule = () => safeReschedule([() => git.reschedule(), () =>
|
|
81
|
+
const reschedule = () => safeReschedule([() => git.reschedule(), () => discover.reschedule()], (msg) => console.error(`[daemon] reschedule failed: ${msg}`));
|
|
91
82
|
reschedule();
|
|
92
83
|
const configPath = vaultsJsonPath();
|
|
93
84
|
watchFile(configPath, { interval: 2000 }, reschedule);
|
|
94
85
|
const shutdown = () => {
|
|
95
86
|
unwatchFile(configPath);
|
|
96
87
|
git.stop();
|
|
97
|
-
couch.stop();
|
|
98
88
|
discover.stop();
|
|
99
89
|
server.stop().finally(() => {
|
|
100
90
|
state.cleanup();
|
package/dist/lib/auth/api.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { type AuthState } from '../fs/config.js';
|
|
|
2
2
|
import { type Links } from '../net/origins.js';
|
|
3
3
|
export { AuthRequiredError, TransientAuthError } from './auth-errors.js';
|
|
4
4
|
export declare const refreshOrThrow: (auth: AuthState, links: Links) => Promise<void>;
|
|
5
|
-
export declare const currentBearer: (readAuth: () => AuthState | null, links: Links) => Promise<string | null>;
|
|
6
5
|
export declare const authedGet: <T>(auth: AuthState, links: Links, url: string) => Promise<T>;
|
|
7
6
|
export declare const authedPost: (auth: AuthState, links: Links, url: string, body: unknown) => Promise<Response>;
|
|
8
7
|
export { introspectToken, type TokenSession } from './introspect.js';
|
package/dist/lib/auth/api.js
CHANGED
|
@@ -47,18 +47,6 @@ const tryRefresh = async (auth, links) => {
|
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
|
-
// The current OAuth bearer for background (couch) sync. Reads auth.json fresh on every call - the
|
|
51
|
-
// user may sign in or out between ticks - and refreshes once when the stored token is past its
|
|
52
|
-
// stated expiry. Returns null when signed out so a caller pauses with zero network, never throws.
|
|
53
|
-
export const currentBearer = async (readAuth, links) => {
|
|
54
|
-
const auth = readAuth();
|
|
55
|
-
if (!auth?.tokens.accessToken)
|
|
56
|
-
return null;
|
|
57
|
-
const expired = auth.tokens.expiresAt !== undefined && auth.tokens.expiresAt <= Date.now();
|
|
58
|
-
if (expired && !(await tryRefresh(auth, links)))
|
|
59
|
-
return null;
|
|
60
|
-
return auth.tokens.accessToken;
|
|
61
|
-
};
|
|
62
50
|
// redirect: 'manual' so the bearer is never replayed to a redirect target; any 3xx is an error.
|
|
63
51
|
const isRedirect = (res) => res.type === 'opaqueredirect' || (res.status >= 300 && res.status < 400);
|
|
64
52
|
export const authedGet = async (auth, links, url) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AuthState } from '../fs/config.js';
|
|
2
2
|
import { type Links } from '../net/origins.js';
|
|
3
|
-
export type ProvisionStatus = 'provisioned' | 'exists' | '
|
|
3
|
+
export type ProvisionStatus = 'provisioned' | 'exists' | 'unauthenticated' | 'offline';
|
|
4
4
|
export interface ProvisionResult {
|
|
5
5
|
status: ProvisionStatus;
|
|
6
6
|
message: string;
|
|
@@ -6,39 +6,28 @@ export const defaultProvisionDeps = () => ({
|
|
|
6
6
|
links: () => buildLinks(siteFqdn()),
|
|
7
7
|
post: authedPost,
|
|
8
8
|
});
|
|
9
|
-
// The API error envelope carries the code as `error.code` (or a top-level `code`); read it best
|
|
10
|
-
// effort - a non-JSON body just yields undefined and the caller falls back to non-fatal.
|
|
11
|
-
const errorCode = async (res) => {
|
|
12
|
-
try {
|
|
13
|
-
const body = (await res.json());
|
|
14
|
-
return body.error?.code ?? body.code;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
9
|
const registeredLocally = (name, tail) => `Vault '${name}' registered locally${tail}`;
|
|
21
10
|
export const provisionAccountVault = async (name, deps = defaultProvisionDeps()) => {
|
|
22
11
|
const auth = deps.readAuth();
|
|
23
12
|
if (!auth) {
|
|
24
13
|
return {
|
|
25
14
|
status: 'unauthenticated',
|
|
26
|
-
message: registeredLocally(name, ' - run `agentage setup` to
|
|
15
|
+
message: registeredLocally(name, ' - run `agentage setup` to create it in your account.'),
|
|
27
16
|
};
|
|
28
17
|
}
|
|
29
18
|
// A PAT is an MCP-surface credential; the backend REST provisioning endpoint rejects plain
|
|
30
|
-
// bearers (only session cookies), so it cannot
|
|
19
|
+
// bearers (only session cookies), so it cannot create the account memory. Fail clearly.
|
|
31
20
|
if (auth.kind === 'pat') {
|
|
32
21
|
return {
|
|
33
22
|
status: 'unauthenticated',
|
|
34
|
-
message: registeredLocally(name, ' - account
|
|
23
|
+
message: registeredLocally(name, ' - account provisioning needs an interactive session (run `agentage setup`); ' +
|
|
35
24
|
'a personal access token only authorizes memory (MCP) calls.'),
|
|
36
25
|
};
|
|
37
26
|
}
|
|
38
27
|
const links = deps.links();
|
|
39
28
|
let res;
|
|
40
29
|
try {
|
|
41
|
-
res = await deps.post(auth, links, `${links.api}/memories`, { name
|
|
30
|
+
res = await deps.post(auth, links, `${links.api}/memories`, { name });
|
|
42
31
|
}
|
|
43
32
|
catch {
|
|
44
33
|
return {
|
|
@@ -53,18 +42,7 @@ export const provisionAccountVault = async (name, deps = defaultProvisionDeps())
|
|
|
53
42
|
if (res.status === 401)
|
|
54
43
|
return {
|
|
55
44
|
status: 'unauthenticated',
|
|
56
|
-
message: registeredLocally(name, ' - run `agentage setup` to
|
|
57
|
-
};
|
|
58
|
-
const code = await errorCode(res);
|
|
59
|
-
if (res.status === 403 && code === 'CHANNEL_DISABLED')
|
|
60
|
-
return {
|
|
61
|
-
status: 'disabled',
|
|
62
|
-
message: registeredLocally(name, '. Account sync is not enabled on this server.'),
|
|
63
|
-
};
|
|
64
|
-
if (res.status === 409 && code === 'CHANNEL_CONFLICT')
|
|
65
|
-
return {
|
|
66
|
-
status: 'conflict',
|
|
67
|
-
message: registeredLocally(name, `. A memory named '${name}' already exists on another channel - not syncing.`),
|
|
45
|
+
message: registeredLocally(name, ' - run `agentage setup` to create it in your account.'),
|
|
68
46
|
};
|
|
69
47
|
// Any other status stays non-fatal: keep the local entry, let the daemon retry later.
|
|
70
48
|
return { status: 'offline', message: registeredLocally(name, ' - will provision when online.') };
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { type SyncResult } from '../../sync/git/cycle.js';
|
|
2
|
-
import { type CouchSyncResult } from '../../sync/couch/manager.js';
|
|
3
2
|
import { type SyncStatus } from '../../sync/git/manager.js';
|
|
4
|
-
export type SyncRunResult = SyncResult | CouchSyncResult;
|
|
5
3
|
import { type MemoryClient } from '../memory/memory-client.js';
|
|
6
4
|
export interface Health {
|
|
7
5
|
ok: boolean;
|
|
@@ -13,7 +11,7 @@ export interface Health {
|
|
|
13
11
|
}
|
|
14
12
|
export declare const health: (port: number, timeoutMs?: number) => Promise<Health | null>;
|
|
15
13
|
export declare const syncStatus: (port: number, timeoutMs?: number) => Promise<SyncStatus | null>;
|
|
16
|
-
export declare const syncRun: (port: number, vault: string) => Promise<
|
|
14
|
+
export declare const syncRun: (port: number, vault: string) => Promise<SyncResult>;
|
|
17
15
|
export declare const waitForHealth: (port: number, opts?: {
|
|
18
16
|
timeoutMs?: number;
|
|
19
17
|
intervalMs?: number;
|
|
@@ -47,8 +47,7 @@ export const syncStatus = async (port, timeoutMs = 1000) => {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
|
-
// Ask the daemon to sync one vault now; the daemon runs the cycle in its own process.
|
|
51
|
-
// shape depends on the vault's channel (git SyncResult vs account CouchSyncResult).
|
|
50
|
+
// Ask the daemon to sync one vault now; the daemon runs the git cycle in its own process.
|
|
52
51
|
export const syncRun = async (port, vault) => {
|
|
53
52
|
const res = await fetch(`${base(port)}/api/sync/run`, {
|
|
54
53
|
method: 'POST',
|
|
@@ -20,18 +20,20 @@ const checkSite = async (siteUrl, headers) => {
|
|
|
20
20
|
const res = await fetchJsonUnref(siteUrl, 3000, headers);
|
|
21
21
|
return res !== null;
|
|
22
22
|
};
|
|
23
|
-
// Fold the
|
|
24
|
-
//
|
|
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).
|
|
25
26
|
const summarizeSync = (sync) => {
|
|
26
27
|
const git = Array.isArray(sync.vaults) ? sync.vaults : [];
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const error = git.find((v) => v.lastError)?.lastError ?? couch.find((v) => v.lastError)?.lastError;
|
|
30
|
-
const running = git.some((v) => v.running) || couch.some((v) => v.running);
|
|
28
|
+
const error = git.find((v) => v.lastError)?.lastError;
|
|
29
|
+
const running = git.some((v) => v.running);
|
|
31
30
|
const state = error ? 'error' : running ? 'syncing' : 'ok';
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
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 };
|
|
35
37
|
};
|
|
36
38
|
const probeDaemon = async () => {
|
|
37
39
|
const port = resolvePort();
|
|
@@ -21,12 +21,16 @@ const statusCell = (v) => {
|
|
|
21
21
|
return `${mark(false)} error (${shortError(v.lastError)})`;
|
|
22
22
|
case 'unknown':
|
|
23
23
|
return chalk.dim('- unknown (daemon stopped)');
|
|
24
|
+
case 'unsynced':
|
|
25
|
+
return `${mark(false)} not synced - account vaults have no sync channel`;
|
|
24
26
|
case 'idle':
|
|
25
27
|
return v.channel === 'local' ? chalk.dim('- local only') : chalk.dim('- idle');
|
|
26
28
|
}
|
|
27
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.
|
|
28
32
|
const countLabel = (vaults) => {
|
|
29
|
-
const connected = vaults.filter((v) => v.channel
|
|
33
|
+
const connected = vaults.filter((v) => v.channel === 'git').length;
|
|
30
34
|
if (connected > 0)
|
|
31
35
|
return `${connected} connected`;
|
|
32
36
|
const n = vaults.length;
|
|
@@ -38,7 +42,8 @@ export const vaultLines = (vaults) => {
|
|
|
38
42
|
if (vaults.length === 0)
|
|
39
43
|
return [`${'vaults'.padEnd(10)} none - run: agentage vault add <name> --local`];
|
|
40
44
|
const nameW = Math.max(...vaults.map((v) => v.name.length));
|
|
45
|
+
const chanW = Math.max(...vaults.map((v) => v.channel.length));
|
|
41
46
|
const header = `${'vaults'.padEnd(10)} ${countLabel(vaults)}`;
|
|
42
|
-
const rows = vaults.map((v) => ` ${v.name.padEnd(nameW)} ${v.channel.padEnd(
|
|
47
|
+
const rows = vaults.map((v) => ` ${v.name.padEnd(nameW)} ${v.channel.padEnd(chanW)} ${statusCell(v)}`);
|
|
43
48
|
return [header, ...rows];
|
|
44
49
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type VaultsConfig } from '@agentage/memory-core';
|
|
2
2
|
import { type SyncStatus } from '../../sync/git/manager.js';
|
|
3
|
-
export type VaultChannel = 'local' | 'git' | '
|
|
4
|
-
export type VaultSyncState = 'ok' | 'syncing' | 'error' | 'idle' | 'unknown';
|
|
3
|
+
export type VaultChannel = 'local' | 'git' | 'account';
|
|
4
|
+
export type VaultSyncState = 'ok' | 'syncing' | 'error' | 'idle' | 'unknown' | 'unsynced';
|
|
5
5
|
export interface VaultStatus {
|
|
6
6
|
name: string;
|
|
7
7
|
channel: VaultChannel;
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { isAccountVault } from '@agentage/memory-core';
|
|
2
2
|
import { loadVaultsConfig } from '../vault/vaults.js';
|
|
3
|
-
// Config alone decides the channel: an
|
|
4
|
-
// origin
|
|
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.
|
|
5
6
|
const channelOf = (entry) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
: 'local';
|
|
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
11
|
};
|
|
12
|
-
// Live state from the daemon wins; a local-only vault is `idle` (nothing to sync),
|
|
13
|
-
//
|
|
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).
|
|
14
15
|
const stateFrom = (channel, live, daemonUp) => {
|
|
15
16
|
if (channel === 'local')
|
|
16
17
|
return 'idle';
|
|
18
|
+
if (channel === 'account')
|
|
19
|
+
return 'unsynced';
|
|
17
20
|
if (!daemonUp)
|
|
18
21
|
return 'unknown';
|
|
19
22
|
if (!live)
|
|
@@ -24,13 +27,11 @@ const stateFrom = (channel, live, daemonUp) => {
|
|
|
24
27
|
return 'syncing';
|
|
25
28
|
return live.lastRun ? 'ok' : 'idle';
|
|
26
29
|
};
|
|
27
|
-
// Index the daemon's per-vault reports by name
|
|
30
|
+
// Index the daemon's per-vault git reports by name.
|
|
28
31
|
const indexLive = (sync) => {
|
|
29
32
|
const map = new Map();
|
|
30
33
|
for (const v of sync?.vaults ?? [])
|
|
31
34
|
map.set(v.vault, { running: v.running, lastError: v.lastError, lastRun: v.lastRun });
|
|
32
|
-
for (const c of sync?.couch ?? [])
|
|
33
|
-
map.set(c.vault, { running: c.running, lastError: c.lastError, lastRun: c.lastSync });
|
|
34
35
|
return map;
|
|
35
36
|
};
|
|
36
37
|
// The full per-vault picture: every configured vault (so local-only vaults are never hidden),
|
|
@@ -3,9 +3,8 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { expandPath, isAccountVault, validateConfig, } from '@agentage/memory-core';
|
|
4
4
|
import { redactRemoteUrl } from '../../sync/git/remote-url.js';
|
|
5
5
|
import { isValidVaultName } from './vaults.schema.js';
|
|
6
|
-
// The human-facing type of an entry: an agentage origin is an account vault
|
|
7
|
-
//
|
|
8
|
-
// origin without a path is remote.
|
|
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.
|
|
9
8
|
export const vaultType = (entry) => {
|
|
10
9
|
if (isAccountVault(entry))
|
|
11
10
|
return 'account';
|
|
@@ -80,7 +80,7 @@ export const createDiscoverWatcher = (deps = {}) => {
|
|
|
80
80
|
return [];
|
|
81
81
|
for (const c of added) {
|
|
82
82
|
log(`discovered account vault '${c.name}' -> ${c.entry.path}`);
|
|
83
|
-
void provision(c.name).catch(() => { }); // never fatal: the
|
|
83
|
+
void provision(c.name).catch(() => { }); // never fatal: the next scan re-provisions
|
|
84
84
|
}
|
|
85
85
|
return added;
|
|
86
86
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type VaultsConfig } from '@agentage/memory-core';
|
|
2
|
-
import { type CouchTargetStatus } from '../couch/manager.js';
|
|
3
2
|
import { type DiscoverStatus } from '../discover/watcher.js';
|
|
4
3
|
import { type SyncResult } from './cycle.js';
|
|
5
4
|
import { type SyncTarget } from './planner.js';
|
|
@@ -14,7 +13,6 @@ export interface VaultSyncState {
|
|
|
14
13
|
}
|
|
15
14
|
export interface SyncStatus {
|
|
16
15
|
vaults: VaultSyncState[];
|
|
17
|
-
couch?: CouchTargetStatus[];
|
|
18
16
|
discover?: DiscoverStatus;
|
|
19
17
|
}
|
|
20
18
|
export interface SyncManagerDeps {
|
package/dist/sync/git/planner.js
CHANGED
|
@@ -7,15 +7,15 @@ export const DEFAULT_INTERVAL_SECONDS = 300;
|
|
|
7
7
|
// When `ignore` is absent these editor/runtime files are excluded from sync; a set value REPLACES
|
|
8
8
|
// the defaults, and an empty array syncs everything.
|
|
9
9
|
export const DEFAULT_IGNORE = ['.obsidian/', 'data.json'];
|
|
10
|
-
// The reserved
|
|
10
|
+
// The reserved account remote is a sentinel, not a URL: never synced over external git.
|
|
11
11
|
const RESERVED_REMOTE = 'agentage';
|
|
12
12
|
export const resolveIgnore = (ignore) => ignore === undefined ? [...DEFAULT_IGNORE] : ignore;
|
|
13
13
|
export const intervalMs = (seconds) => Math.max(0, Math.floor(seconds)) * 1000;
|
|
14
14
|
// One vault may carry several origins; each gets a distinct remote name within its own repo.
|
|
15
15
|
const remoteNameFor = (index) => (index === 0 ? 'sync' : `sync-${index}`);
|
|
16
16
|
// Flatten (vault, origin) pairs into sync targets. A target needs a local `path` (the working
|
|
17
|
-
// copy to commit/push from) AND an external origin; origin-only entries (
|
|
18
|
-
//
|
|
17
|
+
// copy to commit/push from) AND an external origin; origin-only entries (remote backends) and the
|
|
18
|
+
// reserved account remote are skipped.
|
|
19
19
|
export const syncTargets = (config) => {
|
|
20
20
|
const out = [];
|
|
21
21
|
for (const [vault, entry] of Object.entries(config.vaults ?? {})) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentage/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "The agentage CLI - connect this machine to agentage from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -40,15 +40,15 @@
|
|
|
40
40
|
"prepublishOnly": "npm run verify"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@agentage/memory-core": "^0.
|
|
44
|
-
"@agentage/server-memory": "^0.
|
|
43
|
+
"@agentage/memory-core": "^0.5.0",
|
|
44
|
+
"@agentage/server-memory": "^0.3.0",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
46
|
"chalk": "^6.0.0",
|
|
47
47
|
"commander": "^15.0.0",
|
|
48
48
|
"open": "^11.0.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@anthropic-ai/sdk": "0.
|
|
51
|
+
"@anthropic-ai/sdk": "0.116.0",
|
|
52
52
|
"@playwright/test": "latest",
|
|
53
53
|
"@types/node": "latest",
|
|
54
54
|
"@typescript-eslint/eslint-plugin": "latest",
|