@claude-flow/cli 3.71.0 → 3.71.2

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 (37) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/catalog-manifest.json +2 -2
  3. package/dist/src/commands/deployment.js +1 -1
  4. package/dist/src/commands/embeddings.js +6 -6
  5. package/dist/src/commands/init.js +4 -0
  6. package/dist/src/commands/ruvector/backup.js +1 -1
  7. package/dist/src/commands/ruvector/migrate.js +1 -1
  8. package/dist/src/commands/session.js +10 -7
  9. package/dist/src/init/executor.js +15 -5
  10. package/dist/src/init/types.d.ts +2 -0
  11. package/dist/src/init/types.js +4 -1
  12. package/dist/src/mcp-tools/claims-tools.js +7 -1
  13. package/dist/src/mcp-tools/daa-tools.js +22 -2
  14. package/dist/src/mcp-tools/embeddings-tools.js +2 -1
  15. package/dist/src/mcp-tools/memory-tools.js +8 -0
  16. package/dist/src/mcp-tools/session-tools.js +42 -4
  17. package/dist/src/mcp-tools/terminal-tools.js +5 -0
  18. package/dist/src/mcp-tools/workflow-tools.js +22 -3
  19. package/dist/src/memory/memory-bridge.d.ts +6 -0
  20. package/dist/src/memory/memory-bridge.js +98 -15
  21. package/dist/src/memory/memory-initializer.js +32 -10
  22. package/dist/src/services/claims-store.js +5 -2
  23. package/dist/src/services/ruvector-training.js +18 -5
  24. package/node_modules/@claude-flow/mcp/dist/server.d.ts.map +1 -1
  25. package/node_modules/@claude-flow/mcp/dist/server.js +6 -0
  26. package/node_modules/@claude-flow/mcp/dist/server.js.map +1 -1
  27. package/node_modules/@claude-flow/mcp/dist/session-manager.d.ts +6 -0
  28. package/node_modules/@claude-flow/mcp/dist/session-manager.d.ts.map +1 -1
  29. package/node_modules/@claude-flow/mcp/dist/session-manager.js +20 -0
  30. package/node_modules/@claude-flow/mcp/dist/session-manager.js.map +1 -1
  31. package/node_modules/@claude-flow/mcp/package.json +1 -1
  32. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.d.ts.map +1 -1
  33. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.js +3 -1
  34. package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.js.map +1 -1
  35. package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
  36. package/package.json +4 -4
  37. package/plugins/ruflo-console/dist/tui.mjs +51 -51
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": {
3
- "version": "3.71.0",
3
+ "version": "3.71.2",
4
4
  "files": {
5
5
  "auto-memory-hook.mjs": "631fac9d518835e2852a406888316bbbddddb49e7ed8e406d2e971f46833df9a",
6
6
  "hook-handler.cjs": "0ce70981aa33f76d008b6f49626effaf4269918c4094f1a28c1ef6fceb097d6f",
@@ -10,6 +10,6 @@
10
10
  "learning-loop.cjs": "d046cd6f8efb7dd40e1d2451516c181fc8b26477aefb1124c0df5f52a23f9190"
11
11
  }
12
12
  },
13
- "signature": "KB4MvG90TT/O/OQIfSmwOW+cxr0l/Q5pwbOl1H8hSsjF4pQM5WCxB7tjBN7fH6RFYrQqlaS+4wRTgxcaGtmuAg==",
13
+ "signature": "awy0GR0jee+KKCI+7BgzocA29zGZTAZcNYRAwp7IDQ6xwzc6Rby9hqhTnk+68AHde2YvpNpuO8AyqU/P9JDFBw==",
14
14
  "algorithm": "ed25519"
15
15
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 7,
4
- "generatedAt": "2026-11-01T20:14:57.317Z",
5
- "gitSha": "5f3dacc5",
4
+ "generatedAt": "2026-11-04T18:54:59.719Z",
5
+ "gitSha": "af95dd71",
6
6
  "catalog": {
7
7
  "agents": 177,
8
8
  "tools": 450,
@@ -79,7 +79,7 @@ const deployCommand = {
79
79
  action: async (ctx) => {
80
80
  try {
81
81
  const envName = String(ctx.flags['env'] || 'staging');
82
- const dryRun = Boolean(ctx.flags['dry-run']);
82
+ const dryRun = ctx.flags.dryRun === true || ctx.flags['dry-run'] === true;
83
83
  const description = ctx.flags['description'] ? String(ctx.flags['description']) : undefined;
84
84
  let version = ctx.flags['version'] ? String(ctx.flags['version']) : null;
85
85
  if (!version) {
@@ -121,7 +121,7 @@ const searchCommand = {
121
121
  const threshold = rawThreshold === undefined || rawThreshold === null
122
122
  ? 0.5
123
123
  : parseFloat(String(rawThreshold));
124
- const dbPath = ctx.flags['db-path'] || '.swarm/memory.db';
124
+ const dbPath = ctx.flags.dbPath || '.swarm/memory.db';
125
125
  if (!query) {
126
126
  output.printError('Query is required');
127
127
  return { success: false, exitCode: 1 };
@@ -394,7 +394,7 @@ const collectionsCommand = {
394
394
  ],
395
395
  action: async (ctx) => {
396
396
  const action = ctx.flags.action || 'list';
397
- const dbPath = ctx.flags['db-path'] || '.swarm/memory.db';
397
+ const dbPath = ctx.flags.dbPath || '.swarm/memory.db';
398
398
  output.writeln();
399
399
  output.writeln(output.bold('Embedding Collections (Namespaces)'));
400
400
  output.writeln(output.dim('─'.repeat(60)));
@@ -500,7 +500,7 @@ const indexCommand = {
500
500
  action: async (ctx) => {
501
501
  const action = ctx.flags.action || 'status';
502
502
  const collection = ctx.flags.collection;
503
- const efConstruction = parseInt(ctx.flags['ef-construction'] || '200', 10);
503
+ const efConstruction = parseInt(ctx.flags.efConstruction || '200', 10);
504
504
  const m = parseInt(ctx.flags.m || '16', 10);
505
505
  output.writeln();
506
506
  output.writeln(output.bold(`HNSW Index: ${action}`));
@@ -850,7 +850,7 @@ const chunkCommand = {
850
850
  action: async (ctx) => {
851
851
  const embeddings = await getEmbeddings();
852
852
  const text = ctx.flags.text || '';
853
- const maxSize = parseInt(ctx.flags['max-size'] || '512', 10);
853
+ const maxSize = parseInt(ctx.flags.maxSize || '512', 10);
854
854
  const overlap = parseInt(ctx.flags.overlap || '50', 10);
855
855
  const strategy = ctx.flags.strategy || 'sentence';
856
856
  output.writeln();
@@ -1275,7 +1275,7 @@ const cacheCommand = {
1275
1275
  ],
1276
1276
  action: async (ctx) => {
1277
1277
  const action = ctx.flags.action || 'stats';
1278
- const dbPath = ctx.flags['db-path'] || '.cache/embeddings.db';
1278
+ const dbPath = ctx.flags.dbPath || '.cache/embeddings.db';
1279
1279
  output.writeln();
1280
1280
  output.writeln(output.bold('Embedding Cache'));
1281
1281
  output.writeln(output.dim('─'.repeat(50)));
@@ -1479,7 +1479,7 @@ const benchmarkCommand = {
1479
1479
  ],
1480
1480
  action: async (ctx) => {
1481
1481
  const iterations = parseInt(ctx.flags.iterations || '10', 10);
1482
- const batchSize = parseInt(ctx.flags['batch-size'] || '5', 10);
1482
+ const batchSize = parseInt(ctx.flags.batchSize || '5', 10);
1483
1483
  const full = ctx.flags.full === true;
1484
1484
  output.writeln();
1485
1485
  output.writeln(output.bold('Embedding Performance Benchmark'));
@@ -1091,6 +1091,7 @@ const wizardCommand = {
1091
1091
  { value: 'github', label: 'GitHub', hint: 'GitHub integration skills', selected: true },
1092
1092
  { value: 'flowNexus', label: 'Flow Nexus', hint: 'Cloud platform skills', selected: false },
1093
1093
  { value: 'v3', label: 'V3', hint: 'V3 implementation skills', selected: true },
1094
+ { value: 'legacy', label: 'Legacy', hint: 'Skills that document commands v3 does not ship', selected: false },
1094
1095
  ],
1095
1096
  });
1096
1097
  options.skills.core = skillSets.includes('core');
@@ -1098,6 +1099,7 @@ const wizardCommand = {
1098
1099
  options.skills.github = skillSets.includes('github');
1099
1100
  options.skills.flowNexus = skillSets.includes('flowNexus');
1100
1101
  options.skills.v3 = skillSets.includes('v3');
1102
+ options.skills.legacy = skillSets.includes('legacy');
1101
1103
  }
1102
1104
  // Hooks selection
1103
1105
  if (options.components.settings) {
@@ -1354,6 +1356,7 @@ const skillsCommand = {
1354
1356
  { name: 'agentdb', description: 'Install AgentDB skills', type: 'boolean', default: false },
1355
1357
  { name: 'github', description: 'Install GitHub skills', type: 'boolean', default: false },
1356
1358
  { name: 'v3', description: 'Install V3 skills', type: 'boolean', default: false },
1359
+ { name: 'legacy', description: 'Install skills that document commands v3 does not ship (stream-chain, sparc)', type: 'boolean', default: false },
1357
1360
  ],
1358
1361
  action: async (ctx) => {
1359
1362
  const options = {
@@ -1380,6 +1383,7 @@ const skillsCommand = {
1380
1383
  browser: false,
1381
1384
  v3: ctx.flags.v3,
1382
1385
  dualMode: false,
1386
+ legacy: ctx.flags.legacy,
1383
1387
  },
1384
1388
  };
1385
1389
  const spinner = output.createSpinner({ text: 'Installing skills...' });
@@ -437,7 +437,7 @@ const restoreSubcommand = {
437
437
  const config = getConnectionConfig(ctx);
438
438
  const inputPath = ctx.flags.input;
439
439
  const clean = ctx.flags.clean;
440
- const dryRun = ctx.flags['dry-run'];
440
+ const dryRun = ctx.flags.dryRun === true || ctx.flags['dry-run'] === true;
441
441
  output.writeln();
442
442
  output.writeln(output.bold('RuVector Restore'));
443
443
  output.writeln(output.dim('='.repeat(60)));
@@ -246,7 +246,7 @@ export const migrateCommand = {
246
246
  ],
247
247
  action: async (ctx) => {
248
248
  const config = getConnectionConfig(ctx);
249
- const dryRun = ctx.flags['dry-run'];
249
+ const dryRun = ctx.flags.dryRun === true || ctx.flags['dry-run'] === true;
250
250
  const force = ctx.flags.force;
251
251
  const targetVersion = ctx.flags.to;
252
252
  const rollback = ctx.flags.down;
@@ -211,9 +211,9 @@ const saveCommand = {
211
211
  const result = await callMCPTool('session_save', {
212
212
  name: sessionName,
213
213
  description,
214
- includeMemory: ctx.flags['include-memory'] !== false,
215
- includeAgents: ctx.flags['include-agents'] !== false,
216
- includeTasks: ctx.flags['include-tasks'] !== false
214
+ includeMemory: ctx.flags.includeMemory !== false && ctx.flags['include-memory'] !== false,
215
+ includeAgents: ctx.flags.includeAgents !== false && ctx.flags['include-agents'] !== false,
216
+ includeTasks: ctx.flags.includeTasks !== false && ctx.flags['include-tasks'] !== false
217
217
  });
218
218
  const failure = toolError(result);
219
219
  if (failure) {
@@ -342,9 +342,12 @@ const restoreCommand = {
342
342
  spinner.start();
343
343
  try {
344
344
  // Determine what to restore
345
- const restoreMemory = !ctx.flags['agents-only'] && !ctx.flags['tasks-only'];
346
- const restoreAgents = !ctx.flags['memory-only'] && !ctx.flags['tasks-only'];
347
- const restoreTasks = !ctx.flags['memory-only'] && !ctx.flags['agents-only'];
345
+ const memoryOnly = ctx.flags.memoryOnly === true || ctx.flags['memory-only'] === true;
346
+ const agentsOnly = ctx.flags.agentsOnly === true || ctx.flags['agents-only'] === true;
347
+ const tasksOnly = ctx.flags.tasksOnly === true || ctx.flags['tasks-only'] === true;
348
+ const restoreMemory = !agentsOnly && !tasksOnly;
349
+ const restoreAgents = !memoryOnly && !tasksOnly;
350
+ const restoreTasks = !memoryOnly && !agentsOnly;
348
351
  const result = await callMCPTool('session_restore', {
349
352
  sessionId,
350
353
  restoreMemory,
@@ -552,7 +555,7 @@ const exportCommand = {
552
555
  try {
553
556
  const result = await callMCPTool('session_export', {
554
557
  sessionId,
555
- includeMemory: ctx.flags['include-memory'] !== false
558
+ includeMemory: ctx.flags.includeMemory !== false && ctx.flags['include-memory'] !== false
556
559
  });
557
560
  const failure = toolError(result);
558
561
  if (failure || result.data === undefined) {
@@ -29,11 +29,9 @@ const SKILLS_MAP = {
29
29
  core: [
30
30
  'swarm-orchestration',
31
31
  'swarm-advanced',
32
- 'sparc-methodology',
33
32
  'hooks-automation',
34
33
  'pair-programming',
35
34
  'verification-quality',
36
- 'stream-chain',
37
35
  'skill-builder',
38
36
  ],
39
37
  browser: ['browser'], // agent-browser integration
@@ -47,12 +45,11 @@ const SKILLS_MAP = {
47
45
  'reasoningbank-agentdb',
48
46
  'reasoningbank-intelligence',
49
47
  ],
48
+ // github-code-review and github-project-management only use hooks, which v3
49
+ // ships; the other three drive `ruflo github`, so they live in skills.legacy.
50
50
  github: [
51
51
  'github-code-review',
52
- 'github-multi-repo',
53
52
  'github-project-management',
54
- 'github-release-management',
55
- 'github-workflow-automation',
56
53
  ],
57
54
  flowNexus: [
58
55
  'flow-nexus-neural',
@@ -70,6 +67,17 @@ const SKILLS_MAP = {
70
67
  'v3-security-overhaul',
71
68
  'v3-swarm-coordination',
72
69
  ],
70
+ // ADR-128 / #4539: skills that document v2-era verbs v3 does not ship
71
+ // (`stream-chain`, `sparc`, `github`). Their examples fail with `Unknown
72
+ // command`, so they are opt-in like skills.v3 instead of being installed by
73
+ // default.
74
+ legacy: [
75
+ 'github-multi-repo',
76
+ 'github-release-management',
77
+ 'github-workflow-automation',
78
+ 'sparc-methodology',
79
+ 'stream-chain',
80
+ ],
73
81
  };
74
82
  /**
75
83
  * Commands to copy based on configuration
@@ -1077,6 +1085,8 @@ async function copySkills(targetDir, options, result) {
1077
1085
  skillsToCopy.push(...SKILLS_MAP.v3);
1078
1086
  if (skillsConfig.dualMode)
1079
1087
  skillsToCopy.push(...SKILLS_MAP.dualMode);
1088
+ if (skillsConfig.legacy)
1089
+ skillsToCopy.push(...SKILLS_MAP.legacy);
1080
1090
  }
1081
1091
  // Find source skills directory
1082
1092
  const sourceSkillsDir = findSourceDir('skills', options.sourceBaseDir);
@@ -76,6 +76,8 @@ export interface SkillsConfig {
76
76
  v3: boolean;
77
77
  /** Include dual-mode skills (Claude Code + Codex hybrid) */
78
78
  dualMode: boolean;
79
+ /** Include legacy skills that document commands v3 does not ship (opt-in) */
80
+ legacy: boolean;
79
81
  /** Include all available skills */
80
82
  all: boolean;
81
83
  }
@@ -73,11 +73,12 @@ export const DEFAULT_INIT_OPTIONS = {
73
73
  skills: {
74
74
  core: true,
75
75
  agentdb: true,
76
- github: true,
76
+ github: true, // #4539: only the hook-only GitHub skills remain; the rest are skills.legacy
77
77
  flowNexus: false,
78
78
  browser: true,
79
79
  v3: false, // ADR-128: opt-in like agents.v3; use --full or `init skills --v3`
80
80
  dualMode: false, // Optional: enable with --dual flag
81
+ legacy: false, // #4539: stream-chain/sparc document verbs v3 does not ship
81
82
  all: false,
82
83
  },
83
84
  commands: {
@@ -186,6 +187,7 @@ export const MINIMAL_INIT_OPTIONS = {
186
187
  browser: false,
187
188
  v3: false,
188
189
  dualMode: false,
190
+ legacy: false,
189
191
  all: false,
190
192
  },
191
193
  agents: {
@@ -246,6 +248,7 @@ export const FULL_INIT_OPTIONS = {
246
248
  browser: true,
247
249
  v3: true,
248
250
  dualMode: true, // Include in full init
251
+ legacy: true, // Include in full init
249
252
  all: true,
250
253
  },
251
254
  commands: {
@@ -402,6 +402,10 @@ export const claimsTools = [
402
402
  if (progress !== undefined) {
403
403
  claim.progress = Math.min(100, Math.max(0, progress));
404
404
  }
405
+ // claims_status never sets 'stealable', so a claim it updates is no
406
+ // longer up for grabs: drop it from the list claims_stealable and
407
+ // claims_steal read.
408
+ delete store.stealable[issueId];
405
409
  store.claims[issueId] = claim;
406
410
  saveClaims(store);
407
411
  return {
@@ -592,7 +596,9 @@ export const claimsTools = [
592
596
  claim.claimant = stealer;
593
597
  claim.status = 'active';
594
598
  claim.statusChangedAt = now;
595
- claim.context = stealableInfo.context;
599
+ // The context given to claims_mark-stealable is the handoff note for the
600
+ // stealer; without one, keep the context recorded with the claim.
601
+ claim.context = stealableInfo.context ?? claim.context;
596
602
  delete store.stealable[issueId];
597
603
  store.claims[issueId] = claim;
598
604
  saveClaims(store);
@@ -28,17 +28,37 @@ function ensureDAADir() {
28
28
  mkdirSync(dir, { recursive: true });
29
29
  }
30
30
  }
31
+ /**
32
+ * SECURITY: record maps are keyed by ids taken from MCP tool input, and
33
+ * validateIdentifier accepts names like `__proto__` and `constructor`. On a
34
+ * plain object those resolve to Object.prototype or a built-in, which a handler
35
+ * would then write into. Null-prototype maps only ever return own records.
36
+ */
37
+ function withRecordMaps(store, keys) {
38
+ const maps = store;
39
+ for (const key of keys) {
40
+ const map = maps[key];
41
+ if (map && typeof map === 'object' && !Array.isArray(map))
42
+ Object.setPrototypeOf(map, null);
43
+ else
44
+ maps[key] = Object.create(null);
45
+ }
46
+ return store;
47
+ }
48
+ const DAA_MAPS = ['agents', 'workflows', 'knowledge'];
31
49
  function loadDAAStore() {
32
50
  try {
33
51
  const path = getDAAPath();
34
52
  if (existsSync(path)) {
35
- return JSON.parse(readFileSync(path, 'utf-8'));
53
+ const parsed = JSON.parse(readFileSync(path, 'utf-8'));
54
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
55
+ return withRecordMaps(parsed, DAA_MAPS);
36
56
  }
37
57
  }
38
58
  catch {
39
59
  // Return empty store
40
60
  }
41
- return { agents: {}, workflows: {}, knowledge: {}, version: '3.0.0' };
61
+ return withRecordMaps({ agents: {}, workflows: {}, knowledge: {}, version: '3.0.0' }, DAA_MAPS);
42
62
  }
43
63
  function saveDAAStore(store) {
44
64
  ensureDAADir();
@@ -425,7 +425,8 @@ export const embeddingsTools = [
425
425
  }
426
426
  const query = input.query;
427
427
  const topK = input.topK || 5;
428
- const threshold = input.threshold || 0.5;
428
+ // `??`, not `||`: threshold 0 is a valid request for every match, as in memory_search.
429
+ const threshold = input.threshold ?? 0.5;
429
430
  const namespace = input.namespace;
430
431
  {
431
432
  const v = validateText(query, 'query');
@@ -203,6 +203,14 @@ function resolveProjectMemoryDir(claudeProjectsDir, projectPathOverride) {
203
203
  if (/^[A-Za-z]:[\\/]/.test(source)) {
204
204
  candidates.add(source.replace(/[:\\/]/g, '-').replace(/\s+/g, '-'));
205
205
  }
206
+ // Candidate 6: Claude Code's own rule, which replaces every character
207
+ // other than [A-Za-z0-9] with `-` (the cost tracker's
208
+ // _encode-project-path.mjs does the same, #4366). The candidates above
209
+ // keep `_`, `.` and the like, so `/work/my_app` never found its folder
210
+ // `-work-my-app`. A WSL path is also tried in its Windows form.
211
+ candidates.add(source.replace(/[^a-zA-Z0-9]/g, '-'));
212
+ if (wsl)
213
+ candidates.add(`${wsl[1].toUpperCase()}:${wsl[2] ?? ''}`.replace(/[^a-zA-Z0-9]/g, '-'));
206
214
  }
207
215
  for (const projectHash of candidates) {
208
216
  const memDir = join(claudeProjectsDir, projectHash, 'memory');
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Tool definitions for session management with file persistence.
5
5
  */
6
- import { existsSync, readFileSync, readdirSync, unlinkSync, statSync, writeFileSync } from 'node:fs';
6
+ import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, unlinkSync, statSync, writeFileSync } from 'node:fs';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { gunzipSync } from 'node:zlib';
9
- import { join } from 'node:path';
9
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
10
10
  import { getProjectCwd } from './types.js';
11
11
  import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
12
12
  import { validateIdentifier, validateText } from './validate-input.js';
@@ -351,6 +351,41 @@ function isSessionRecordLike(value) {
351
351
  const v = value;
352
352
  return 'data' in v || 'stats' in v || 'sessionId' in v || 'savedAt' in v;
353
353
  }
354
+ /**
355
+ * SECURITY: `outputPath` comes from MCP tool input. Resolve it against the
356
+ * project root and require the real (symlink-resolved) parent directory to stay
357
+ * inside the real project root, and refuse an existing symlink as the target,
358
+ * so session_export cannot write files elsewhere on the host. The CLI's
359
+ * `session export` writes its own file and does not pass outputPath.
360
+ */
361
+ function containedExportPath(outputPath) {
362
+ if (!outputPath || outputPath.includes('\0'))
363
+ return null;
364
+ try {
365
+ const root = realpathSync(getProjectCwd());
366
+ const abs = resolve(root, outputPath);
367
+ const parent = realpathSync(dirname(abs));
368
+ const target = join(parent, basename(abs));
369
+ const rel = relative(root, target);
370
+ if (!rel || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
371
+ return null;
372
+ const existing = lstatOrNull(target);
373
+ if (existing && (existing.isSymbolicLink() || !existing.isFile()))
374
+ return null;
375
+ return target;
376
+ }
377
+ catch {
378
+ return null;
379
+ }
380
+ }
381
+ function lstatOrNull(p) {
382
+ try {
383
+ return lstatSync(p);
384
+ }
385
+ catch {
386
+ return null;
387
+ }
388
+ }
354
389
  export const sessionTools = [
355
390
  {
356
391
  name: 'session_save',
@@ -705,9 +740,12 @@ export const sessionTools = [
705
740
  let path = null;
706
741
  const outputPath = input.outputPath ? String(input.outputPath) : null;
707
742
  if (outputPath) {
743
+ const target = containedExportPath(outputPath);
744
+ if (!target)
745
+ return { sessionId, error: 'outputPath must be a file inside the project directory (no symlinks)' };
708
746
  try {
709
- writeFileSync(outputPath, JSON.stringify(session, null, 2), 'utf-8');
710
- path = outputPath;
747
+ writeFileSync(target, JSON.stringify(session, null, 2), 'utf-8');
748
+ path = target;
711
749
  }
712
750
  catch (e) {
713
751
  return { sessionId, error: `Could not write ${outputPath}: ${e.message}` };
@@ -132,6 +132,11 @@ export const terminalTools = [
132
132
  const store = loadTerminalStore();
133
133
  const sessionId = input.sessionId;
134
134
  const command = input.command;
135
+ // An explicit sessionId must name a stored session; only fall back to
136
+ // (or create) the default session when no sessionId was given.
137
+ if (sessionId && !store.sessions[sessionId]) {
138
+ return { success: false, error: 'Session not found' };
139
+ }
135
140
  // Find or create default session
136
141
  let session = sessionId ? store.sessions[sessionId] : Object.values(store.sessions).find(s => s.status === 'active');
137
142
  if (!session) {
@@ -55,18 +55,37 @@ function ensureWorkflowDir() {
55
55
  mkdirSync(dir, { recursive: true });
56
56
  }
57
57
  }
58
+ /**
59
+ * SECURITY: record maps are keyed by ids taken from MCP tool input, and
60
+ * validateIdentifier accepts names like `__proto__` and `constructor`. On a
61
+ * plain object those resolve to Object.prototype or a built-in, which a handler
62
+ * would then write into. Null-prototype maps only ever return own records.
63
+ */
64
+ function withRecordMaps(store, keys) {
65
+ const maps = store;
66
+ for (const key of keys) {
67
+ const map = maps[key];
68
+ if (map && typeof map === 'object' && !Array.isArray(map))
69
+ Object.setPrototypeOf(map, null);
70
+ else
71
+ maps[key] = Object.create(null);
72
+ }
73
+ return store;
74
+ }
75
+ const WORKFLOW_MAPS = ['workflows', 'templates'];
58
76
  function loadWorkflowStore() {
59
77
  try {
60
78
  const path = getWorkflowPath();
61
79
  if (existsSync(path)) {
62
- const data = readFileSync(path, 'utf-8');
63
- return JSON.parse(data);
80
+ const data = JSON.parse(readFileSync(path, 'utf-8'));
81
+ if (data && typeof data === 'object' && !Array.isArray(data))
82
+ return withRecordMaps(data, WORKFLOW_MAPS);
64
83
  }
65
84
  }
66
85
  catch {
67
86
  // Return default store on error
68
87
  }
69
- return { workflows: {}, templates: {}, version: '3.0.0' };
88
+ return withRecordMaps({ workflows: {}, templates: {}, version: '3.0.0' }, WORKFLOW_MAPS);
70
89
  }
71
90
  function saveWorkflowStore(store) {
72
91
  ensureWorkflowDir();
@@ -614,6 +614,12 @@ export declare function bridgeHealthCheck(dbPath?: string): Promise<{
614
614
  hits: number;
615
615
  misses: number;
616
616
  };
617
+ /** Why AgentDB failed to open, or null when it opened (#4654). */
618
+ agentdbInitError?: string | null;
619
+ /** Last bridge-level failure, including the AgentDB lock cause (#4622/#4654). */
620
+ bridgeFailureReason?: string | null;
621
+ /** Actionable next step when a second process owns the native store. */
622
+ hint?: string;
617
623
  hierarchicalMemory?: {
618
624
  controller: string;
619
625
  durable: boolean;