@claude-flow/cli 3.48.0 → 3.50.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/.claude/helpers/helpers.manifest.json +4 -4
- package/.claude/helpers/hook-handler.cjs +20 -4
- package/.claude/helpers/{memory.js → memory.cjs} +1 -1
- package/.claude/helpers/{router.js → router.cjs} +1 -1
- package/.claude/helpers/{session.js → session.cjs} +1 -1
- package/README.md +1 -53
- package/catalog-manifest.json +4 -4
- package/dist/src/commands/doctor.d.ts +19 -1
- package/dist/src/commands/doctor.js +88 -9
- package/dist/src/commands/hooks.js +7 -4
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init.js +20 -0
- package/dist/src/commands/memory.js +30 -7
- package/dist/src/commands/mods.d.ts +13 -0
- package/dist/src/commands/mods.js +126 -0
- package/dist/src/commands/plugins.js +44 -7
- package/dist/src/commands/policy.js +5 -2
- package/dist/src/commands/session.js +128 -21
- package/dist/src/commands/swarm.js +11 -11
- package/dist/src/index.js +10 -1
- package/dist/src/init/executor.js +11 -5
- package/dist/src/init/helper-companions.d.ts +3 -0
- package/dist/src/init/helper-companions.js +34 -0
- package/dist/src/init/helper-integrity.d.ts +21 -0
- package/dist/src/init/helper-integrity.js +62 -0
- package/dist/src/init/helper-refresh.d.ts +18 -11
- package/dist/src/init/helper-refresh.js +56 -13
- package/dist/src/init/helpers-generator.js +22 -10
- package/dist/src/mcp-tools/hooks-tools.d.ts +16 -3
- package/dist/src/mcp-tools/hooks-tools.js +87 -14
- package/dist/src/mcp-tools/memory-tools.d.ts +7 -0
- package/dist/src/mcp-tools/memory-tools.js +43 -6
- package/dist/src/mcp-tools/session-tools.d.ts +15 -0
- package/dist/src/mcp-tools/session-tools.js +236 -50
- package/dist/src/memory/memory-initializer.js +6 -3
- package/dist/src/mods/claude-installs.d.ts +29 -0
- package/dist/src/mods/claude-installs.js +83 -0
- package/dist/src/mods/install.d.ts +64 -0
- package/dist/src/mods/install.js +135 -0
- package/dist/src/mods/policy-projection.d.ts +39 -0
- package/dist/src/mods/policy-projection.js +65 -0
- package/dist/src/mods/probe.d.ts +28 -0
- package/dist/src/mods/probe.js +118 -0
- package/dist/src/plugins/manager.d.ts +37 -10
- package/dist/src/plugins/manager.js +106 -20
- package/dist/src/plugins/trust-policy.d.ts +61 -0
- package/dist/src/plugins/trust-policy.js +84 -0
- package/dist/src/services/policy-runtime.d.ts +6 -0
- package/dist/src/services/policy-runtime.js +42 -2
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +2 -2
|
@@ -42,7 +42,15 @@ const MAX_QUERY_LENGTH = 4096;
|
|
|
42
42
|
// validateMemoryInput. Imported by sanitizeMemoryKey so write-side sanitization
|
|
43
43
|
// and read-side rejection can never drift apart (the symmetry bug behind #1884).
|
|
44
44
|
const DANGEROUS_KEY_CHARS = /[;&|`$(){}[\]<>!#\\\0]|\.\.[/\\]/g;
|
|
45
|
-
const DANGEROUS_KEY_PATTERN = /[;&|`$(){}[\]<>!#\\\0]|\.\.[/\\]/;
|
|
45
|
+
export const DANGEROUS_KEY_PATTERN = /[;&|`$(){}[\]<>!#\\\0]|\.\.[/\\]/;
|
|
46
|
+
/**
|
|
47
|
+
* #3570: the one key rule for every memory write path (MCP store, CLI store,
|
|
48
|
+
* import). Plain `/` stays legal (`probe/x`); traversal and shell metacharacters
|
|
49
|
+
* do not. Returns the error message, or null when the key is acceptable.
|
|
50
|
+
*/
|
|
51
|
+
export function memoryKeyError(key) {
|
|
52
|
+
return DANGEROUS_KEY_PATTERN.test(key) ? 'Key contains disallowed characters' : null;
|
|
53
|
+
}
|
|
46
54
|
function validateMemoryInput(key, value, query, namespace) {
|
|
47
55
|
if (key && key.length > MAX_KEY_LENGTH) {
|
|
48
56
|
throw new Error(`Key exceeds maximum length of ${MAX_KEY_LENGTH} characters`);
|
|
@@ -54,9 +62,9 @@ function validateMemoryInput(key, value, query, namespace) {
|
|
|
54
62
|
throw new Error(`Query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`);
|
|
55
63
|
}
|
|
56
64
|
// Reject path traversal and shell metacharacters in keys/namespaces (#1425)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
const keyError = key ? memoryKeyError(key) : null;
|
|
66
|
+
if (keyError)
|
|
67
|
+
throw new Error(keyError);
|
|
60
68
|
if (namespace && DANGEROUS_KEY_PATTERN.test(namespace)) {
|
|
61
69
|
throw new Error('Namespace contains disallowed characters');
|
|
62
70
|
}
|
|
@@ -413,6 +421,11 @@ export const memoryTools = [
|
|
|
413
421
|
};
|
|
414
422
|
}
|
|
415
423
|
validateMemoryInput(key, value, undefined, namespace);
|
|
424
|
+
// #3570: a namespace written here must be exportable and purgeable, so it
|
|
425
|
+
// passes the same validator export and purge use.
|
|
426
|
+
const vNs = validateIdentifier(namespace, 'namespace');
|
|
427
|
+
if (!vNs.valid)
|
|
428
|
+
throw new Error(vNs.error);
|
|
416
429
|
const startTime = performance.now();
|
|
417
430
|
try {
|
|
418
431
|
const result = await storeEntry({
|
|
@@ -1542,6 +1555,9 @@ export const memoryTools = [
|
|
|
1542
1555
|
await ensureInitialized(dbPath);
|
|
1543
1556
|
const { storeEntry } = await getMemoryFunctions();
|
|
1544
1557
|
const t0 = Date.now();
|
|
1558
|
+
// Values are re-embedded on import; count the vectors actually written
|
|
1559
|
+
// rather than reporting a constant 0 next to entries that show a vector.
|
|
1560
|
+
let vectors = 0;
|
|
1545
1561
|
const inputPath = String(input.inputPath ?? '');
|
|
1546
1562
|
if (!inputPath || !existsSync(inputPath))
|
|
1547
1563
|
return { error: `File not found: ${inputPath || '(empty)'}` };
|
|
@@ -1559,6 +1575,24 @@ export const memoryTools = [
|
|
|
1559
1575
|
if (!v.valid)
|
|
1560
1576
|
throw new Error(v.error);
|
|
1561
1577
|
}
|
|
1578
|
+
// #3570: validate every entry's namespace up front so a bad file writes nothing.
|
|
1579
|
+
if (!nsOverride) {
|
|
1580
|
+
for (const e of entries) {
|
|
1581
|
+
if (e && typeof e.key === 'string' && e.namespace !== undefined) {
|
|
1582
|
+
const v = validateIdentifier(String(e.namespace), 'namespace');
|
|
1583
|
+
if (!v.valid)
|
|
1584
|
+
throw new Error(v.error);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// #3570 follow-up: keys get the same up-front, all-or-nothing check.
|
|
1589
|
+
for (const e of entries) {
|
|
1590
|
+
if (e && typeof e.key === 'string') {
|
|
1591
|
+
const keyError = memoryKeyError(e.key);
|
|
1592
|
+
if (keyError)
|
|
1593
|
+
throw new Error(`${keyError}: ${JSON.stringify(e.key)}`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1562
1596
|
let imported = 0;
|
|
1563
1597
|
let skipped = 0;
|
|
1564
1598
|
for (const e of entries) {
|
|
@@ -1569,8 +1603,11 @@ export const memoryTools = [
|
|
|
1569
1603
|
const value = typeof e.value === 'string' ? e.value : JSON.stringify(e.value ?? null);
|
|
1570
1604
|
try {
|
|
1571
1605
|
const result = await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false, dbPath });
|
|
1572
|
-
if (result.success)
|
|
1606
|
+
if (result.success) {
|
|
1573
1607
|
imported++;
|
|
1608
|
+
if (result.embedding)
|
|
1609
|
+
vectors++;
|
|
1610
|
+
}
|
|
1574
1611
|
else
|
|
1575
1612
|
skipped++;
|
|
1576
1613
|
}
|
|
@@ -1580,7 +1617,7 @@ export const memoryTools = [
|
|
|
1580
1617
|
}
|
|
1581
1618
|
return {
|
|
1582
1619
|
inputPath,
|
|
1583
|
-
imported: { entries: imported, vectors
|
|
1620
|
+
imported: { entries: imported, vectors, patterns: 0 },
|
|
1584
1621
|
skipped,
|
|
1585
1622
|
duration: Date.now() - t0,
|
|
1586
1623
|
};
|
|
@@ -4,5 +4,20 @@
|
|
|
4
4
|
* Tool definitions for session management with file persistence.
|
|
5
5
|
*/
|
|
6
6
|
import { type MCPTool } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* #3573: what session_save learned about memory, so callers can say
|
|
9
|
+
* "not included" or "no store" instead of printing an invented 0.
|
|
10
|
+
*/
|
|
11
|
+
export interface MemoryCaptureReport {
|
|
12
|
+
requested: boolean;
|
|
13
|
+
status: 'not-requested' | 'captured' | 'no-store' | 'error';
|
|
14
|
+
entries: number;
|
|
15
|
+
sources: {
|
|
16
|
+
memoryDb: number;
|
|
17
|
+
agentdb: number;
|
|
18
|
+
legacyJson: number;
|
|
19
|
+
};
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
7
22
|
export declare const sessionTools: MCPTool[];
|
|
8
23
|
//# sourceMappingURL=session-tools.d.ts.map
|
|
@@ -69,17 +69,126 @@ function listSessions() {
|
|
|
69
69
|
}
|
|
70
70
|
return sessions;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
/** Snapshot format written by session_save since #3573. */
|
|
73
|
+
const MEMORY_SNAPSHOT_FORMAT = 'ruflo-session-memory/2';
|
|
74
|
+
const MEMORY_PAGE_SIZE = 500;
|
|
75
|
+
async function readLiveEntries(listEntries, dbPath, encryptWrites) {
|
|
76
|
+
const rows = [];
|
|
77
|
+
// Bounded: stop on a short page, once `total` rows are in hand, or after a
|
|
78
|
+
// hard cap, so a backend that ignores `offset` cannot loop forever.
|
|
79
|
+
for (let page = 0, offset = 0; page < 10_000; page++, offset += MEMORY_PAGE_SIZE) {
|
|
80
|
+
const result = await listEntries({
|
|
81
|
+
dbPath, includeContent: true, limit: MEMORY_PAGE_SIZE, offset,
|
|
82
|
+
...(encryptWrites === undefined ? {} : { encryptWrites }),
|
|
83
|
+
});
|
|
84
|
+
if (!result.success)
|
|
85
|
+
throw new Error(result.error || `could not list ${dbPath}`);
|
|
86
|
+
rows.push(...result.entries);
|
|
87
|
+
if (result.entries.length < MEMORY_PAGE_SIZE || rows.length >= result.total)
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
return rows;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* #3573: capture the memory the user actually has.
|
|
94
|
+
*
|
|
95
|
+
* Before this, session_save read only the pre-SQLite `.claude-flow/memory/store.json`,
|
|
96
|
+
* which a current install never writes, so `--include-memory` saved nothing and
|
|
97
|
+
* reported "Memory Entries: 0". The snapshot now covers the same store `memory
|
|
98
|
+
* list` reads (resolveDbPath), plus rows that exist only in the sibling AgentDB
|
|
99
|
+
* store written by the MCP path, de-duplicated by namespace+key (default CLI
|
|
100
|
+
* writes are mirrored into AgentDB, so most rows are in both). A legacy
|
|
101
|
+
* store.json is still captured for backward compatibility.
|
|
102
|
+
*/
|
|
103
|
+
async function captureMemorySnapshot() {
|
|
104
|
+
const report = {
|
|
105
|
+
requested: true, status: 'captured', entries: 0,
|
|
106
|
+
sources: { memoryDb: 0, agentdb: 0, legacyJson: 0 },
|
|
107
|
+
};
|
|
108
|
+
const entries = {};
|
|
109
|
+
let legacy;
|
|
110
|
+
const legacyPath = join(getProjectCwd(), STORAGE_DIR, 'memory', 'store.json');
|
|
111
|
+
if (existsSync(legacyPath)) {
|
|
76
112
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
113
|
+
legacy = JSON.parse(readFileSync(legacyPath, 'utf-8'));
|
|
114
|
+
const legacyEntries = legacy.entries || {};
|
|
115
|
+
for (const [id, entry] of Object.entries(legacyEntries)) {
|
|
116
|
+
entries[id] = entry;
|
|
117
|
+
report.sources.legacyJson++;
|
|
80
118
|
}
|
|
81
119
|
}
|
|
82
|
-
catch { /*
|
|
120
|
+
catch { /* unreadable legacy file is not memory we can restore */ }
|
|
121
|
+
}
|
|
122
|
+
let sawStore = legacy !== undefined;
|
|
123
|
+
try {
|
|
124
|
+
const { listEntries, resolveDbPath } = await import('../memory/memory-initializer.js');
|
|
125
|
+
const primary = resolveDbPath();
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
if (existsSync(primary)) {
|
|
128
|
+
sawStore = true;
|
|
129
|
+
for (const row of await readLiveEntries(listEntries, primary)) {
|
|
130
|
+
const id = `${row.namespace}::${row.key}`;
|
|
131
|
+
seen.add(id);
|
|
132
|
+
entries[id] = {
|
|
133
|
+
key: row.key, value: row.content ?? '', namespace: row.namespace,
|
|
134
|
+
provenanceType: row.provenanceType, source: 'memory-db',
|
|
135
|
+
};
|
|
136
|
+
report.sources.memoryDb++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let sibling = null;
|
|
140
|
+
try {
|
|
141
|
+
const { siblingAgentDbPath } = await import('../memory/memory-bridge.js');
|
|
142
|
+
sibling = siblingAgentDbPath(primary);
|
|
143
|
+
}
|
|
144
|
+
catch { /* no bridge, no sibling store */ }
|
|
145
|
+
if (sibling && existsSync(sibling)) {
|
|
146
|
+
sawStore = true;
|
|
147
|
+
// The AgentDB store is read by native SQLite and must stay plaintext.
|
|
148
|
+
for (const row of await readLiveEntries(listEntries, sibling, false)) {
|
|
149
|
+
const id = `${row.namespace}::${row.key}`;
|
|
150
|
+
if (seen.has(id))
|
|
151
|
+
continue;
|
|
152
|
+
seen.add(id);
|
|
153
|
+
entries[id] = {
|
|
154
|
+
key: row.key, value: row.content ?? '', namespace: row.namespace,
|
|
155
|
+
provenanceType: row.provenanceType, source: 'agentdb',
|
|
156
|
+
};
|
|
157
|
+
report.sources.agentdb++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
report.status = 'error';
|
|
163
|
+
report.error = e.message;
|
|
164
|
+
}
|
|
165
|
+
report.entries = Object.keys(entries).length;
|
|
166
|
+
if (report.status !== 'error' && !sawStore)
|
|
167
|
+
report.status = 'no-store';
|
|
168
|
+
if (report.entries === 0 && legacy === undefined) {
|
|
169
|
+
return { report };
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
memory: {
|
|
173
|
+
format: MEMORY_SNAPSHOT_FORMAT,
|
|
174
|
+
entries,
|
|
175
|
+
...(legacy !== undefined ? { legacy } : {}),
|
|
176
|
+
},
|
|
177
|
+
report,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Load related stores for session data
|
|
181
|
+
async function loadRelatedStores(options) {
|
|
182
|
+
const data = {};
|
|
183
|
+
let memoryCapture = {
|
|
184
|
+
requested: false, status: 'not-requested', entries: 0,
|
|
185
|
+
sources: { memoryDb: 0, agentdb: 0, legacyJson: 0 },
|
|
186
|
+
};
|
|
187
|
+
if (options.includeMemory) {
|
|
188
|
+
const captured = await captureMemorySnapshot();
|
|
189
|
+
memoryCapture = captured.report;
|
|
190
|
+
if (captured.memory)
|
|
191
|
+
data.memory = captured.memory;
|
|
83
192
|
}
|
|
84
193
|
if (options.includeTasks) {
|
|
85
194
|
try {
|
|
@@ -99,7 +208,92 @@ function loadRelatedStores(options) {
|
|
|
99
208
|
}
|
|
100
209
|
catch { /* ignore */ }
|
|
101
210
|
}
|
|
102
|
-
return data;
|
|
211
|
+
return { data, memoryCapture };
|
|
212
|
+
}
|
|
213
|
+
/** Count entries in a memory snapshot of either format. */
|
|
214
|
+
function countMemoryEntries(memory) {
|
|
215
|
+
if (!memory || typeof memory !== 'object')
|
|
216
|
+
return 0;
|
|
217
|
+
return Object.keys(memory.entries || {}).length;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Restore a memory snapshot into the live store and report what was actually
|
|
221
|
+
* written. #3573: the previous restore swallowed every write failure and then
|
|
222
|
+
* echoed the count recorded at save time.
|
|
223
|
+
*/
|
|
224
|
+
async function restoreMemorySnapshot(memory) {
|
|
225
|
+
const outcome = { restored: 0, failed: 0, errors: [] };
|
|
226
|
+
const isV2 = memory.format === MEMORY_SNAPSHOT_FORMAT;
|
|
227
|
+
// Legacy snapshots are the old store.json itself; v2 snapshots carry it
|
|
228
|
+
// under `legacy` only when one existed at save time.
|
|
229
|
+
const legacyJson = isV2 ? memory.legacy : memory;
|
|
230
|
+
if (legacyJson !== undefined) {
|
|
231
|
+
const memoryDir = join(getProjectCwd(), STORAGE_DIR, 'memory');
|
|
232
|
+
if (!existsSync(memoryDir))
|
|
233
|
+
mkdirRestricted(memoryDir);
|
|
234
|
+
writeFileRestricted(join(memoryDir, 'store.json'), JSON.stringify(legacyJson, null, 2));
|
|
235
|
+
}
|
|
236
|
+
const entries = memory.entries;
|
|
237
|
+
if (!entries)
|
|
238
|
+
return outcome;
|
|
239
|
+
let storeEntry;
|
|
240
|
+
let isValidProvenanceType = () => false;
|
|
241
|
+
try {
|
|
242
|
+
const mod = await import('../memory/memory-initializer.js');
|
|
243
|
+
storeEntry = mod.storeEntry;
|
|
244
|
+
try {
|
|
245
|
+
if (typeof mod.isValidProvenanceType === 'function')
|
|
246
|
+
isValidProvenanceType = mod.isValidProvenanceType;
|
|
247
|
+
}
|
|
248
|
+
catch { /* optional: without it, provenance is simply not carried over */ }
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
outcome.failed = Object.keys(entries).length;
|
|
252
|
+
outcome.errors.push(`memory store unavailable: ${e.message}`);
|
|
253
|
+
return outcome;
|
|
254
|
+
}
|
|
255
|
+
for (const entry of Object.values(entries)) {
|
|
256
|
+
const key = entry.key || entry.id || '';
|
|
257
|
+
const value = entry.value || entry.content || '';
|
|
258
|
+
if (!key || !value) {
|
|
259
|
+
outcome.failed++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
// A snapshot from another version may carry a provenance value this one
|
|
264
|
+
// rejects; drop it rather than fail an otherwise-good row.
|
|
265
|
+
const provenance = entry.provenanceType && entry.provenanceType !== 'unknown'
|
|
266
|
+
&& isValidProvenanceType(entry.provenanceType) ? entry.provenanceType : undefined;
|
|
267
|
+
const result = await storeEntry({
|
|
268
|
+
key,
|
|
269
|
+
value,
|
|
270
|
+
namespace: entry.namespace || 'restored',
|
|
271
|
+
upsert: true,
|
|
272
|
+
...(provenance ? { provenanceType: provenance } : {}),
|
|
273
|
+
});
|
|
274
|
+
if (result && result.success === false) {
|
|
275
|
+
outcome.failed++;
|
|
276
|
+
if (result.error && outcome.errors.length < 5)
|
|
277
|
+
outcome.errors.push(`${key}: ${result.error}`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
outcome.restored++;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
outcome.failed++;
|
|
285
|
+
if (outcome.errors.length < 5)
|
|
286
|
+
outcome.errors.push(`${key}: ${e.message}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return outcome;
|
|
290
|
+
}
|
|
291
|
+
/** A session record is an object that carries at least one of its defining fields. */
|
|
292
|
+
function isSessionRecordLike(value) {
|
|
293
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
294
|
+
return false;
|
|
295
|
+
const v = value;
|
|
296
|
+
return 'data' in v || 'stats' in v || 'sessionId' in v || 'savedAt' in v;
|
|
103
297
|
}
|
|
104
298
|
export const sessionTools = [
|
|
105
299
|
{
|
|
@@ -129,7 +323,7 @@ export const sessionTools = [
|
|
|
129
323
|
}
|
|
130
324
|
const sessionId = `session-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
131
325
|
// Load related data based on options
|
|
132
|
-
const data = loadRelatedStores({
|
|
326
|
+
const { data, memoryCapture } = await loadRelatedStores({
|
|
133
327
|
includeMemory: input.includeMemory,
|
|
134
328
|
includeTasks: input.includeTasks,
|
|
135
329
|
includeAgents: input.includeAgents,
|
|
@@ -138,7 +332,7 @@ export const sessionTools = [
|
|
|
138
332
|
const stats = {
|
|
139
333
|
tasks: data.tasks ? Object.keys(data.tasks.tasks || {}).length : 0,
|
|
140
334
|
agents: data.agents ? Object.keys(data.agents.agents || {}).length : 0,
|
|
141
|
-
memoryEntries:
|
|
335
|
+
memoryEntries: countMemoryEntries(data.memory),
|
|
142
336
|
totalSize: 0,
|
|
143
337
|
};
|
|
144
338
|
const session = {
|
|
@@ -158,6 +352,7 @@ export const sessionTools = [
|
|
|
158
352
|
name: session.name,
|
|
159
353
|
savedAt: session.savedAt,
|
|
160
354
|
stats: session.stats,
|
|
355
|
+
memoryCapture,
|
|
161
356
|
path: getSessionPath(sessionId),
|
|
162
357
|
};
|
|
163
358
|
},
|
|
@@ -207,35 +402,11 @@ export const sessionTools = [
|
|
|
207
402
|
}
|
|
208
403
|
}
|
|
209
404
|
if (session) {
|
|
210
|
-
// Restore data to respective stores
|
|
211
|
-
//
|
|
405
|
+
// Restore data to respective stores. audit_1776853149979: tighten
|
|
406
|
+
// perms on the restored stores too.
|
|
407
|
+
let memoryRestore;
|
|
212
408
|
if (input.restoreMemory !== false && session.data?.memory) {
|
|
213
|
-
|
|
214
|
-
if (!existsSync(memoryDir))
|
|
215
|
-
mkdirRestricted(memoryDir);
|
|
216
|
-
writeFileRestricted(join(memoryDir, 'store.json'), JSON.stringify(session.data.memory, null, 2));
|
|
217
|
-
// Also populate active sql.js SQLite database so memory-tools can find entries
|
|
218
|
-
try {
|
|
219
|
-
const { storeEntry } = await import('../memory/memory-initializer.js');
|
|
220
|
-
const memoryData = session.data.memory;
|
|
221
|
-
if (memoryData.entries) {
|
|
222
|
-
for (const entry of Object.values(memoryData.entries)) {
|
|
223
|
-
const key = entry.key || entry.id || '';
|
|
224
|
-
const value = entry.value || entry.content || '';
|
|
225
|
-
if (key && value) {
|
|
226
|
-
await storeEntry({
|
|
227
|
-
key,
|
|
228
|
-
value,
|
|
229
|
-
namespace: entry.namespace || 'restored',
|
|
230
|
-
upsert: true,
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
// Legacy JSON restore is the fallback -- sql.js import may not be available
|
|
238
|
-
}
|
|
409
|
+
memoryRestore = await restoreMemorySnapshot(session.data.memory);
|
|
239
410
|
}
|
|
240
411
|
if (input.restoreTasks !== false && session.data?.tasks) {
|
|
241
412
|
const taskDir = join(getProjectCwd(), STORAGE_DIR, 'tasks');
|
|
@@ -260,6 +431,11 @@ export const sessionTools = [
|
|
|
260
431
|
},
|
|
261
432
|
restoredAt: new Date().toISOString(),
|
|
262
433
|
stats: session.stats,
|
|
434
|
+
// #3573: counts actually written, not the count recorded at save time.
|
|
435
|
+
...(memoryRestore ? {
|
|
436
|
+
memoryRestore,
|
|
437
|
+
stats: { ...session.stats, memoryEntriesRestored: memoryRestore.restored },
|
|
438
|
+
} : {}),
|
|
263
439
|
};
|
|
264
440
|
}
|
|
265
441
|
return {
|
|
@@ -484,30 +660,40 @@ export const sessionTools = [
|
|
|
484
660
|
// #1916: `ruflo session import <file>` referenced an unregistered
|
|
485
661
|
// `session_import` tool. Reads a session JSON and re-saves it locally.
|
|
486
662
|
name: 'session_import',
|
|
487
|
-
description: 'Import a session
|
|
663
|
+
description: 'Import a session (produced by session_export) into the local session store and optionally activate it. Pass either inputPath (a session JSON file) or data (the session record itself). Use when native Read is wrong because the file is a structured session record that must be re-registered (new id, stats recomputed) rather than just read. For reading the file, native Read is fine. Pair with session_export on the source.',
|
|
488
664
|
category: 'session',
|
|
489
665
|
inputSchema: {
|
|
490
666
|
type: 'object',
|
|
491
667
|
properties: {
|
|
492
|
-
inputPath: { type: 'string', description: 'Path to the session JSON file to import' },
|
|
668
|
+
inputPath: { type: 'string', description: 'Path to the session JSON file to import (or pass data)' },
|
|
669
|
+
data: { type: 'object', description: 'The session record itself, as written by session_export (or pass inputPath)' },
|
|
493
670
|
name: { type: 'string', description: 'Override the imported session name' },
|
|
494
671
|
activate: { type: 'boolean', description: 'Restore the imported session into the active stores' },
|
|
495
672
|
},
|
|
496
|
-
required: ['inputPath'],
|
|
497
673
|
},
|
|
498
674
|
handler: async (input) => {
|
|
499
|
-
const inputPath = String(input.inputPath ?? '');
|
|
500
|
-
if (!inputPath || !existsSync(inputPath))
|
|
501
|
-
return { error: `File not found: ${inputPath || '(empty)'}` };
|
|
502
675
|
let parsed;
|
|
503
|
-
|
|
504
|
-
parsed =
|
|
676
|
+
if (input.data !== undefined && input.data !== null) {
|
|
677
|
+
parsed = input.data;
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
const inputPath = String(input.inputPath ?? '');
|
|
681
|
+
if (!inputPath)
|
|
682
|
+
return { error: 'Provide inputPath (a session JSON file) or data (a session record)' };
|
|
683
|
+
if (!existsSync(inputPath))
|
|
684
|
+
return { error: `File not found: ${inputPath}` };
|
|
685
|
+
try {
|
|
686
|
+
parsed = JSON.parse(readFileSync(inputPath, 'utf-8'));
|
|
687
|
+
}
|
|
688
|
+
catch (e) {
|
|
689
|
+
return { error: `Invalid session JSON: ${e.message}` };
|
|
690
|
+
}
|
|
505
691
|
}
|
|
506
|
-
|
|
507
|
-
return { error:
|
|
692
|
+
if (!isSessionRecordLike(parsed)) {
|
|
693
|
+
return { error: 'Not a session record: expected an object produced by session export' };
|
|
508
694
|
}
|
|
509
695
|
const newId = `session-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
510
|
-
const stats =
|
|
696
|
+
const stats = { tasks: 0, agents: 0, memoryEntries: 0, totalSize: 0, ...(parsed.stats || {}) };
|
|
511
697
|
const session = {
|
|
512
698
|
sessionId: newId,
|
|
513
699
|
name: input.name ? String(input.name) : (parsed.name || 'imported-session'),
|
|
@@ -15,6 +15,7 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
import { readFileMaybeEncrypted, writeFileAtomic, writeFileRestricted } from '../fs-secure.js';
|
|
17
17
|
import { restoreMemoryDbFromBackup } from '../services/memory-backup.js';
|
|
18
|
+
import { validateIdentifier } from '../mcp-tools/validate-input.js';
|
|
18
19
|
/**
|
|
19
20
|
* ADR-323 — typed memory provenance. Distinguishes WHO/WHAT wrote a memory
|
|
20
21
|
* entry (a user's stated claim vs an agent's own output vs a tool result vs
|
|
@@ -3392,11 +3393,13 @@ export async function withMemoryDbLock(dbPath, fn) {
|
|
|
3392
3393
|
}
|
|
3393
3394
|
}
|
|
3394
3395
|
}
|
|
3395
|
-
const NAMESPACE_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
|
|
3396
3396
|
export async function purgeNamespace(options) {
|
|
3397
3397
|
const { namespace, dbPath: customPath } = options;
|
|
3398
|
-
|
|
3399
|
-
|
|
3398
|
+
// #3570: the same validator store, import and export use, so any namespace
|
|
3399
|
+
// that can be written can also be purged (`team:alice` included).
|
|
3400
|
+
const vNs = validateIdentifier(namespace, 'namespace');
|
|
3401
|
+
if (!vNs.valid) {
|
|
3402
|
+
return { success: false, deletedCount: 0, remainingEntries: 0, error: `Invalid namespace: ${vNs.error}` };
|
|
3400
3403
|
}
|
|
3401
3404
|
const swarmDir = getMemoryRoot();
|
|
3402
3405
|
const dbPath = customPath ? path.resolve(customPath) : path.join(swarmDir, 'memory.db');
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Claude Code binaries a shell would find, and whether the one it runs
|
|
3
|
+
* can load mods (ADR-404). Mods are on by default from Claude Code 2.1.287;
|
|
4
|
+
* from 2.1.277 they load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1; older
|
|
5
|
+
* builds predate them. A server-side rollout switch can still hold them off
|
|
6
|
+
* on any version (reported separately). A stale install earlier on PATH
|
|
7
|
+
* (a 2.1.107 npm-global beside a current native one, say) silently decides.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MODS_DEFAULT_ON = "2.1.287";
|
|
10
|
+
export declare const MODS_FIRST = "2.1.277";
|
|
11
|
+
export interface ClaudeInstall {
|
|
12
|
+
path: string;
|
|
13
|
+
version: string | null;
|
|
14
|
+
}
|
|
15
|
+
export type VersionOf = (path: string) => string | null;
|
|
16
|
+
/** a < b for dotted numeric versions. */
|
|
17
|
+
export declare function versionLess(a: string, b: string): boolean;
|
|
18
|
+
/** `claude --version` of one binary, bounded; null when it cannot say. */
|
|
19
|
+
export declare const defaultVersionOf: VersionOf;
|
|
20
|
+
/** Every `claude` on PATH, in PATH order (the first is what runs), plus ~/.local/bin. */
|
|
21
|
+
export declare function findClaudeInstalls(env: NodeJS.ProcessEnv, home: string, versionOf?: VersionOf): ClaudeInstall[];
|
|
22
|
+
export interface InstallsFinding {
|
|
23
|
+
status: 'pass' | 'warn';
|
|
24
|
+
message: string;
|
|
25
|
+
fix?: string;
|
|
26
|
+
}
|
|
27
|
+
/** The doctor's reading of what `claude` resolves to and what else is installed. */
|
|
28
|
+
export declare function judgeInstalls(installs: readonly ClaudeInstall[], enableEnvSet: boolean): InstallsFinding;
|
|
29
|
+
//# sourceMappingURL=claude-installs.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Claude Code binaries a shell would find, and whether the one it runs
|
|
3
|
+
* can load mods (ADR-404). Mods are on by default from Claude Code 2.1.287;
|
|
4
|
+
* from 2.1.277 they load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1; older
|
|
5
|
+
* builds predate them. A server-side rollout switch can still hold them off
|
|
6
|
+
* on any version (reported separately). A stale install earlier on PATH
|
|
7
|
+
* (a 2.1.107 npm-global beside a current native one, say) silently decides.
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
11
|
+
import { delimiter, join } from 'node:path';
|
|
12
|
+
export const MODS_DEFAULT_ON = '2.1.287';
|
|
13
|
+
export const MODS_FIRST = '2.1.277';
|
|
14
|
+
const parse = (v) => v.split('.').map((n) => Number(n));
|
|
15
|
+
/** a < b for dotted numeric versions. */
|
|
16
|
+
export function versionLess(a, b) {
|
|
17
|
+
const x = parse(a);
|
|
18
|
+
const y = parse(b);
|
|
19
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
20
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0))
|
|
21
|
+
return (x[i] ?? 0) < (y[i] ?? 0);
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/** `claude --version` of one binary, bounded; null when it cannot say. */
|
|
26
|
+
export const defaultVersionOf = (path) => {
|
|
27
|
+
try {
|
|
28
|
+
const out = execFileSync(path, ['--version'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
29
|
+
return out.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/** Every `claude` on PATH, in PATH order (the first is what runs), plus ~/.local/bin. */
|
|
36
|
+
export function findClaudeInstalls(env, home, versionOf = defaultVersionOf) {
|
|
37
|
+
const names = process.platform === 'win32' ? ['claude.exe', 'claude.cmd', 'claude'] : ['claude'];
|
|
38
|
+
const dirs = [...(env.PATH ?? '').split(delimiter).filter(Boolean), join(home, '.local', 'bin')];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const installs = [];
|
|
41
|
+
for (const dir of dirs) {
|
|
42
|
+
for (const name of names) {
|
|
43
|
+
const path = join(dir, name);
|
|
44
|
+
let real;
|
|
45
|
+
try {
|
|
46
|
+
if (!existsSync(path) || !statSync(path).isFile())
|
|
47
|
+
continue;
|
|
48
|
+
real = realpathSync(path);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (seen.has(real))
|
|
54
|
+
continue;
|
|
55
|
+
seen.add(real);
|
|
56
|
+
installs.push({ path, version: versionOf(path) });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return installs;
|
|
60
|
+
}
|
|
61
|
+
/** The doctor's reading of what `claude` resolves to and what else is installed. */
|
|
62
|
+
export function judgeInstalls(installs, enableEnvSet) {
|
|
63
|
+
if (installs.length === 0)
|
|
64
|
+
return { status: 'warn', message: 'no claude binary on PATH', fix: 'install Claude Code >= 2.1.287' };
|
|
65
|
+
const [first] = installs;
|
|
66
|
+
const others = installs.slice(1).filter((i) => i.version !== first.version);
|
|
67
|
+
const mixed = others.length
|
|
68
|
+
? `; also installed: ${others.map((i) => `${i.path} (${i.version ?? 'unknown'})`).join(', ')}. The first on PATH runs; a stale one can shadow a current one.`
|
|
69
|
+
: '';
|
|
70
|
+
const runs = `${first.path} (${first.version ?? 'version unknown'})`;
|
|
71
|
+
if (!first.version)
|
|
72
|
+
return { status: 'warn', message: `claude resolves to ${runs}${mixed}` };
|
|
73
|
+
if (!versionLess(first.version, MODS_DEFAULT_ON)) {
|
|
74
|
+
return { status: others.length ? 'warn' : 'pass', message: `claude resolves to ${runs}: mods on by default${mixed}` };
|
|
75
|
+
}
|
|
76
|
+
if (!versionLess(first.version, MODS_FIRST)) {
|
|
77
|
+
return enableEnvSet
|
|
78
|
+
? { status: others.length ? 'warn' : 'pass', message: `claude resolves to ${runs}: mods load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (set)${mixed}` }
|
|
79
|
+
: { status: 'warn', message: `claude resolves to ${runs}: below ${MODS_DEFAULT_ON}, mods need CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (not set)${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON}, or export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` };
|
|
80
|
+
}
|
|
81
|
+
return { status: 'warn', message: `claude resolves to ${runs}: predates mods (< ${MODS_FIRST}); classic hooks handle every event${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON} and remove the stale install from PATH` };
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=claude-installs.js.map
|