@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.
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/deployment.js +1 -1
- package/dist/src/commands/embeddings.js +6 -6
- package/dist/src/commands/init.js +4 -0
- package/dist/src/commands/ruvector/backup.js +1 -1
- package/dist/src/commands/ruvector/migrate.js +1 -1
- package/dist/src/commands/session.js +10 -7
- package/dist/src/init/executor.js +15 -5
- package/dist/src/init/types.d.ts +2 -0
- package/dist/src/init/types.js +4 -1
- package/dist/src/mcp-tools/claims-tools.js +7 -1
- package/dist/src/mcp-tools/daa-tools.js +22 -2
- package/dist/src/mcp-tools/embeddings-tools.js +2 -1
- package/dist/src/mcp-tools/memory-tools.js +8 -0
- package/dist/src/mcp-tools/session-tools.js +42 -4
- package/dist/src/mcp-tools/terminal-tools.js +5 -0
- package/dist/src/mcp-tools/workflow-tools.js +22 -3
- package/dist/src/memory/memory-bridge.d.ts +6 -0
- package/dist/src/memory/memory-bridge.js +98 -15
- package/dist/src/memory/memory-initializer.js +32 -10
- package/dist/src/services/claims-store.js +5 -2
- package/dist/src/services/ruvector-training.js +18 -5
- package/node_modules/@claude-flow/mcp/dist/server.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/server.js +6 -0
- package/node_modules/@claude-flow/mcp/dist/server.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/session-manager.d.ts +6 -0
- package/node_modules/@claude-flow/mcp/dist/session-manager.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/session-manager.js +20 -0
- package/node_modules/@claude-flow/mcp/dist/session-manager.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.js +3 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/application/policy-engine.js.map +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/package.json +1 -1
- package/package.json +4 -4
- package/plugins/ruflo-console/dist/tui.mjs +51 -51
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.71.
|
|
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": "
|
|
13
|
+
"signature": "awy0GR0jee+KKCI+7BgzocA29zGZTAZcNYRAwp7IDQ6xwzc6Rby9hqhTnk+68AHde2YvpNpuO8AyqU/P9JDFBw==",
|
|
14
14
|
"algorithm": "ed25519"
|
|
15
15
|
}
|
package/catalog-manifest.json
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
346
|
-
const
|
|
347
|
-
const
|
|
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);
|
package/dist/src/init/types.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/src/init/types.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
710
|
-
path =
|
|
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
|
-
|
|
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;
|