@claude-flow/cli 3.55.0 → 3.56.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 (64) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/.claude/settings.json +9 -9
  3. package/catalog-manifest.json +2 -2
  4. package/dist/src/auth/client.js +40 -24
  5. package/dist/src/commands/agent.js +12 -0
  6. package/dist/src/commands/analyze.js +18 -14
  7. package/dist/src/commands/daemon.js +11 -19
  8. package/dist/src/commands/doctor.js +65 -58
  9. package/dist/src/commands/memory-backup.d.ts +1 -1
  10. package/dist/src/commands/memory-backup.js +23 -9
  11. package/dist/src/commands/memory.d.ts +0 -4
  12. package/dist/src/commands/memory.js +10 -2
  13. package/dist/src/commands/plugins.js +13 -1
  14. package/dist/src/commands/task.js +12 -0
  15. package/dist/src/init/settings-generator.js +3 -3
  16. package/dist/src/mcp-tools/agent-execute-core.js +244 -188
  17. package/dist/src/mcp-tools/agent-tools.js +15 -16
  18. package/dist/src/mcp-tools/agentdb-tools.js +22 -3
  19. package/dist/src/mcp-tools/claims-tools.js +3 -11
  20. package/dist/src/mcp-tools/hooks-tools.js +47 -2
  21. package/dist/src/mcp-tools/memory-tools.js +29 -10
  22. package/dist/src/mcp-tools/record-store.d.ts +3 -0
  23. package/dist/src/mcp-tools/record-store.js +21 -0
  24. package/dist/src/mcp-tools/session-tools.js +47 -13
  25. package/dist/src/mcp-tools/task-tools.js +5 -11
  26. package/dist/src/mcp-tools/workflow-tools.js +78 -29
  27. package/dist/src/memory/append-conditions.d.ts +28 -0
  28. package/dist/src/memory/append-conditions.js +56 -0
  29. package/dist/src/memory/graph-edge-writer.d.ts +9 -0
  30. package/dist/src/memory/graph-edge-writer.js +90 -0
  31. package/dist/src/memory/memory-bridge.d.ts +4 -0
  32. package/dist/src/memory/memory-bridge.js +50 -9
  33. package/dist/src/memory/memory-initializer.d.ts +4 -0
  34. package/dist/src/memory/memory-initializer.js +95 -61
  35. package/dist/src/memory/memory-root.d.ts +2 -0
  36. package/dist/src/memory/memory-root.js +28 -0
  37. package/dist/src/memory/sona-optimizer.d.ts +2 -0
  38. package/dist/src/memory/sona-optimizer.js +4 -0
  39. package/dist/src/parser.js +7 -0
  40. package/dist/src/plugins/store/discovery.d.ts +6 -0
  41. package/dist/src/plugins/store/discovery.js +7 -3
  42. package/dist/src/ruvector/neural-router.js +7 -2
  43. package/dist/src/services/claim-service.js +39 -18
  44. package/dist/src/services/claims-store.d.ts +3 -0
  45. package/dist/src/services/claims-store.js +40 -0
  46. package/dist/src/services/distill-tuning.js +17 -10
  47. package/dist/src/services/memory-backup.d.ts +7 -0
  48. package/dist/src/services/memory-backup.js +29 -2
  49. package/dist/src/services/memory-distillation.js +2 -2
  50. package/dist/src/services/worker-daemon.d.ts +4 -2
  51. package/dist/src/services/worker-daemon.js +14 -6
  52. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  53. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
  54. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
  55. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
  56. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
  57. package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
  58. package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
  59. package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
  60. package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
  61. package/node_modules/@claude-flow/security/package.json +1 -1
  62. package/package.json +4 -4
  63. package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
  64. package/.claude/agents/tmp.json +0 -0
@@ -32,6 +32,12 @@ export interface PluginDiscoveryResult {
32
32
  source?: string;
33
33
  fromCache?: boolean;
34
34
  error?: string;
35
+ /**
36
+ * True when the signed registry could not be fetched or verified and the CLI fell back to
37
+ * its built-in plugin list. That list is safe to use (fixed, first-party npm names), but it
38
+ * is NOT the verified registry, and callers must say so rather than report a discovery.
39
+ */
40
+ demo?: boolean;
35
41
  }
36
42
  /**
37
43
  * Plugin Discovery Service
@@ -3,7 +3,6 @@
3
3
  * Discovers plugin registries via IPNS and fetches from IPFS
4
4
  * Parallel implementation to pattern store for plugins
5
5
  */
6
- import * as crypto from 'crypto';
7
6
  import { resolveIPNS, fetchFromIPFS, verifyEd25519Signature } from '../../transfer/ipfs/client.js';
8
7
  /**
9
8
  * Fetch real npm download stats for a package
@@ -115,7 +114,9 @@ export class PluginDiscoveryService {
115
114
  success: true,
116
115
  registry: cached.registry,
117
116
  fromCache: true,
118
- source: registry.name,
117
+ // A cached fallback is still the fallback: never relabel it as the real registry.
118
+ source: cached.demo ? `${registry.name} (demo)` : registry.name,
119
+ ...(cached.demo ? { demo: true } : {}),
119
120
  };
120
121
  }
121
122
  try {
@@ -223,13 +224,16 @@ export class PluginDiscoveryService {
223
224
  this.cache.set(registry.ipnsName, {
224
225
  registry: demoRegistry,
225
226
  timestamp: Date.now(),
227
+ demo: true,
226
228
  });
227
229
  return {
228
230
  success: true,
229
231
  registry: demoRegistry,
230
- cid: `bafybeiplugin${crypto.randomBytes(16).toString('hex')}`,
232
+ // No CID: the built-in list was not fetched from IPFS. (This used to be a random
233
+ // `bafybeiplugin…` string, printed as "Registry CID" as if it were a real address.)
231
234
  source: `${registry.name} (demo)`,
232
235
  fromCache: false,
236
+ demo: true,
233
237
  };
234
238
  }
235
239
  /**
@@ -288,8 +288,13 @@ async function resolveBackend(cfg) {
288
288
  return r;
289
289
  return {
290
290
  route: (e) => {
291
- const result = r.route(e);
292
- return { ...result, predictedQuality: cal.transform(result.predictedQuality) };
291
+ // Calibration can move a candidate across the quality bar. Select
292
+ // from the calibrated predictions, not the raw router's old pick.
293
+ const all = r.predictAll(e).map(c => ({ ...c, predictedQuality: cal.transform(c.predictedQuality) }));
294
+ const clearing = all.filter(c => c.predictedQuality >= cfg.qualityBar)
295
+ .sort((a, b) => a.costPerMTok - b.costPerMTok);
296
+ const pick = clearing[0] ?? [...all].sort((a, b) => b.predictedQuality - a.predictedQuality)[0];
297
+ return { ...pick, metBar: pick.predictedQuality >= cfg.qualityBar };
293
298
  },
294
299
  predictAll: (e) => r.predictAll(e).map(c => ({ ...c, predictedQuality: cal.transform(c.predictedQuality) })),
295
300
  };
@@ -16,6 +16,7 @@ import { EventEmitter } from 'events';
16
16
  import * as fs from 'fs';
17
17
  import * as path from 'path';
18
18
  import { execFileSync } from 'child_process';
19
+ import { readClaimsStore } from './claims-store.js';
19
20
  // ============================================================================
20
21
  // Default Configuration
21
22
  // ============================================================================
@@ -57,28 +58,29 @@ export class ClaimService extends EventEmitter {
57
58
  // Load existing claims
58
59
  await this.loadClaims();
59
60
  }
60
- async loadClaims() {
61
- const claimsFile = path.join(this.storagePath, 'claims.json');
62
- if (fs.existsSync(claimsFile)) {
63
- try {
64
- const data = JSON.parse(fs.readFileSync(claimsFile, 'utf-8'));
65
- for (const claim of data.claims || []) {
66
- claim.claimedAt = new Date(claim.claimedAt);
67
- claim.statusChangedAt = new Date(claim.statusChangedAt);
68
- if (claim.expiresAt)
69
- claim.expiresAt = new Date(claim.expiresAt);
70
- this.claims.set(claim.issueId, claim);
71
- }
72
- }
73
- catch {
74
- // Start fresh if file is corrupted
75
- }
61
+ loadClaims() {
62
+ const data = readClaimsStore(path.join(this.storagePath, 'claims.json'));
63
+ const claims = new Map();
64
+ const stealable = new Map();
65
+ for (const claim of Object.values(data.claims)) {
66
+ claim.claimedAt = new Date(claim.claimedAt);
67
+ claim.statusChangedAt = new Date(claim.statusChangedAt);
68
+ if (claim.expiresAt)
69
+ claim.expiresAt = new Date(claim.expiresAt);
70
+ claims.set(claim.issueId, claim);
71
+ }
72
+ for (const [id, info] of Object.entries(data.stealable)) {
73
+ stealable.set(id, { ...info, stealableAt: new Date(info.stealableAt) });
76
74
  }
75
+ this.claims = claims;
76
+ this.stealableInfo = stealable;
77
77
  }
78
78
  async saveClaims() {
79
79
  const claimsFile = path.join(this.storagePath, 'claims.json');
80
80
  const data = {
81
- claims: Array.from(this.claims.values()),
81
+ ...readClaimsStore(claimsFile),
82
+ claims: Object.fromEntries(this.claims),
83
+ stealable: Object.fromEntries(this.stealableInfo),
82
84
  savedAt: new Date().toISOString(),
83
85
  };
84
86
  fs.writeFileSync(claimsFile, JSON.stringify(data, null, 2));
@@ -87,6 +89,7 @@ export class ClaimService extends EventEmitter {
87
89
  // Core Claiming
88
90
  // ==========================================================================
89
91
  async claim(issueId, claimant) {
92
+ this.loadClaims();
90
93
  // Check if already claimed
91
94
  const existing = this.claims.get(issueId);
92
95
  if (existing && existing.status !== 'stealable') {
@@ -116,6 +119,7 @@ export class ClaimService extends EventEmitter {
116
119
  return { success: true, claim };
117
120
  }
118
121
  async release(issueId, claimant) {
122
+ this.loadClaims();
119
123
  const claim = this.claims.get(issueId);
120
124
  if (!claim) {
121
125
  throw new Error(`Issue ${issueId} is not claimed`);
@@ -137,6 +141,7 @@ export class ClaimService extends EventEmitter {
137
141
  // Handoffs
138
142
  // ==========================================================================
139
143
  async requestHandoff(issueId, from, to, reason) {
144
+ this.loadClaims();
140
145
  const claim = this.claims.get(issueId);
141
146
  if (!claim) {
142
147
  throw new Error(`Issue ${issueId} is not claimed`);
@@ -158,6 +163,7 @@ export class ClaimService extends EventEmitter {
158
163
  });
159
164
  }
160
165
  async acceptHandoff(issueId, claimant) {
166
+ this.loadClaims();
161
167
  const claim = this.claims.get(issueId);
162
168
  if (!claim || claim.status !== 'handoff-pending') {
163
169
  throw new Error(`No pending handoff for issue ${issueId}`);
@@ -181,6 +187,7 @@ export class ClaimService extends EventEmitter {
181
187
  });
182
188
  }
183
189
  async rejectHandoff(issueId, claimant, reason) {
190
+ this.loadClaims();
184
191
  const claim = this.claims.get(issueId);
185
192
  if (!claim || claim.status !== 'handoff-pending') {
186
193
  throw new Error(`No pending handoff for issue ${issueId}`);
@@ -205,6 +212,7 @@ export class ClaimService extends EventEmitter {
205
212
  // Status Updates
206
213
  // ==========================================================================
207
214
  async updateStatus(issueId, status, note) {
215
+ this.loadClaims();
208
216
  const claim = this.claims.get(issueId);
209
217
  if (!claim) {
210
218
  throw new Error(`Issue ${issueId} is not claimed`);
@@ -227,6 +235,7 @@ export class ClaimService extends EventEmitter {
227
235
  });
228
236
  }
229
237
  async updateProgress(issueId, progress) {
238
+ this.loadClaims();
230
239
  const claim = this.claims.get(issueId);
231
240
  if (!claim) {
232
241
  throw new Error(`Issue ${issueId} is not claimed`);
@@ -235,6 +244,7 @@ export class ClaimService extends EventEmitter {
235
244
  await this.saveClaims();
236
245
  }
237
246
  async requestReview(issueId, reviewers) {
247
+ this.loadClaims();
238
248
  const claim = this.claims.get(issueId);
239
249
  if (!claim) {
240
250
  throw new Error(`Issue ${issueId} is not claimed`);
@@ -254,6 +264,7 @@ export class ClaimService extends EventEmitter {
254
264
  // Work Stealing
255
265
  // ==========================================================================
256
266
  async markStealable(issueId, info) {
267
+ this.loadClaims();
257
268
  const claim = this.claims.get(issueId);
258
269
  if (!claim) {
259
270
  throw new Error(`Issue ${issueId} is not claimed`);
@@ -273,6 +284,7 @@ export class ClaimService extends EventEmitter {
273
284
  });
274
285
  }
275
286
  async steal(issueId, stealer) {
287
+ this.loadClaims();
276
288
  const claim = this.claims.get(issueId);
277
289
  if (!claim) {
278
290
  return { success: false, error: `Issue ${issueId} is not claimed` };
@@ -309,6 +321,7 @@ export class ClaimService extends EventEmitter {
309
321
  return { success: true, claim, previousOwner, context: info };
310
322
  }
311
323
  async getStealable(agentType) {
324
+ this.loadClaims();
312
325
  const stealable = [];
313
326
  for (const claim of this.claims.values()) {
314
327
  if (claim.status !== 'stealable')
@@ -323,6 +336,7 @@ export class ClaimService extends EventEmitter {
323
336
  return stealable;
324
337
  }
325
338
  async contestSteal(issueId, originalClaimant, reason) {
339
+ this.loadClaims();
326
340
  const claim = this.claims.get(issueId);
327
341
  if (!claim) {
328
342
  throw new Error(`Issue ${issueId} is not claimed`);
@@ -340,6 +354,7 @@ export class ClaimService extends EventEmitter {
340
354
  // Load Balancing
341
355
  // ==========================================================================
342
356
  async getAgentLoad(agentId) {
357
+ this.loadClaims();
343
358
  const claims = [];
344
359
  let blockedCount = 0;
345
360
  for (const claim of this.claims.values()) {
@@ -362,6 +377,7 @@ export class ClaimService extends EventEmitter {
362
377
  };
363
378
  }
364
379
  async rebalance(swarmId) {
380
+ this.loadClaims();
365
381
  const result = { moved: [], suggested: [] };
366
382
  // Get all agent loads
367
383
  const agentLoads = new Map();
@@ -410,6 +426,7 @@ export class ClaimService extends EventEmitter {
410
426
  // Queries
411
427
  // ==========================================================================
412
428
  async getClaimedBy(claimant) {
429
+ this.loadClaims();
413
430
  return Array.from(this.claims.values()).filter(c => this.isSameClaimant(c.claimant, claimant));
414
431
  }
415
432
  async getAvailableIssues(_filters) {
@@ -418,18 +435,22 @@ export class ClaimService extends EventEmitter {
418
435
  return [];
419
436
  }
420
437
  async getIssueStatus(issueId) {
438
+ this.loadClaims();
421
439
  return this.claims.get(issueId) || null;
422
440
  }
423
441
  async getAllClaims() {
442
+ this.loadClaims();
424
443
  return Array.from(this.claims.values());
425
444
  }
426
445
  async getByStatus(status) {
446
+ this.loadClaims();
427
447
  return Array.from(this.claims.values()).filter(c => c.status === status);
428
448
  }
429
449
  // ==========================================================================
430
450
  // Auto-Management
431
451
  // ==========================================================================
432
452
  async expireStale(maxAgeMinutes) {
453
+ this.loadClaims();
433
454
  const threshold = maxAgeMinutes ?? this.config.staleThresholdMinutes;
434
455
  const now = Date.now();
435
456
  const expired = [];
@@ -445,7 +466,7 @@ export class ClaimService extends EventEmitter {
445
466
  progress: claim.progress,
446
467
  context: `Stale: No activity for ${Math.round(age)} minutes`,
447
468
  });
448
- expired.push(claim);
469
+ expired.push(this.claims.get(claim.issueId));
449
470
  }
450
471
  }
451
472
  return expired;
@@ -0,0 +1,3 @@
1
+ /** Read legacy CLI arrays and MCP dictionaries; never replace unreadable state. */
2
+ export declare function readClaimsStore(file: string): Record<string, unknown>;
3
+ //# sourceMappingURL=claims-store.d.ts.map
@@ -0,0 +1,40 @@
1
+ /** Shared on-disk shape for the CLI claim service and public MCP claim tools. */
2
+ import { readFileSync } from 'node:fs';
3
+ function record(value) {
4
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
5
+ }
6
+ /** Read legacy CLI arrays and MCP dictionaries; never replace unreadable state. */
7
+ export function readClaimsStore(file) {
8
+ let text;
9
+ try {
10
+ text = readFileSync(file, 'utf8');
11
+ }
12
+ catch (error) {
13
+ if (error.code === 'ENOENT')
14
+ return { claims: {}, stealable: {}, contests: {} };
15
+ throw error;
16
+ }
17
+ const data = JSON.parse(text);
18
+ if (!record(data) || (!Array.isArray(data.claims) && !record(data.claims))) {
19
+ throw new Error('Invalid claims store: expected a claims array or object');
20
+ }
21
+ const entries = Array.isArray(data.claims)
22
+ ? data.claims.map((claim) => [record(claim) ? claim.issueId : undefined, claim])
23
+ : Object.entries(data.claims);
24
+ const claims = Object.create(null);
25
+ for (const [id, claim] of entries) {
26
+ if (typeof id !== 'string' || !id || !record(claim) || claim.issueId !== id ||
27
+ !record(claim.claimant) || typeof claim.status !== 'string' ||
28
+ typeof claim.claimedAt !== 'string' || typeof claim.statusChangedAt !== 'string' ||
29
+ Object.hasOwn(claims, id)) {
30
+ throw new Error('Invalid claims store: malformed or duplicate claim');
31
+ }
32
+ claims[id] = claim;
33
+ }
34
+ for (const key of ['stealable', 'contests']) {
35
+ if (data[key] !== undefined && !record(data[key]))
36
+ throw new Error(`Invalid claims store: ${key} must be an object`);
37
+ }
38
+ return { ...data, claims, stealable: data.stealable ?? {}, contests: data.contests ?? {} };
39
+ }
40
+ //# sourceMappingURL=claims-store.js.map
@@ -43,6 +43,7 @@ import * as crypto from 'crypto';
43
43
  import { runDistillation } from './memory-distillation.js';
44
44
  import { distillTrajectoryContent } from '../memory/structured-distill.js';
45
45
  import { cosineSim } from '../memory/hybrid-retrieval.js';
46
+ import { loadBetterSqlite3 } from '../memory/shared-sqlite.js';
46
47
  // ── Grid defaults (requirement 1) ───────────────────────────────────────
47
48
  export const DEFAULT_GRID_BATCH_SIZE = [100, 200, 500];
48
49
  export const DEFAULT_GRID_DEDUP_DISTANCE = [0.05, 0.1, 0.15, 0.2, 0.3];
@@ -71,7 +72,22 @@ export async function tuneDistillation(options) {
71
72
  if (!dbPath || !fs.existsSync(dbPath)) {
72
73
  throw new Error(`tuneDistillation: source db not found at ${dbPath}`);
73
74
  }
74
- const Database = await loadBetterSqlite3();
75
+ // #3883 — resolve the SAME better-sqlite3 copy AgentDB resolves (shared-sqlite.ts),
76
+ // not a separately-imported copy. Verified: every `new Database(...)` call in this
77
+ // module (readCopy below, and tmpCopy inside evaluateCandidate) opens a temp file
78
+ // copy made via fs.copyFileSync — `dbPath`/`sourceDbPath` themselves are NEVER
79
+ // opened with a DB connection anywhere in this module (see module header
80
+ // "Isolation" section). So the #3693 identity hazard (two better-sqlite3 copies
81
+ // fighting over one live file's -wal/-shm sidecars) does not apply to today's
82
+ // call sites here — AgentDB never holds a live handle on a temp copy. This swap
83
+ // is for consistency with the shared loader's contract, not an active fix.
84
+ let Database;
85
+ try {
86
+ Database = await loadBetterSqlite3();
87
+ }
88
+ catch {
89
+ Database = null;
90
+ }
75
91
  if (!Database) {
76
92
  throw new Error('tuneDistillation: better-sqlite3 unavailable — cannot run the tuning harness');
77
93
  }
@@ -498,13 +514,4 @@ function safeUnlink(p) {
498
514
  }
499
515
  }
500
516
  }
501
- async function loadBetterSqlite3() {
502
- try {
503
- const mod = 'better-sqlite3';
504
- return (await import(mod)).default;
505
- }
506
- catch {
507
- return null;
508
- }
509
- }
510
517
  //# sourceMappingURL=distill-tuning.js.map
@@ -20,6 +20,13 @@ export interface BackupResult {
20
20
  skipped?: string;
21
21
  }
22
22
  export declare function defaultMemoryDbPath(cwd?: string): string;
23
+ export interface ProjectBackupResult extends BackupResult {
24
+ backups: Array<BackupResult & {
25
+ dbPath: string;
26
+ }>;
27
+ }
28
+ /** Back up the active project stores; an explicit full-file override stays single-store. */
29
+ export declare function backupProjectMemoryDbs(cwd: string, opts?: Omit<BackupOptions, 'dbPath'>): Promise<ProjectBackupResult>;
23
30
  export declare function backupMemoryDb(opts?: BackupOptions): Promise<BackupResult>;
24
31
  export interface RestoreResult {
25
32
  restored: boolean;
@@ -15,9 +15,37 @@
15
15
  import * as fs from 'fs';
16
16
  import * as path from 'path';
17
17
  import { readFileMaybeEncrypted } from '../fs-secure.js';
18
+ import { loadBetterSqlite3 } from '../memory/shared-sqlite.js';
19
+ import { resolveMemoryRoot } from '../memory/memory-root.js';
18
20
  export function defaultMemoryDbPath(cwd = process.cwd()) {
19
21
  return path.join(cwd, '.swarm', 'memory.db');
20
22
  }
23
+ /** Back up the active project stores; an explicit full-file override stays single-store. */
24
+ export async function backupProjectMemoryDbs(cwd, opts = {}) {
25
+ const override = process.env.CLAUDE_FLOW_DB_PATH;
26
+ let sources;
27
+ if (override && override.trim().length > 0) {
28
+ sources = [path.resolve(cwd, override)];
29
+ }
30
+ else {
31
+ const root = resolveMemoryRoot(cwd);
32
+ sources = [path.join(root, 'memory.db'), path.join(root, 'agentdb-memory.db')];
33
+ }
34
+ const backups = [];
35
+ for (const dbPath of sources) {
36
+ backups.push({ ...await backupMemoryDb({ ...opts, dbPath }), dbPath });
37
+ }
38
+ const successful = backups.filter(result => result.backedUp);
39
+ const failed = backups.find(result => !result.backedUp && result.skipped !== 'no-db');
40
+ return {
41
+ ...successful[0],
42
+ backedUp: successful.length > 0 && !failed,
43
+ sizeBytes: successful.reduce((sum, result) => sum + (result.sizeBytes ?? 0), 0),
44
+ rotatedAway: successful.flatMap(result => result.rotatedAway ?? []),
45
+ skipped: failed?.skipped ?? (successful.length === 0 ? 'no-db' : undefined),
46
+ backups,
47
+ };
48
+ }
21
49
  /** ISO timestamp safe for filenames (no ':' or '.'). */
22
50
  function fileStamp(ms) {
23
51
  return new Date(ms).toISOString().replace(/[:.]/g, '-');
@@ -228,8 +256,7 @@ export async function restoreMemoryDbFromBackup(dbPath, opts = {}) {
228
256
  // encrypted snapshots and provides the same checks on WASM-only hosts.
229
257
  let Database = null;
230
258
  try {
231
- const mod = 'better-sqlite3';
232
- Database = (await import(mod)).default;
259
+ Database = await loadBetterSqlite3();
233
260
  }
234
261
  catch {
235
262
  /* use the sql.js verifier */
@@ -27,6 +27,7 @@
27
27
  import * as fs from 'fs';
28
28
  import * as path from 'path';
29
29
  import { distillTrajectoryContent, serialiseDistilled } from '../memory/structured-distill.js';
30
+ import { loadBetterSqlite3 } from '../memory/shared-sqlite.js';
30
31
  function emptyReport(dryRun, extra = {}) {
31
32
  return {
32
33
  processed: 0, episodes: 0, episodeEmbeddings: 0,
@@ -104,8 +105,7 @@ export async function runDistillation(options) {
104
105
  return emptyReport(dryRun, { skipped: 'no-db' });
105
106
  let Database;
106
107
  try {
107
- const mod = 'better-sqlite3';
108
- Database = (await import(mod)).default;
108
+ Database = await loadBetterSqlite3();
109
109
  }
110
110
  catch {
111
111
  return emptyReport(dryRun, { skipped: 'better-sqlite3 unavailable' });
@@ -69,6 +69,8 @@ export interface DaemonConfig {
69
69
  workers: WorkerConfig[];
70
70
  enabledWorkers?: string[];
71
71
  }
72
+ /** Parse only worker types the daemon can actually schedule. */
73
+ export declare function parseEnabledWorkers(value: string): WorkerType[];
72
74
  /**
73
75
  * Worker Daemon - Manages background workers with Node.js
74
76
  */
@@ -368,8 +370,8 @@ export declare class WorkerDaemon extends EventEmitter {
368
370
  */
369
371
  private runConsolidateWorker;
370
372
  /**
371
- * Nightly memory-DB backup worker (24h interval). Takes a WAL-safe, consistent
372
- * snapshot of .swarm/memory.db with rotation (keep last N). Never throws — a
373
+ * Nightly memory-DB backup worker (24h interval). Takes WAL-safe, consistent
374
+ * snapshots of the active CLI and AgentDB stores with rotation (keep last N). Never throws — a
373
375
  * worker must not crash the daemon; a skip/error is written to the metrics
374
376
  * file. Opt-out by omitting `backup` from `-w`; offsite GCS is opt-in via
375
377
  * RUFLO_BACKUP_GCS (a gs:// prefix), retention via RUFLO_BACKUP_KEEP.
@@ -24,7 +24,7 @@ import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-exe
24
24
  // incremental (rowid cursor), non-destructive, transactional, and
25
25
  // quick_check-gated, so it's safe to call unconditionally on every tick.
26
26
  import { runDistillation, defaultMemoryDbPath } from './memory-distillation.js';
27
- import { backupMemoryDb } from './memory-backup.js';
27
+ import { backupProjectMemoryDbs } from './memory-backup.js';
28
28
  import { resolveGitWorkspaceIdentity } from './git-workspace-identity.js';
29
29
  import { getWorkspaceLeaseRegistry } from './workspace-lease.js';
30
30
  import { getRepoSupervisorRegistry } from './repo-supervisor.js';
@@ -43,6 +43,14 @@ const DEFAULT_WORKERS = [
43
43
  { type: 'predict', intervalMs: 10 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Predictive preloading', enabled: false },
44
44
  { type: 'document', intervalMs: 60 * 60 * 1000, offsetMs: 0, priority: 'low', description: 'Auto-documentation', enabled: false },
45
45
  ];
46
+ /** Parse only worker types the daemon can actually schedule. */
47
+ export function parseEnabledWorkers(value) {
48
+ const names = value.split(',');
49
+ if (!names.length || names.some(name => !DEFAULT_WORKERS.some(worker => worker.type === name))) {
50
+ throw new Error(`--workers must be a comma-separated list of: ${DEFAULT_WORKERS.map(worker => worker.type).join(',')}`);
51
+ }
52
+ return [...new Set(names)];
53
+ }
46
54
  // Worker timeout — must exceed the longest per-worker headless timeout (15 min for audit/refactor).
47
55
  // Previously 5 min, which caused orphan processes when daemon timeout fired before executor timeout (#1117).
48
56
  const DEFAULT_WORKER_TIMEOUT_MS = 16 * 60 * 1000;
@@ -192,7 +200,7 @@ export class WorkerDaemon extends EventEmitter {
192
200
  aiWorkersEnabled: config?.aiWorkersEnabled
193
201
  ?? fileConfig.aiWorkersEnabled
194
202
  ?? (process.env.RUFLO_DAEMON_AI_WORKERS === '1'),
195
- workers: config?.workers ?? DEFAULT_WORKERS,
203
+ workers: (config?.workers ?? DEFAULT_WORKERS).map(worker => ({ ...worker })),
196
204
  enabledWorkers: config?.enabledWorkers,
197
205
  };
198
206
  // #2935 — deferred from readDaemonConfigFromFile(): this.config (and
@@ -1627,8 +1635,8 @@ export class WorkerDaemon extends EventEmitter {
1627
1635
  return result;
1628
1636
  }
1629
1637
  /**
1630
- * Nightly memory-DB backup worker (24h interval). Takes a WAL-safe, consistent
1631
- * snapshot of .swarm/memory.db with rotation (keep last N). Never throws — a
1638
+ * Nightly memory-DB backup worker (24h interval). Takes WAL-safe, consistent
1639
+ * snapshots of the active CLI and AgentDB stores with rotation (keep last N). Never throws — a
1632
1640
  * worker must not crash the daemon; a skip/error is written to the metrics
1633
1641
  * file. Opt-out by omitting `backup` from `-w`; offsite GCS is opt-in via
1634
1642
  * RUFLO_BACKUP_GCS (a gs:// prefix), retention via RUFLO_BACKUP_KEEP.
@@ -1641,14 +1649,14 @@ export class WorkerDaemon extends EventEmitter {
1641
1649
  let result;
1642
1650
  try {
1643
1651
  const keepEnv = Number(process.env.RUFLO_BACKUP_KEEP);
1644
- const r = await backupMemoryDb({
1645
- dbPath: defaultMemoryDbPath(this.projectRoot),
1652
+ const r = await backupProjectMemoryDbs(this.projectRoot, {
1646
1653
  keep: Number.isFinite(keepEnv) && keepEnv > 0 ? keepEnv : 7,
1647
1654
  gcs: process.env.RUFLO_BACKUP_GCS || undefined,
1648
1655
  });
1649
1656
  result = {
1650
1657
  timestamp: new Date().toISOString(),
1651
1658
  backedUp: r.backedUp,
1659
+ backups: r.backups,
1652
1660
  path: r.path,
1653
1661
  sizeBytes: r.sizeBytes ?? 0,
1654
1662
  rotatedAway: r.rotatedAway?.length ?? 0,
File without changes
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Ephemeral-port localhost callback server for the browser-based OAuth flow
3
- * (ADR-306). A minimal single-request HTTP server — deliberately not a full
3
+ * (ADR-306). A minimal single-callback HTTP server — deliberately not a full
4
4
  * framework for one GET route that lives for one request and then shuts
5
5
  * down. A TypeScript port of meta-proxy's `src/oauth/callback_server.rs`.
6
6
  *
@@ -18,6 +18,7 @@ export declare class CallbackServer {
18
18
  private readonly server;
19
19
  readonly redirectUri: string;
20
20
  readonly port: number;
21
+ private readonly callback;
21
22
  private constructor();
22
23
  /**
23
24
  * Binds `127.0.0.1:0` (OS-assigned ephemeral port) and returns the server
@@ -1 +1 @@
1
- {"version":3,"file":"callback-server.d.ts","sourceRoot":"","sources":["../../src/oauth/callback-server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAeD,qBAAa,oBAAqB,SAAQ,KAAK;;CAK9C;AAED,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,MAAM;aACP,WAAW,EAAE,MAAM;aACnB,IAAI,EAAE,MAAM;IAH9B,OAAO;IAMP;;;;;OAKG;WACU,IAAI,IAAI,OAAO,CAAC,cAAc,CAAC;IAa5C;;;;;OAKG;IACG,aAAa,CAAC,SAAS,SAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;CA0B7E"}
1
+ {"version":3,"file":"callback-server.d.ts","sourceRoot":"","sources":["../../src/oauth/callback-server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAeD,qBAAa,oBAAqB,SAAQ,KAAK;;CAK9C;AAED,qBAAa,cAAc;IAGvB,OAAO,CAAC,QAAQ,CAAC,MAAM;aACP,WAAW,EAAE,MAAM;aACnB,IAAI,EAAE,MAAM;IAJ9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0B;IACnD,OAAO;IAkDP;;;;;OAKG;WACU,IAAI,IAAI,OAAO,CAAC,cAAc,CAAC;IAa5C;;;;;OAKG;IACG,aAAa,CAAC,SAAS,SAAqB,GAAG,OAAO,CAAC,cAAc,CAAC;CAa7E"}
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Ephemeral-port localhost callback server for the browser-based OAuth flow
3
- * (ADR-306). A minimal single-request HTTP server — deliberately not a full
3
+ * (ADR-306). A minimal single-callback HTTP server — deliberately not a full
4
4
  * framework for one GET route that lives for one request and then shuts
5
5
  * down. A TypeScript port of meta-proxy's `src/oauth/callback_server.rs`.
6
6
  *
@@ -28,10 +28,55 @@ export class CallbackServer {
28
28
  server;
29
29
  redirectUri;
30
30
  port;
31
+ callback;
31
32
  constructor(server, redirectUri, port) {
32
33
  this.server = server;
33
34
  this.redirectUri = redirectUri;
34
35
  this.port = port;
36
+ // Install the listener before bind() returns: opening the browser can
37
+ // deliver a callback before the caller reaches awaitCallback().
38
+ this.callback = new Promise((resolve) => {
39
+ let received = false;
40
+ this.server.on('request', (req, res) => {
41
+ let url;
42
+ try {
43
+ url = new URL(req.url ?? '/', 'http://127.0.0.1');
44
+ }
45
+ catch {
46
+ res.writeHead(400, { connection: 'close' });
47
+ res.end();
48
+ return;
49
+ }
50
+ if (url.pathname !== '/oauth/callback') {
51
+ res.writeHead(404, { connection: 'close' });
52
+ res.end();
53
+ return;
54
+ }
55
+ if (req.method !== 'GET') {
56
+ res.writeHead(405, { allow: 'GET', connection: 'close' });
57
+ res.end();
58
+ return;
59
+ }
60
+ if (received) {
61
+ res.writeHead(409, { connection: 'close' });
62
+ res.end();
63
+ return;
64
+ }
65
+ received = true;
66
+ const result = {
67
+ code: url.searchParams.get('code'),
68
+ state: url.searchParams.get('state'),
69
+ error: url.searchParams.get('error'),
70
+ };
71
+ res.writeHead(200, {
72
+ 'content-type': 'text/html; charset=utf-8',
73
+ connection: 'close',
74
+ });
75
+ res.end(SUCCESS_PAGE);
76
+ this.server.close();
77
+ resolve(result);
78
+ });
79
+ });
35
80
  }
36
81
  /**
37
82
  * Binds `127.0.0.1:0` (OS-assigned ephemeral port) and returns the server
@@ -63,22 +108,9 @@ export class CallbackServer {
63
108
  this.server.close();
64
109
  reject(new CallbackTimeoutError());
65
110
  }, waitForMs);
66
- this.server.once('request', (req, res) => {
111
+ this.callback.then((result) => {
67
112
  clearTimeout(timer);
68
- const url = new URL(req.url ?? '/oauth/callback', 'http://127.0.0.1');
69
- const result = {
70
- code: url.searchParams.get('code'),
71
- state: url.searchParams.get('state'),
72
- error: url.searchParams.get('error'),
73
- };
74
- res.writeHead(200, {
75
- 'content-type': 'text/html; charset=utf-8',
76
- connection: 'close',
77
- });
78
- res.end(SUCCESS_PAGE, () => {
79
- this.server.close();
80
- resolve(result);
81
- });
113
+ resolve(result);
82
114
  });
83
115
  });
84
116
  }