@agentage/cli 0.26.0 → 0.28.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/auth/setup.js +1 -1
- 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 +6 -16
- 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/fs/config.d.ts +1 -1
- package/dist/lib/fs/config.js +8 -3
- 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
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.28.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.4.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.117.1",
|
|
52
52
|
"@playwright/test": "latest",
|
|
53
53
|
"@types/node": "latest",
|
|
54
54
|
"@typescript-eslint/eslint-plugin": "latest",
|
package/dist/sync/couch/cycle.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { ensureWire, pendingCount } from './wire.js';
|
|
2
|
-
// One couch cycle: commit dirty local truth first, drain queued pushes/deletions, then push+pull.
|
|
3
|
-
// Every failure is caught and recorded (lastError / paused); it never throws to the caller.
|
|
4
|
-
export const runCouchCycle = async (rt, st) => {
|
|
5
|
-
const vault = st.target.vault;
|
|
6
|
-
const build = (extra) => ({
|
|
7
|
-
vault,
|
|
8
|
-
channel: 'couch',
|
|
9
|
-
ok: true,
|
|
10
|
-
committed: false,
|
|
11
|
-
pulled: false,
|
|
12
|
-
pendingCount: pendingCount(st),
|
|
13
|
-
...extra,
|
|
14
|
-
});
|
|
15
|
-
if (st.running)
|
|
16
|
-
return build({});
|
|
17
|
-
st.running = true;
|
|
18
|
-
try {
|
|
19
|
-
const bearer = await rt.getBearer();
|
|
20
|
-
if (!bearer) {
|
|
21
|
-
st.paused = 'signed out';
|
|
22
|
-
st.lastError = undefined;
|
|
23
|
-
return build({ paused: 'signed out' });
|
|
24
|
-
}
|
|
25
|
-
const decision = await rt.discovery.channelFor(vault, bearer);
|
|
26
|
-
if (decision.kind === 'paused') {
|
|
27
|
-
st.paused = decision.reason;
|
|
28
|
-
st.lastError = undefined;
|
|
29
|
-
return build({ paused: decision.reason });
|
|
30
|
-
}
|
|
31
|
-
st.paused = undefined;
|
|
32
|
-
const couch = await ensureWire(rt, st, decision);
|
|
33
|
-
const pre = await rt.commitDirty(st.target.path, `sync: ${rt.nowIso()}`);
|
|
34
|
-
await couch.flushPending(); // drain queued pushes AND queued deletions first
|
|
35
|
-
const res = await couch.syncNow(); // pushAll + reconcile deletions, then pullOnce
|
|
36
|
-
const post = await rt.commitDirty(st.target.path, `sync: couch ${rt.nowIso()}`);
|
|
37
|
-
if (res.error) {
|
|
38
|
-
st.lastError = res.error;
|
|
39
|
-
return build({
|
|
40
|
-
ok: false,
|
|
41
|
-
committed: pre.committed,
|
|
42
|
-
pulled: post.committed,
|
|
43
|
-
error: res.error,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
st.lastSync = rt.nowIso();
|
|
47
|
-
st.lastError = undefined;
|
|
48
|
-
return build({ committed: pre.committed, pulled: post.committed });
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
52
|
-
st.lastError = msg;
|
|
53
|
-
return build({ ok: false, error: msg });
|
|
54
|
-
}
|
|
55
|
-
finally {
|
|
56
|
-
st.running = false;
|
|
57
|
-
}
|
|
58
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type FetchJson } from '@agentage/memory-core';
|
|
2
|
-
import { type ProvisionResult } from '../../lib/auth/provision.js';
|
|
3
|
-
export type ChannelDecision = {
|
|
4
|
-
kind: 'couch';
|
|
5
|
-
endpoint: string;
|
|
6
|
-
db: string;
|
|
7
|
-
tokenUrl: string;
|
|
8
|
-
} | {
|
|
9
|
-
kind: 'paused';
|
|
10
|
-
reason: string;
|
|
11
|
-
};
|
|
12
|
-
export interface DiscoveryDeps {
|
|
13
|
-
bootstrapHost: string;
|
|
14
|
-
fetchJson: FetchJson;
|
|
15
|
-
provision: (vault: string) => Promise<ProvisionResult>;
|
|
16
|
-
now?: () => number;
|
|
17
|
-
}
|
|
18
|
-
export interface Discovery {
|
|
19
|
-
channelFor(vault: string, token: string): Promise<ChannelDecision>;
|
|
20
|
-
reset(): void;
|
|
21
|
-
}
|
|
22
|
-
export declare const createDiscovery: (deps: DiscoveryDeps) => Discovery;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { channelForVault, HostResolver } from '@agentage/memory-core';
|
|
2
|
-
const pausedReason = (prov) => {
|
|
3
|
-
switch (prov.status) {
|
|
4
|
-
case 'disabled':
|
|
5
|
-
return 'account sync is not enabled on this server';
|
|
6
|
-
case 'conflict':
|
|
7
|
-
return 'name conflicts with a memory on another channel';
|
|
8
|
-
case 'unauthenticated':
|
|
9
|
-
return 'signed out';
|
|
10
|
-
default:
|
|
11
|
-
return 'provisioning - will retry';
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
export const createDiscovery = (deps) => {
|
|
15
|
-
const resolver = new HostResolver(deps.bootstrapHost, deps.fetchJson, deps.now ?? Date.now);
|
|
16
|
-
const toCouch = (ch) => ch.channel === 'couch'
|
|
17
|
-
? { kind: 'couch', endpoint: ch.endpoint, db: ch.db, tokenUrl: ch.tokenUrl }
|
|
18
|
-
: null;
|
|
19
|
-
return {
|
|
20
|
-
async channelFor(vault, token) {
|
|
21
|
-
const first = toCouch(channelForVault(await resolver.resolve(token), vault));
|
|
22
|
-
if (first)
|
|
23
|
-
return first;
|
|
24
|
-
// Missing from couch_vaults: provision once, refresh discovery once, re-check.
|
|
25
|
-
const prov = await deps.provision(vault);
|
|
26
|
-
resolver.invalidate();
|
|
27
|
-
const second = toCouch(channelForVault(await resolver.resolve(token), vault));
|
|
28
|
-
return second ?? { kind: 'paused', reason: pausedReason(prov) };
|
|
29
|
-
},
|
|
30
|
-
reset() {
|
|
31
|
-
resolver.invalidate();
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join, relative, sep } from 'node:path';
|
|
4
|
-
// The couch channel speaks vault-relative POSIX paths; on Windows the fs layer still uses `\`.
|
|
5
|
-
const toPosix = (p) => (sep === '/' ? p : p.split(sep).join('/'));
|
|
6
|
-
const fromPosix = (p) => (sep === '/' ? p : p.split('/').join(sep));
|
|
7
|
-
// Recursively collect *.md under root as vault-relative POSIX paths. Dot-directories are skipped -
|
|
8
|
-
// `.git` (the engine's own repo) must never enter the content-addressed model, and editor state
|
|
9
|
-
// like `.obsidian/` holds no synced notes; this mirrors memory-core's own local listing.
|
|
10
|
-
const walk = async (root, dir, acc) => {
|
|
11
|
-
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
12
|
-
for (const e of entries) {
|
|
13
|
-
if (e.name.startsWith('.'))
|
|
14
|
-
continue;
|
|
15
|
-
const abs = join(dir, e.name);
|
|
16
|
-
if (e.isDirectory())
|
|
17
|
-
await walk(root, abs, acc);
|
|
18
|
-
else if (e.isFile() && e.name.endsWith('.md'))
|
|
19
|
-
acc.push(toPosix(relative(root, abs)));
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
// A FileStore rooted at one account vault's mirror dir: the seam CouchSync reads/writes through.
|
|
23
|
-
export const createFileStore = (root) => ({
|
|
24
|
-
async listMarkdown() {
|
|
25
|
-
if (!existsSync(root))
|
|
26
|
-
return [];
|
|
27
|
-
const acc = [];
|
|
28
|
-
await walk(root, root, acc);
|
|
29
|
-
return acc.sort();
|
|
30
|
-
},
|
|
31
|
-
async read(path) {
|
|
32
|
-
try {
|
|
33
|
-
return await readFile(join(root, fromPosix(path)), 'utf8');
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return null; // gone from the file set
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
async write(path, body) {
|
|
40
|
-
const abs = join(root, fromPosix(path));
|
|
41
|
-
await mkdir(dirname(abs), { recursive: true });
|
|
42
|
-
await writeFile(abs, body, 'utf8');
|
|
43
|
-
},
|
|
44
|
-
async remove(path) {
|
|
45
|
-
await rm(join(root, fromPosix(path)), { force: true });
|
|
46
|
-
},
|
|
47
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { createSyncGit, GitError } from '../git/git-exec.js';
|
|
4
|
-
// The default local-git commit: stage everything and make one commit when the tree is dirty. An
|
|
5
|
-
// index.lock collision (the engine mid-mutation) is a clean skip - the change stays for next cycle.
|
|
6
|
-
export const gitCommitDirty = async (path, message) => {
|
|
7
|
-
if (!existsSync(path))
|
|
8
|
-
return { committed: false, skipped: false };
|
|
9
|
-
const git = createSyncGit(path);
|
|
10
|
-
try {
|
|
11
|
-
if (!existsSync(join(path, '.git')))
|
|
12
|
-
await git.run(['init', '-b', 'main']);
|
|
13
|
-
await git.run(['add', '-A']);
|
|
14
|
-
if ((await git.exec(['diff', '--cached', '--quiet'])).code === 0)
|
|
15
|
-
return { committed: false, skipped: false };
|
|
16
|
-
await git.run(['commit', '-m', message]);
|
|
17
|
-
return { committed: true, skipped: false };
|
|
18
|
-
}
|
|
19
|
-
catch (err) {
|
|
20
|
-
if (err instanceof GitError && err.kind === 'lock')
|
|
21
|
-
return { committed: false, skipped: true };
|
|
22
|
-
throw err;
|
|
23
|
-
}
|
|
24
|
-
};
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { type CouchSyncManager, type CouchSyncManagerDeps } from './manager.types.js';
|
|
2
|
-
export { type CouchSyncManager, type CouchSyncManagerDeps, type CouchSyncResult, type CouchTargetStatus, } from './manager.types.js';
|
|
3
|
-
export { resolveMutationTarget } from './mutation-target.js';
|
|
4
|
-
export declare const createCouchSyncManager: (deps?: CouchSyncManagerDeps) => CouchSyncManager;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { type FileStore, type VaultsConfig } from '@agentage/memory-core';
|
|
2
|
-
import { type ChannelDecision, type Discovery } from './discovery.js';
|
|
3
|
-
import { type CouchSyncManagerDeps } from './manager.js';
|
|
4
|
-
export declare const config: VaultsConfig;
|
|
5
|
-
export declare const autoConfig: VaultsConfig;
|
|
6
|
-
export declare const noopStore: () => FileStore;
|
|
7
|
-
export declare const couchDecision: ChannelDecision;
|
|
8
|
-
export declare const makeManager: (over?: Partial<CouchSyncManagerDeps>) => {
|
|
9
|
-
mgr: import("./manager.types.js").CouchSyncManager;
|
|
10
|
-
couch: {
|
|
11
|
-
pushFileLive: import("vitest").Mock<() => Promise<void>>;
|
|
12
|
-
removeFile: import("vitest").Mock<() => Promise<void>>;
|
|
13
|
-
flushPending: import("vitest").Mock<() => Promise<void>>;
|
|
14
|
-
syncNow: import("vitest").Mock<() => Promise<{
|
|
15
|
-
pushed: boolean;
|
|
16
|
-
pulled: boolean;
|
|
17
|
-
}>>;
|
|
18
|
-
};
|
|
19
|
-
discovery: Discovery;
|
|
20
|
-
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { vi } from 'vitest';
|
|
2
|
-
import { createCouchSyncManager } from './manager.js';
|
|
3
|
-
export const config = {
|
|
4
|
-
version: 1,
|
|
5
|
-
default: 'acct',
|
|
6
|
-
vaults: {
|
|
7
|
-
acct: { path: '/tmp/acct', origin: [{ remote: 'agentage', interval: 0 }] },
|
|
8
|
-
git: { path: '/tmp/git', origin: [{ remote: 'git@h:g.git' }] },
|
|
9
|
-
local: { path: '/tmp/local' },
|
|
10
|
-
},
|
|
11
|
-
};
|
|
12
|
-
export const autoConfig = {
|
|
13
|
-
version: 1,
|
|
14
|
-
default: 'acct',
|
|
15
|
-
vaults: {
|
|
16
|
-
acct: { path: '/tmp/acct', origin: [{ remote: 'agentage', interval: 300 }] },
|
|
17
|
-
two: { path: '/tmp/two', origin: [{ remote: 'agentage', interval: 300 }] },
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
export const noopStore = () => ({
|
|
21
|
-
listMarkdown: async () => [],
|
|
22
|
-
read: async () => null,
|
|
23
|
-
write: async () => { },
|
|
24
|
-
remove: async () => { },
|
|
25
|
-
});
|
|
26
|
-
export const couchDecision = {
|
|
27
|
-
kind: 'couch',
|
|
28
|
-
endpoint: 'https://couch.test',
|
|
29
|
-
db: 'mem_acct',
|
|
30
|
-
tokenUrl: 'https://auth.test/couch-token',
|
|
31
|
-
};
|
|
32
|
-
export const makeManager = (over = {}) => {
|
|
33
|
-
const couch = {
|
|
34
|
-
pushFileLive: vi.fn(async () => { }),
|
|
35
|
-
removeFile: vi.fn(async () => { }),
|
|
36
|
-
flushPending: vi.fn(async () => { }),
|
|
37
|
-
syncNow: vi.fn(async () => ({ pushed: true, pulled: true })),
|
|
38
|
-
};
|
|
39
|
-
const discovery = {
|
|
40
|
-
channelFor: vi.fn(async () => couchDecision),
|
|
41
|
-
reset: vi.fn(),
|
|
42
|
-
};
|
|
43
|
-
const mgr = createCouchSyncManager({
|
|
44
|
-
getConfig: () => config,
|
|
45
|
-
configDir: () => '/tmp/cfg',
|
|
46
|
-
getBearer: async () => 'tok',
|
|
47
|
-
discovery,
|
|
48
|
-
makeCouchSync: () => couch,
|
|
49
|
-
makeFileStore: noopStore,
|
|
50
|
-
makeStatePersistence: () => ({ load: async () => null, save: async () => { } }),
|
|
51
|
-
commitDirty: async () => ({ committed: false, skipped: false }),
|
|
52
|
-
now: () => '2026-01-01T00:00:00Z',
|
|
53
|
-
...over,
|
|
54
|
-
});
|
|
55
|
-
return { mgr, couch, discovery };
|
|
56
|
-
};
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { CouchSync } from '@agentage/memory-core';
|
|
2
|
-
import { currentBearer } from '../../lib/auth/api.js';
|
|
3
|
-
import { getConfigDir, readAuth } from '../../lib/fs/config.js';
|
|
4
|
-
import { links, siteFqdn } from '../../lib/net/origins.js';
|
|
5
|
-
import { requestHeaders } from '../../lib/net/user-agent.js';
|
|
6
|
-
import { defaultProvisionDeps, provisionAccountVault } from '../../lib/auth/provision.js';
|
|
7
|
-
import { loadVaultsConfig } from '../../lib/vault/vaults.js';
|
|
8
|
-
import { intervalMs } from '../git/planner.js';
|
|
9
|
-
import { runCouchCycle } from './cycle.js';
|
|
10
|
-
import { createDiscovery } from './discovery.js';
|
|
11
|
-
import { createFileStore } from './file-store.js';
|
|
12
|
-
import { gitCommitDirty } from './local-commit.js';
|
|
13
|
-
import { resolveMutationTarget } from './mutation-target.js';
|
|
14
|
-
import { pushOnWrite } from './push-on-write.js';
|
|
15
|
-
import { createStatePersistence } from './state-store.js';
|
|
16
|
-
import { autoCouchTargets, couchTargets } from './targets.js';
|
|
17
|
-
import { getState, pendingCount } from './wire.js';
|
|
18
|
-
export { resolveMutationTarget } from './mutation-target.js';
|
|
19
|
-
// Couch sync runs inside the daemon; identify the caller as the daemon on every outbound request.
|
|
20
|
-
const daemonHeaders = () => requestHeaders({ component: 'daemon' });
|
|
21
|
-
const defaultFetch = (url, init) => {
|
|
22
|
-
const base = init;
|
|
23
|
-
const merged = { ...base, headers: { ...daemonHeaders(), ...base?.headers } };
|
|
24
|
-
return globalThis.fetch(url, merged);
|
|
25
|
-
};
|
|
26
|
-
const defaultFetchJson = async (url, token) => {
|
|
27
|
-
const res = await globalThis.fetch(url, {
|
|
28
|
-
headers: { authorization: `Bearer ${token}`, ...daemonHeaders() },
|
|
29
|
-
});
|
|
30
|
-
const json = await res.json().catch(() => null);
|
|
31
|
-
return { status: res.status, json };
|
|
32
|
-
};
|
|
33
|
-
// The daemon-side couch scheduler: per-account-vault timers + a persistent CouchSync per target for
|
|
34
|
-
// sync-on-save. Every couch failure is caught and recorded (lastError / paused); it never crashes
|
|
35
|
-
// the daemon and never blocks a memory API response.
|
|
36
|
-
export const createCouchSyncManager = (deps = {}) => {
|
|
37
|
-
const getConfig = deps.getConfig ?? (() => loadVaultsConfig().config);
|
|
38
|
-
const getBearer = deps.getBearer ?? (() => currentBearer(readAuth, links(siteFqdn())));
|
|
39
|
-
const makeFileStore = deps.makeFileStore ?? createFileStore;
|
|
40
|
-
const makeCouchSync = deps.makeCouchSync ??
|
|
41
|
-
((files, cfg, f, authorize, onUnauthorized, state, log) => new CouchSync(files, cfg, f, authorize, onUnauthorized, state, log));
|
|
42
|
-
const rt = {
|
|
43
|
-
configDir: deps.configDir ?? getConfigDir,
|
|
44
|
-
getBearer,
|
|
45
|
-
fetch: deps.fetch ?? defaultFetch,
|
|
46
|
-
makeCouchSync,
|
|
47
|
-
makeStatePersistence: deps.makeStatePersistence ?? createStatePersistence,
|
|
48
|
-
commitDirty: deps.commitDirty ?? gitCommitDirty,
|
|
49
|
-
discovery: deps.discovery ??
|
|
50
|
-
createDiscovery({
|
|
51
|
-
bootstrapHost: links(siteFqdn()).sync,
|
|
52
|
-
fetchJson: defaultFetchJson,
|
|
53
|
-
provision: (vault) => provisionAccountVault(vault, defaultProvisionDeps()),
|
|
54
|
-
}),
|
|
55
|
-
nowIso: deps.now ?? (() => new Date().toISOString()),
|
|
56
|
-
log: deps.log ?? (() => { }),
|
|
57
|
-
};
|
|
58
|
-
const states = new Map();
|
|
59
|
-
const timers = new Map();
|
|
60
|
-
const ensureTargetState = (target) => {
|
|
61
|
-
const existing = states.get(target.vault);
|
|
62
|
-
if (existing) {
|
|
63
|
-
existing.target = target;
|
|
64
|
-
return existing;
|
|
65
|
-
}
|
|
66
|
-
const fresh = { target, files: makeFileStore(target.path), running: false };
|
|
67
|
-
states.set(target.vault, fresh);
|
|
68
|
-
return fresh;
|
|
69
|
-
};
|
|
70
|
-
return {
|
|
71
|
-
reschedule() {
|
|
72
|
-
for (const timer of timers.values())
|
|
73
|
-
clearInterval(timer);
|
|
74
|
-
timers.clear();
|
|
75
|
-
const targets = couchTargets(getConfig());
|
|
76
|
-
const live = new Set(targets.map((t) => t.vault));
|
|
77
|
-
for (const vault of [...states.keys()])
|
|
78
|
-
if (!live.has(vault))
|
|
79
|
-
states.delete(vault);
|
|
80
|
-
for (const t of targets)
|
|
81
|
-
void getState(rt, ensureTargetState(t)).catch(() => { });
|
|
82
|
-
for (const t of autoCouchTargets(getConfig())) {
|
|
83
|
-
const timer = setInterval(() => void runCouchCycle(rt, ensureTargetState(t)), intervalMs(t.intervalSeconds));
|
|
84
|
-
timer.unref?.();
|
|
85
|
-
timers.set(t.vault, timer);
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
async runNow(vault) {
|
|
89
|
-
const t = couchTargets(getConfig()).find((x) => x.vault === vault);
|
|
90
|
-
if (!t)
|
|
91
|
-
throw new Error(`'${vault}' is not an account vault`);
|
|
92
|
-
return runCouchCycle(rt, ensureTargetState(t));
|
|
93
|
-
},
|
|
94
|
-
onWrite(verb, body) {
|
|
95
|
-
if (verb !== 'write' && verb !== 'edit' && verb !== 'delete')
|
|
96
|
-
return;
|
|
97
|
-
const target = resolveMutationTarget(getConfig(), body);
|
|
98
|
-
if (!target)
|
|
99
|
-
return;
|
|
100
|
-
const t = couchTargets(getConfig()).find((x) => x.vault === target.vault);
|
|
101
|
-
if (!t)
|
|
102
|
-
return;
|
|
103
|
-
void pushOnWrite(rt, ensureTargetState(t), verb, target.path);
|
|
104
|
-
},
|
|
105
|
-
status() {
|
|
106
|
-
return couchTargets(getConfig()).map((t) => {
|
|
107
|
-
const st = states.get(t.vault);
|
|
108
|
-
return {
|
|
109
|
-
vault: t.vault,
|
|
110
|
-
channel: 'couch',
|
|
111
|
-
intervalSeconds: t.intervalSeconds,
|
|
112
|
-
lastSync: st?.lastSync,
|
|
113
|
-
lastError: st?.lastError,
|
|
114
|
-
pendingCount: pendingCount(st),
|
|
115
|
-
paused: st?.paused,
|
|
116
|
-
running: st?.running ?? false,
|
|
117
|
-
};
|
|
118
|
-
});
|
|
119
|
-
},
|
|
120
|
-
stop() {
|
|
121
|
-
for (const timer of timers.values())
|
|
122
|
-
clearInterval(timer);
|
|
123
|
-
timers.clear();
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
};
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { type CouchState, type CouchStatePersistence, type FetchLike, type FileStore, type SyncResult as CouchChannelResult, type VaultsConfig } from '@agentage/memory-core';
|
|
2
|
-
import { type MemoryVerb } from '../../daemon/actions.js';
|
|
3
|
-
import { type Discovery } from './discovery.js';
|
|
4
|
-
import { type CouchTarget } from './targets.js';
|
|
5
|
-
export interface CouchSyncResult {
|
|
6
|
-
vault: string;
|
|
7
|
-
channel: 'couch';
|
|
8
|
-
ok: boolean;
|
|
9
|
-
committed: boolean;
|
|
10
|
-
pulled: boolean;
|
|
11
|
-
pendingCount: number;
|
|
12
|
-
paused?: string;
|
|
13
|
-
error?: string;
|
|
14
|
-
}
|
|
15
|
-
export interface CouchTargetStatus {
|
|
16
|
-
vault: string;
|
|
17
|
-
channel: 'couch';
|
|
18
|
-
intervalSeconds: number;
|
|
19
|
-
lastSync?: string;
|
|
20
|
-
lastError?: string;
|
|
21
|
-
pendingCount: number;
|
|
22
|
-
paused?: string;
|
|
23
|
-
running: boolean;
|
|
24
|
-
}
|
|
25
|
-
export interface CouchLike {
|
|
26
|
-
pushFileLive(path: string): Promise<void>;
|
|
27
|
-
removeFile(path: string): Promise<void>;
|
|
28
|
-
flushPending(): Promise<void>;
|
|
29
|
-
syncNow(): Promise<CouchChannelResult>;
|
|
30
|
-
}
|
|
31
|
-
export type MakeCouchSync = (files: FileStore, cfg: {
|
|
32
|
-
endpoint: string;
|
|
33
|
-
db: string;
|
|
34
|
-
}, fetch: FetchLike, authorize: () => Promise<string>, onUnauthorized: () => void, state: CouchState, log?: (msg: string) => void) => CouchLike;
|
|
35
|
-
export interface CommitOutcome {
|
|
36
|
-
committed: boolean;
|
|
37
|
-
skipped: boolean;
|
|
38
|
-
}
|
|
39
|
-
export interface CouchSyncManagerDeps {
|
|
40
|
-
getConfig?: () => VaultsConfig;
|
|
41
|
-
configDir?: () => string;
|
|
42
|
-
getBearer?: () => Promise<string | null>;
|
|
43
|
-
discovery?: Discovery;
|
|
44
|
-
fetch?: FetchLike;
|
|
45
|
-
makeFileStore?: (path: string) => FileStore;
|
|
46
|
-
makeStatePersistence?: (configDir: string, vault: string) => CouchStatePersistence;
|
|
47
|
-
makeCouchSync?: MakeCouchSync;
|
|
48
|
-
commitDirty?: (path: string, message: string) => Promise<CommitOutcome>;
|
|
49
|
-
now?: () => string;
|
|
50
|
-
log?: (msg: string) => void;
|
|
51
|
-
}
|
|
52
|
-
export interface CouchSyncManager {
|
|
53
|
-
reschedule(): void;
|
|
54
|
-
runNow(vault: string): Promise<CouchSyncResult>;
|
|
55
|
-
onWrite(verb: MemoryVerb, body: unknown): void;
|
|
56
|
-
status(): CouchTargetStatus[];
|
|
57
|
-
stop(): void;
|
|
58
|
-
}
|
|
59
|
-
export interface TargetState {
|
|
60
|
-
target: CouchTarget;
|
|
61
|
-
files: FileStore;
|
|
62
|
-
state?: CouchState;
|
|
63
|
-
statePromise?: Promise<CouchState>;
|
|
64
|
-
couch?: CouchLike;
|
|
65
|
-
wireKey?: string;
|
|
66
|
-
running: boolean;
|
|
67
|
-
lastSync?: string;
|
|
68
|
-
lastError?: string;
|
|
69
|
-
paused?: string;
|
|
70
|
-
}
|
|
71
|
-
export interface CouchRuntime {
|
|
72
|
-
configDir: () => string;
|
|
73
|
-
getBearer: () => Promise<string | null>;
|
|
74
|
-
fetch: FetchLike;
|
|
75
|
-
makeCouchSync: MakeCouchSync;
|
|
76
|
-
makeStatePersistence: (configDir: string, vault: string) => CouchStatePersistence;
|
|
77
|
-
commitDirty: (path: string, message: string) => Promise<CommitOutcome>;
|
|
78
|
-
discovery: Discovery;
|
|
79
|
-
nowIso: () => string;
|
|
80
|
-
log: (msg: string) => void;
|
|
81
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { isAccountVault } from '@agentage/memory-core';
|
|
2
|
-
// Map one memory-verb wire payload to the account vault + vault-relative POSIX path it mutated, or
|
|
3
|
-
// null when the target is not an account vault (git/local mutations never touch the couch channel).
|
|
4
|
-
export const resolveMutationTarget = (config, body) => {
|
|
5
|
-
const p = (body ?? {});
|
|
6
|
-
const ref = typeof p.ref === 'string' ? p.ref : '';
|
|
7
|
-
if (!ref)
|
|
8
|
-
return null;
|
|
9
|
-
let vault;
|
|
10
|
-
let path;
|
|
11
|
-
if (ref.startsWith('@')) {
|
|
12
|
-
const m = ref.match(/^@([^/]+)\/(.+)$/);
|
|
13
|
-
if (!m)
|
|
14
|
-
return null; // a bare '@vault' is not a file mutation
|
|
15
|
-
vault = m[1];
|
|
16
|
-
path = m[2];
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
vault = (typeof p.opts?.vault === 'string' ? p.opts.vault : undefined) ?? config.default;
|
|
20
|
-
if (!vault) {
|
|
21
|
-
const names = Object.keys(config.vaults ?? {});
|
|
22
|
-
if (names.length === 1)
|
|
23
|
-
vault = names[0];
|
|
24
|
-
}
|
|
25
|
-
path = ref;
|
|
26
|
-
}
|
|
27
|
-
if (!vault)
|
|
28
|
-
return null;
|
|
29
|
-
const entry = config.vaults?.[vault];
|
|
30
|
-
if (!entry || !isAccountVault(entry))
|
|
31
|
-
return null;
|
|
32
|
-
return { vault, path: path.replace(/^\.?\//, '') };
|
|
33
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { ensureWire, getState } from './wire.js';
|
|
2
|
-
// Sync-on-save: push (or tombstone) one path right after the engine committed it. Failures queue
|
|
3
|
-
// the path in the module's persisted pending/deletion sets (retried by the next cycle) and never
|
|
4
|
-
// surface to the API. A delete is durable regardless of auth/network state at delete time: with
|
|
5
|
-
// no wire it enqueues the deletion, and removeFile itself self-enqueues on transport failure.
|
|
6
|
-
export const pushOnWrite = async (rt, st, verb, path) => {
|
|
7
|
-
const defer = async () => {
|
|
8
|
-
const state = await getState(rt, st);
|
|
9
|
-
if (verb === 'delete')
|
|
10
|
-
await state.enqueueDeletion(path);
|
|
11
|
-
else
|
|
12
|
-
await state.enqueue(path);
|
|
13
|
-
};
|
|
14
|
-
try {
|
|
15
|
-
const bearer = await rt.getBearer();
|
|
16
|
-
if (bearer) {
|
|
17
|
-
const decision = await rt.discovery.channelFor(st.target.vault, bearer);
|
|
18
|
-
if (decision.kind === 'couch') {
|
|
19
|
-
const couch = await ensureWire(rt, st, decision);
|
|
20
|
-
if (verb === 'delete')
|
|
21
|
-
await couch.removeFile(path);
|
|
22
|
-
else
|
|
23
|
-
await couch.pushFileLive(path);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
await defer(); // no wire yet (signed out / paused) - queued until one exists
|
|
28
|
-
}
|
|
29
|
-
catch (err) {
|
|
30
|
-
rt.log(`couch push-on-write ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
31
|
-
await defer().catch(() => { });
|
|
32
|
-
}
|
|
33
|
-
};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
-
import { readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
// The couch sync cursor + rev-cache + pending queue for one vault, at
|
|
5
|
-
// <configDir>/couch-state/<vault>.json. Load returns null on a missing OR unparseable file so a
|
|
6
|
-
// corrupt state degrades to a fresh from-scratch sync rather than crashing the daemon; save is
|
|
7
|
-
// atomic (temp + rename) so a crash mid-write never truncates it.
|
|
8
|
-
export const couchStateDir = (configDir) => join(configDir, 'couch-state');
|
|
9
|
-
export const createStatePersistence = (configDir, vault) => {
|
|
10
|
-
const dir = couchStateDir(configDir);
|
|
11
|
-
const path = join(dir, `${encodeURIComponent(vault)}.json`);
|
|
12
|
-
return {
|
|
13
|
-
async load() {
|
|
14
|
-
try {
|
|
15
|
-
return JSON.parse(await readFile(path, 'utf8'));
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
async save(state) {
|
|
22
|
-
mkdirSync(dir, { recursive: true });
|
|
23
|
-
const tmp = `${path}.tmp`;
|
|
24
|
-
await writeFile(tmp, JSON.stringify(state), 'utf8');
|
|
25
|
-
await rename(tmp, path);
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
};
|