claude-flow 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/.proven-config-version +1 -0
- package/.claude/helpers/hook-handler.cjs +20 -4
- package/.claude/helpers/memory.cjs +1 -1
- package/.claude/helpers/router.cjs +1 -1
- package/.claude/helpers/session.cjs +1 -1
- package/.claude/proven-config.json +42 -0
- package/.claude-plugin/marketplace.json +26 -1
- package/README.md +1 -53
- package/README.zh-CN.md +1 -53
- 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
- package/v3/@claude-flow/cli/README.md +3 -53
- package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
- package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +88 -9
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +7 -4
- package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/commands/init.js +20 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +30 -7
- package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/commands/mods.js +126 -0
- package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
- package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
- package/v3/@claude-flow/cli/dist/src/commands/session.js +128 -21
- package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
- package/v3/@claude-flow/cli/dist/src/index.js +10 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +22 -10
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +87 -14
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +43 -6
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +236 -50
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +6 -3
- package/v3/@claude-flow/cli/dist/src/mods/claude-installs.d.ts +29 -0
- package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +83 -0
- package/v3/@claude-flow/cli/dist/src/mods/install.d.ts +64 -0
- package/v3/@claude-flow/cli/dist/src/mods/install.js +135 -0
- package/v3/@claude-flow/cli/dist/src/mods/policy-projection.d.ts +39 -0
- package/v3/@claude-flow/cli/dist/src/mods/policy-projection.js +65 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.js +118 -0
- package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
- package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
- package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
- package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +42 -2
- package/v3/@claude-flow/cli/package.json +2 -2
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { output } from '../output.js';
|
|
9
9
|
import { createPluginDiscoveryService, searchPlugins, getPluginSearchSuggestions, getFeaturedPlugins, getOfficialPlugins, } from '../plugins/store/index.js';
|
|
10
10
|
import { getPluginManager } from '../plugins/manager.js';
|
|
11
|
+
import { DEFAULT_PLUGIN_PERMISSIONS } from '../plugins/trust-policy.js';
|
|
11
12
|
import { getBulkRatings } from '../services/registry-api.js';
|
|
12
13
|
// List subcommand - Now uses IPFS-based registry
|
|
13
14
|
const listCommand = {
|
|
@@ -185,18 +186,29 @@ const installCommand = {
|
|
|
185
186
|
{ name: 'version', short: 'v', type: 'string', description: 'Specific version to install' },
|
|
186
187
|
{ name: 'global', short: 'g', type: 'boolean', description: 'Install globally' },
|
|
187
188
|
{ name: 'dev', short: 'd', type: 'boolean', description: 'Install as dev dependency' },
|
|
188
|
-
{
|
|
189
|
+
{
|
|
190
|
+
name: 'verify',
|
|
191
|
+
type: 'boolean',
|
|
192
|
+
description: 'Verify before registering (default: true). Registry installs must match the registry sha256 checksum. ' +
|
|
193
|
+
'Hooks and commands are withheld from a plugin that declares trustLevel "untrusted"/"unverified" or ' +
|
|
194
|
+
`permissions beyond the default set (${DEFAULT_PLUGIN_PERMISSIONS.join(', ')}) unless --trust is given. ` +
|
|
195
|
+
'--no-verify skips all of this.',
|
|
196
|
+
default: true,
|
|
197
|
+
},
|
|
198
|
+
{ name: 'trust', type: 'boolean', description: 'Register hooks and commands even if the plugin is untrusted or declares elevated permissions', default: false },
|
|
189
199
|
{ name: 'registry', short: 'r', type: 'string', description: 'Registry to use' },
|
|
190
200
|
],
|
|
191
201
|
examples: [
|
|
192
202
|
{ command: 'claude-flow plugins install -n community-analytics', description: 'Install plugin from IPFS' },
|
|
193
203
|
{ command: 'claude-flow plugins install -n ./my-plugin --dev', description: 'Install local plugin' },
|
|
204
|
+
{ command: 'claude-flow plugins install -n ./my-plugin --trust', description: 'Install a local plugin that declares elevated permissions, registering its hooks' },
|
|
194
205
|
],
|
|
195
206
|
action: async (ctx) => {
|
|
196
207
|
const name = ctx.flags.name;
|
|
197
208
|
const version = ctx.flags.version || 'latest';
|
|
198
209
|
const registryName = ctx.flags.registry;
|
|
199
210
|
const verify = ctx.flags.verify !== false;
|
|
211
|
+
const trust = ctx.flags.trust === true;
|
|
200
212
|
if (!name) {
|
|
201
213
|
output.printError('Plugin name is required');
|
|
202
214
|
return { success: false, exitCode: 1 };
|
|
@@ -227,7 +239,7 @@ const installCommand = {
|
|
|
227
239
|
if (isLocalPath) {
|
|
228
240
|
// Install from local path
|
|
229
241
|
spinner.setText(`Installing from ${name}...`);
|
|
230
|
-
result = await manager.installFromLocal(name);
|
|
242
|
+
result = await manager.installFromLocal(name, { verify, trust });
|
|
231
243
|
}
|
|
232
244
|
else {
|
|
233
245
|
// First, try to find in registry for metadata
|
|
@@ -242,7 +254,13 @@ const installCommand = {
|
|
|
242
254
|
}
|
|
243
255
|
// Install from npm (since IPFS is demo mode)
|
|
244
256
|
spinner.setText(`Installing ${name} from npm...`);
|
|
245
|
-
result = await manager.installFromNpm(name, version !== 'latest' ? version : undefined
|
|
257
|
+
result = await manager.installFromNpm(name, version !== 'latest' ? version : undefined, {
|
|
258
|
+
verify,
|
|
259
|
+
trust,
|
|
260
|
+
expectedChecksum: plugin?.checksum,
|
|
261
|
+
registryTrustLevel: plugin?.trustLevel,
|
|
262
|
+
registryPermissions: plugin?.permissions,
|
|
263
|
+
});
|
|
246
264
|
}
|
|
247
265
|
if (!result.success) {
|
|
248
266
|
spinner.fail(`Installation failed: ${result.error}`);
|
|
@@ -251,20 +269,39 @@ const installCommand = {
|
|
|
251
269
|
const installed = result.plugin;
|
|
252
270
|
spinner.succeed(`Installed ${installed.name}@${installed.version}`);
|
|
253
271
|
output.writeln();
|
|
272
|
+
if (result.decision?.verificationSkipped) {
|
|
273
|
+
output.printWarning('Verification skipped (--no-verify): hooks and commands were registered without a trust or checksum check.');
|
|
274
|
+
}
|
|
275
|
+
for (const warning of result.warnings ?? []) {
|
|
276
|
+
output.printWarning(warning);
|
|
277
|
+
}
|
|
254
278
|
const boxContent = [
|
|
255
279
|
`Plugin: ${installed.name}`,
|
|
256
280
|
`Version: ${installed.version}`,
|
|
257
281
|
`Source: ${installed.source}`,
|
|
258
282
|
`Path: ${installed.path || 'N/A'}`,
|
|
259
283
|
``,
|
|
284
|
+
`Trust: ${installed.trustLevel ?? 'not declared'}`,
|
|
285
|
+
`Permissions: ${installed.permissions?.join(', ') || 'none declared'}`,
|
|
286
|
+
`Verification: ${installed.verification ?? 'n/a'}`,
|
|
287
|
+
`Install scripts: ${installed.source === 'local' ? 'n/a (local link)' : installed.scriptsRun ? 'ran' : 'skipped (use --trust to run)'}`,
|
|
288
|
+
``,
|
|
260
289
|
`Hooks registered: ${installed.hooks?.length || 0}`,
|
|
261
290
|
`Commands added: ${installed.commands?.length || 0}`,
|
|
262
291
|
];
|
|
263
|
-
if (plugin) {
|
|
264
|
-
boxContent.push(`Trust: ${plugin.trustLevel}`);
|
|
265
|
-
boxContent.push(`Permissions: ${plugin.permissions.join(', ') || 'none'}`);
|
|
266
|
-
}
|
|
267
292
|
output.printBox(boxContent.join('\n'), 'Installation Complete');
|
|
293
|
+
if (installed.withheld) {
|
|
294
|
+
output.writeln();
|
|
295
|
+
output.printWarning(`Withheld ${installed.withheld.hooks.length} hook(s) and ${installed.withheld.commands.length} command(s) ` +
|
|
296
|
+
`from ${installed.name} because it ${installed.withheld.reasons.join('; ')}.`);
|
|
297
|
+
if (installed.withheld.hooks.length > 0) {
|
|
298
|
+
output.writeln(output.dim(` Hooks not registered: ${installed.withheld.hooks.join(', ')}`));
|
|
299
|
+
}
|
|
300
|
+
if (installed.withheld.commands.length > 0) {
|
|
301
|
+
output.writeln(output.dim(` Commands not added: ${installed.withheld.commands.join(', ')}`));
|
|
302
|
+
}
|
|
303
|
+
output.writeln(output.dim(` To register them, uninstall and reinstall with --trust.`));
|
|
304
|
+
}
|
|
268
305
|
return { success: true, data: installed };
|
|
269
306
|
}
|
|
270
307
|
catch (error) {
|
|
@@ -86,8 +86,11 @@ export const policyCommand = {
|
|
|
86
86
|
const state = loadPolicyState(root);
|
|
87
87
|
return print({ receipts: state.receipts });
|
|
88
88
|
}
|
|
89
|
-
if (operation === 'verify')
|
|
90
|
-
|
|
89
|
+
if (operation === 'verify') {
|
|
90
|
+
const ledger = await verifyPolicyLedger(root);
|
|
91
|
+
const result = print(ledger);
|
|
92
|
+
return ledger.valid ? result : { ...result, success: false, exitCode: 1 };
|
|
93
|
+
}
|
|
91
94
|
throw new Error(`unknown policy operation: ${operation}`);
|
|
92
95
|
}
|
|
93
96
|
catch (error) {
|
|
@@ -28,6 +28,33 @@ function formatDate(dateStr) {
|
|
|
28
28
|
// Otherwise show date
|
|
29
29
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* #3575: the session MCP tools report failure as `{ error }` (or
|
|
33
|
+
* `success: false`) rather than throwing, so every call site must check before
|
|
34
|
+
* printing a success line or reading result fields.
|
|
35
|
+
*/
|
|
36
|
+
function toolError(result) {
|
|
37
|
+
if (!result || typeof result !== 'object')
|
|
38
|
+
return 'the tool returned no result';
|
|
39
|
+
const r = result;
|
|
40
|
+
if (typeof r.error === 'string' && r.error.length > 0)
|
|
41
|
+
return r.error;
|
|
42
|
+
if (r.success === false)
|
|
43
|
+
return 'the operation failed';
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** #3573: say what happened to memory instead of printing an invented 0. */
|
|
47
|
+
function memoryCaptureLabel(capture, fallback) {
|
|
48
|
+
if (!capture)
|
|
49
|
+
return fallback ?? 'unknown';
|
|
50
|
+
if (!capture.requested)
|
|
51
|
+
return 'not included';
|
|
52
|
+
if (capture.status === 'error')
|
|
53
|
+
return `not captured (${capture.error || 'error'})`;
|
|
54
|
+
if (capture.status === 'no-store')
|
|
55
|
+
return '0 (no memory store found)';
|
|
56
|
+
return capture.entries;
|
|
57
|
+
}
|
|
31
58
|
// Format session status
|
|
32
59
|
function formatStatus(status) {
|
|
33
60
|
switch (status) {
|
|
@@ -186,6 +213,12 @@ const saveCommand = {
|
|
|
186
213
|
includeAgents: ctx.flags['include-agents'] !== false,
|
|
187
214
|
includeTasks: ctx.flags['include-tasks'] !== false
|
|
188
215
|
});
|
|
216
|
+
const failure = toolError(result);
|
|
217
|
+
if (failure) {
|
|
218
|
+
spinner.fail('Failed to save session');
|
|
219
|
+
output.printError(failure);
|
|
220
|
+
return { success: false, exitCode: 1 };
|
|
221
|
+
}
|
|
189
222
|
spinner.succeed('Session saved');
|
|
190
223
|
output.writeln();
|
|
191
224
|
const stats = result.stats || {};
|
|
@@ -201,10 +234,13 @@ const saveCommand = {
|
|
|
201
234
|
{ property: 'Saved At', value: new Date(result.savedAt).toLocaleString() },
|
|
202
235
|
{ property: 'Agents', value: stats.agentCount ?? stats.agents ?? 0 },
|
|
203
236
|
{ property: 'Tasks', value: stats.taskCount ?? stats.tasks ?? 0 },
|
|
204
|
-
{ property: 'Memory Entries', value: stats.memoryEntries
|
|
237
|
+
{ property: 'Memory Entries', value: memoryCaptureLabel(result.memoryCapture, stats.memoryEntries) },
|
|
205
238
|
{ property: 'Total Size', value: formatSize(stats.totalSize ?? 0) }
|
|
206
239
|
]
|
|
207
240
|
});
|
|
241
|
+
if (result.memoryCapture?.status === 'error') {
|
|
242
|
+
output.printWarning(`Memory was requested but could not be captured: ${result.memoryCapture.error}`);
|
|
243
|
+
}
|
|
208
244
|
output.writeln();
|
|
209
245
|
output.printSuccess(`Session saved: ${result.sessionId}`);
|
|
210
246
|
output.printInfo(`Restore with: claude-flow session restore ${result.sessionId}`);
|
|
@@ -313,8 +349,25 @@ const restoreCommand = {
|
|
|
313
349
|
restoreAgents,
|
|
314
350
|
restoreTasks
|
|
315
351
|
});
|
|
316
|
-
|
|
352
|
+
const failure = toolError(result) ?? (result.restored === false ? 'Session not found' : null);
|
|
353
|
+
if (failure) {
|
|
354
|
+
spinner.fail('Failed to restore session');
|
|
355
|
+
output.printError(failure);
|
|
356
|
+
return { success: false, exitCode: 1 };
|
|
357
|
+
}
|
|
358
|
+
const memoryRestore = result.memoryRestore;
|
|
359
|
+
const memoryPartial = !!memoryRestore && memoryRestore.failed > 0;
|
|
360
|
+
if (memoryPartial) {
|
|
361
|
+
spinner.fail('Session restored with memory errors');
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
spinner.succeed('Session restored');
|
|
365
|
+
}
|
|
317
366
|
output.writeln();
|
|
367
|
+
// #3573: distinguish "not requested" from "the session holds no memory".
|
|
368
|
+
const memoryStatus = result.restoredComponents.memory
|
|
369
|
+
? (memoryPartial ? output.error('Partial') : output.success('Restored'))
|
|
370
|
+
: restoreMemory ? output.dim('Not in session') : output.dim('Skipped');
|
|
318
371
|
output.printTable({
|
|
319
372
|
columns: [
|
|
320
373
|
{ key: 'component', header: 'Component', width: 20 },
|
|
@@ -324,8 +377,10 @@ const restoreCommand = {
|
|
|
324
377
|
data: [
|
|
325
378
|
{
|
|
326
379
|
component: 'Memory',
|
|
327
|
-
status:
|
|
328
|
-
count: result.restoredComponents.memory
|
|
380
|
+
status: memoryStatus,
|
|
381
|
+
count: result.restoredComponents.memory
|
|
382
|
+
? (result.stats.memoryEntriesRestored ?? result.stats.memoryEntries)
|
|
383
|
+
: 0
|
|
329
384
|
},
|
|
330
385
|
{
|
|
331
386
|
component: 'Agents',
|
|
@@ -340,11 +395,19 @@ const restoreCommand = {
|
|
|
340
395
|
]
|
|
341
396
|
});
|
|
342
397
|
output.writeln();
|
|
343
|
-
|
|
398
|
+
if (memoryPartial) {
|
|
399
|
+
output.printWarning(`${memoryRestore.failed} memory entries could not be restored` +
|
|
400
|
+
(memoryRestore.errors.length ? `: ${memoryRestore.errors.join('; ')}` : ''));
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
output.printSuccess(`Session ${sessionId} restored successfully`);
|
|
404
|
+
}
|
|
344
405
|
if (ctx.flags.format === 'json') {
|
|
345
406
|
output.printJson(result);
|
|
346
407
|
}
|
|
347
|
-
return
|
|
408
|
+
return memoryPartial
|
|
409
|
+
? { success: false, exitCode: 1, data: result }
|
|
410
|
+
: { success: true, data: result };
|
|
348
411
|
}
|
|
349
412
|
catch (error) {
|
|
350
413
|
spinner.fail('Failed to restore session');
|
|
@@ -391,6 +454,11 @@ const deleteCommand = {
|
|
|
391
454
|
}
|
|
392
455
|
try {
|
|
393
456
|
const result = await callMCPTool('session_delete', { sessionId });
|
|
457
|
+
const failure = toolError(result) ?? (result.deleted === false ? 'Session not found' : null);
|
|
458
|
+
if (failure) {
|
|
459
|
+
output.printError(`Failed to delete session: ${failure}`);
|
|
460
|
+
return { success: false, exitCode: 1 };
|
|
461
|
+
}
|
|
394
462
|
output.writeln();
|
|
395
463
|
output.printSuccess(`Session ${sessionId} deleted`);
|
|
396
464
|
if (ctx.flags.format === 'json') {
|
|
@@ -412,7 +480,7 @@ const deleteCommand = {
|
|
|
412
480
|
// Export subcommand
|
|
413
481
|
const exportCommand = {
|
|
414
482
|
name: 'export',
|
|
415
|
-
description: 'Export session to file',
|
|
483
|
+
description: 'Export a saved session to a file. Usage: session export <session-id> [-o file] (or --latest)',
|
|
416
484
|
options: [
|
|
417
485
|
{
|
|
418
486
|
name: 'output',
|
|
@@ -420,6 +488,12 @@ const exportCommand = {
|
|
|
420
488
|
description: 'Output file path',
|
|
421
489
|
type: 'string'
|
|
422
490
|
},
|
|
491
|
+
{
|
|
492
|
+
name: 'latest',
|
|
493
|
+
description: 'Export the most recently saved session when no session id is given',
|
|
494
|
+
type: 'boolean',
|
|
495
|
+
default: false
|
|
496
|
+
},
|
|
423
497
|
{
|
|
424
498
|
name: 'format',
|
|
425
499
|
short: 'f',
|
|
@@ -446,14 +520,23 @@ const exportCommand = {
|
|
|
446
520
|
let outputPath = ctx.flags.output;
|
|
447
521
|
const exportFormat = ctx.flags.format;
|
|
448
522
|
const compress = ctx.flags.compress;
|
|
449
|
-
//
|
|
523
|
+
// #3575: like `session delete`, refuse to guess which session to act on.
|
|
524
|
+
// Exporting the most recent one is available, but only when asked for.
|
|
450
525
|
if (!sessionId) {
|
|
526
|
+
if (!ctx.flags.latest) {
|
|
527
|
+
output.printError('Session ID is required. Pass a session id, or --latest to export the most recently saved session.');
|
|
528
|
+
return { success: false, exitCode: 1 };
|
|
529
|
+
}
|
|
451
530
|
try {
|
|
452
531
|
const current = await callMCPTool('session_current', {});
|
|
532
|
+
if (toolError(current) || !current.sessionId) {
|
|
533
|
+
output.printError('No saved sessions to export.');
|
|
534
|
+
return { success: false, exitCode: 1 };
|
|
535
|
+
}
|
|
453
536
|
sessionId = current.sessionId;
|
|
454
537
|
}
|
|
455
538
|
catch {
|
|
456
|
-
output.printError('No
|
|
539
|
+
output.printError('No saved sessions to export.');
|
|
457
540
|
return { success: false, exitCode: 1 };
|
|
458
541
|
}
|
|
459
542
|
}
|
|
@@ -469,6 +552,12 @@ const exportCommand = {
|
|
|
469
552
|
sessionId,
|
|
470
553
|
includeMemory: ctx.flags['include-memory'] !== false
|
|
471
554
|
});
|
|
555
|
+
const failure = toolError(result);
|
|
556
|
+
if (failure || result.data === undefined) {
|
|
557
|
+
spinner.fail('Failed to export session');
|
|
558
|
+
output.printError(failure || 'The export returned no session data');
|
|
559
|
+
return { success: false, exitCode: 1 };
|
|
560
|
+
}
|
|
472
561
|
// Format output
|
|
473
562
|
let content;
|
|
474
563
|
if (exportFormat === 'yaml') {
|
|
@@ -484,7 +573,8 @@ const exportCommand = {
|
|
|
484
573
|
fs.writeFileSync(absolutePath, content, 'utf-8');
|
|
485
574
|
spinner.succeed('Session exported');
|
|
486
575
|
output.writeln();
|
|
487
|
-
|
|
576
|
+
// session_export returns the whole record under `data`; its stats live there.
|
|
577
|
+
const exportStats = result.stats || result.data?.stats || {};
|
|
488
578
|
output.printTable({
|
|
489
579
|
columns: [
|
|
490
580
|
{ key: 'property', header: 'Property', width: 18 },
|
|
@@ -522,7 +612,7 @@ const exportCommand = {
|
|
|
522
612
|
// Import subcommand
|
|
523
613
|
const importCommand = {
|
|
524
614
|
name: 'import',
|
|
525
|
-
description: 'Import session from file',
|
|
615
|
+
description: 'Import a session from a file written by session export. Usage: session import <file> [--name n] [--activate]',
|
|
526
616
|
options: [
|
|
527
617
|
{
|
|
528
618
|
name: 'name',
|
|
@@ -555,21 +645,32 @@ const importCommand = {
|
|
|
555
645
|
const spinner = output.createSpinner({ text: 'Importing session...' });
|
|
556
646
|
spinner.start();
|
|
557
647
|
try {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
// Parse based on extension
|
|
648
|
+
// The YAML written by `session export --format yaml` is display-only;
|
|
649
|
+
// there is no YAML parser here, so say so instead of failing obscurely.
|
|
561
650
|
if (absolutePath.endsWith('.yaml') || absolutePath.endsWith('.yml')) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
651
|
+
try {
|
|
652
|
+
JSON.parse(fs.readFileSync(absolutePath, 'utf-8'));
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
spinner.fail('Failed to import session');
|
|
656
|
+
output.printError('YAML session import is not supported. Export with --format json and import that file.');
|
|
657
|
+
return { success: false, exitCode: 1 };
|
|
658
|
+
}
|
|
567
659
|
}
|
|
660
|
+
// #3575: session_import reads the file itself from `inputPath`. Passing a
|
|
661
|
+
// parsed `data` object used to hit its required-argument check, and the
|
|
662
|
+
// CLI then printed "Session imported" before crashing on the error result.
|
|
568
663
|
const result = await callMCPTool('session_import', {
|
|
569
|
-
|
|
664
|
+
inputPath: absolutePath,
|
|
570
665
|
name: sessionName,
|
|
571
666
|
activate
|
|
572
667
|
});
|
|
668
|
+
const failure = toolError(result);
|
|
669
|
+
if (failure) {
|
|
670
|
+
spinner.fail('Failed to import session');
|
|
671
|
+
output.printError(failure);
|
|
672
|
+
return { success: false, exitCode: 1 };
|
|
673
|
+
}
|
|
573
674
|
spinner.succeed('Session imported');
|
|
574
675
|
output.writeln();
|
|
575
676
|
output.printTable({
|
|
@@ -619,6 +720,11 @@ const currentCommand = {
|
|
|
619
720
|
action: async (ctx) => {
|
|
620
721
|
try {
|
|
621
722
|
const result = await callMCPTool('session_current', { includeStats: true });
|
|
723
|
+
if (toolError(result)) {
|
|
724
|
+
output.printWarning('No active session');
|
|
725
|
+
output.printInfo('Save one with "claude-flow session save"');
|
|
726
|
+
return { success: true, data: { active: false } };
|
|
727
|
+
}
|
|
622
728
|
if (ctx.flags.format === 'json') {
|
|
623
729
|
output.printJson(result);
|
|
624
730
|
return { success: true, data: result };
|
|
@@ -727,7 +833,8 @@ export const sessionCommand = {
|
|
|
727
833
|
{ command: 'claude-flow session save -n "checkpoint-1"', description: 'Save current session' },
|
|
728
834
|
{ command: 'claude-flow session restore session-123', description: 'Restore a session' },
|
|
729
835
|
{ command: 'claude-flow session delete session-123', description: 'Delete a session' },
|
|
730
|
-
{ command: 'claude-flow session export -o backup.json', description: 'Export session to file' },
|
|
836
|
+
{ command: 'claude-flow session export session-123 -o backup.json', description: 'Export a session to file' },
|
|
837
|
+
{ command: 'claude-flow session export --latest -o backup.json', description: 'Export the most recently saved session' },
|
|
731
838
|
{ command: 'claude-flow session import backup.json', description: 'Import session from file' },
|
|
732
839
|
{ command: 'claude-flow session current', description: 'Show current session' }
|
|
733
840
|
],
|
|
@@ -196,17 +196,14 @@ function getSwarmStatus(swarmId) {
|
|
|
196
196
|
// Ignore
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
|
-
//
|
|
200
|
-
//
|
|
199
|
+
// Progress is completed / total tasks. With no tasks there is nothing to be
|
|
200
|
+
// a fraction of, so it is `null` with a reason — never an invented number
|
|
201
|
+
// (#3572: a hard-coded 5% was shown for a swarm with no tasks, forever).
|
|
201
202
|
const totalTasks = completedTasks + inProgressTasks + pendingTasks;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
else if (swarmState) {
|
|
207
|
-
// Swarm initialized but no tasks yet
|
|
208
|
-
progress = 5;
|
|
209
|
-
}
|
|
203
|
+
const progress = totalTasks > 0
|
|
204
|
+
? Math.round((completedTasks / totalTasks) * 100)
|
|
205
|
+
: null;
|
|
206
|
+
const progressReason = totalTasks > 0 ? null : 'no tasks';
|
|
210
207
|
// Determine status
|
|
211
208
|
let status = 'idle';
|
|
212
209
|
if (inProgressTasks > 0 || activeAgents > 0) {
|
|
@@ -243,6 +240,7 @@ function getSwarmStatus(swarmId) {
|
|
|
243
240
|
completed: 0
|
|
244
241
|
},
|
|
245
242
|
progress,
|
|
243
|
+
progressReason,
|
|
246
244
|
tasks: {
|
|
247
245
|
total: totalTasks,
|
|
248
246
|
completed: completedTasks,
|
|
@@ -738,7 +736,9 @@ const statusCommand = {
|
|
|
738
736
|
output.writeln(output.bold(`Swarm Status: ${status.id ?? output.dim('unknown id')}`));
|
|
739
737
|
output.writeln();
|
|
740
738
|
// Progress bar
|
|
741
|
-
output.writeln(
|
|
739
|
+
output.writeln(status.progress === null
|
|
740
|
+
? `Overall Progress: ${output.dim(`n/a (${status.progressReason})`)}`
|
|
741
|
+
: `Overall Progress: ${output.progressBar(status.progress, 100, 40)}`);
|
|
742
742
|
output.writeln();
|
|
743
743
|
// Agent status
|
|
744
744
|
output.writeln(output.bold('Agents'));
|
|
@@ -155,10 +155,19 @@ export class CLI {
|
|
|
155
155
|
// loudly (not silent) — the existing project helpers were left intact.
|
|
156
156
|
this.output.printWarning(`Skipped helper auto-refresh — ${r.blocked}. Reinstall @claude-flow/cli from a trusted source.`);
|
|
157
157
|
}
|
|
158
|
+
else if (r.healed) {
|
|
159
|
+
// #3565: a critical helper's stamp matched but its on-disk content
|
|
160
|
+
// didn't hash-match the signed manifest — restored, but always
|
|
161
|
+
// surfaced (not verbose-gated like a routine version-bump refresh).
|
|
162
|
+
this.output.printWarning(`Detected tampered critical helper(s) in .claude/helpers (${(r.tampered || []).join(', ')}) — restored verified content from the installed package. If this wasn't expected, find out what modified them.`);
|
|
163
|
+
}
|
|
158
164
|
else if (r.refreshed && this.output.isVerbose()) {
|
|
159
165
|
this.output.printDebug(`Refreshed .claude/helpers (${r.from} → ${r.to})`);
|
|
160
166
|
}
|
|
161
|
-
if (r.global?.
|
|
167
|
+
if (r.global?.healed) {
|
|
168
|
+
this.output.printWarning(`Detected tampered critical helper(s) in ~/.claude/helpers (${(r.global.tampered || []).join(', ')}) — restored verified content.`);
|
|
169
|
+
}
|
|
170
|
+
else if (r.global?.refreshed && this.output.isVerbose()) {
|
|
162
171
|
this.output.printDebug(`Refreshed ~/.claude/helpers (${r.global.from} → ${r.global.to})`);
|
|
163
172
|
}
|
|
164
173
|
else if (r.global?.blocked && r.global.blocked !== r.blocked) {
|
|
@@ -18,6 +18,7 @@ import { generatePreCommitHook, generatePostCommitHook, generateSessionManager,
|
|
|
18
18
|
import { getInstalledCliVersion, HELPERS_STAMP_FILE } from './helper-refresh.js';
|
|
19
19
|
import { generateClaudeMd } from './claudemd-generator.js';
|
|
20
20
|
import { recordMemoryPackagePath } from './memory-package-resolver.js';
|
|
21
|
+
import { ensureCommonJsCompanions } from './helper-companions.js';
|
|
21
22
|
import { scanSettingsForRisk, formatRiskFindingsAsWarnings } from './settings-risk-scanner.js';
|
|
22
23
|
/**
|
|
23
24
|
* Skills to copy based on configuration
|
|
@@ -499,7 +500,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
499
500
|
const sourceHelpersForUpgrade = findSourceHelpersDir();
|
|
500
501
|
if (sourceHelpersForUpgrade) {
|
|
501
502
|
// Keep in sync with helper-refresh.ts:CRITICAL_HELPERS.
|
|
502
|
-
const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'router.
|
|
503
|
+
const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs', 'statusline.cjs', 'router.cjs'];
|
|
503
504
|
for (const helperName of criticalHelpers) {
|
|
504
505
|
const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
|
|
505
506
|
const sourcePath = path.join(sourceHelpersForUpgrade, helperName);
|
|
@@ -524,7 +525,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
524
525
|
'hook-handler.cjs': generateHookHandler(),
|
|
525
526
|
'intelligence.cjs': generateIntelligenceStub(),
|
|
526
527
|
'auto-memory-hook.mjs': generateAutoMemoryHook(),
|
|
527
|
-
'router.
|
|
528
|
+
'router.cjs': generateAgentRouter(), // ADR-389 / #3555
|
|
528
529
|
};
|
|
529
530
|
for (const [helperName, content] of Object.entries(generatedCritical)) {
|
|
530
531
|
const targetPath = path.join(targetDir, '.claude', 'helpers', helperName);
|
|
@@ -541,6 +542,11 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
541
542
|
catch { }
|
|
542
543
|
}
|
|
543
544
|
}
|
|
545
|
+
// #3555: the refreshed hook-handler requires the .cjs companions; older
|
|
546
|
+
// installs only have session.js / memory.js, which can't load in ESM.
|
|
547
|
+
for (const name of await ensureCommonJsCompanions(path.join(targetDir, '.claude', 'helpers'))) {
|
|
548
|
+
result.created.push(`.claude/helpers/${name}`);
|
|
549
|
+
}
|
|
544
550
|
// Stamp the installed version so the startup auto-refresh treats these as
|
|
545
551
|
// current (no redundant re-copy on the next command).
|
|
546
552
|
try {
|
|
@@ -1295,9 +1301,9 @@ async function writeHelpers(targetDir, options, result) {
|
|
|
1295
1301
|
const helpers = {
|
|
1296
1302
|
'pre-commit': generatePreCommitHook(),
|
|
1297
1303
|
'post-commit': generatePostCommitHook(),
|
|
1298
|
-
'session.
|
|
1299
|
-
'router.
|
|
1300
|
-
'memory.
|
|
1304
|
+
'session.cjs': generateSessionManager(),
|
|
1305
|
+
'router.cjs': generateAgentRouter(),
|
|
1306
|
+
'memory.cjs': generateMemoryHelper(),
|
|
1301
1307
|
'hook-handler.cjs': generateHookHandler(),
|
|
1302
1308
|
'intelligence.cjs': generateIntelligenceStub(),
|
|
1303
1309
|
'auto-memory-hook.mjs': generateAutoMemoryHook(),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommonJS companion helpers that `hook-handler.cjs` requires but that are not
|
|
3
|
+
* in the signed critical set (#3555).
|
|
4
|
+
*
|
|
5
|
+
* They are CommonJS, so they ship as `.cjs`: a `.js` copy cannot load in a
|
|
6
|
+
* `"type":"module"` project. Installs made before the rename have only the
|
|
7
|
+
* `.js` copies, which the refreshed hook-handler no longer requires, so the
|
|
8
|
+
* refresh writes any missing `.cjs` companion from the CLI's own compiled
|
|
9
|
+
* generators (the trust root; no external file to verify). Stale `.js` copies
|
|
10
|
+
* are left in place and ignored. Existing `.cjs` files are never overwritten.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
export const COMMONJS_COMPANION_HELPERS = ['session.cjs', 'memory.cjs'];
|
|
15
|
+
export async function ensureCommonJsCompanions(helpersDir) {
|
|
16
|
+
const missing = COMMONJS_COMPANION_HELPERS.filter((name) => !fs.existsSync(path.join(helpersDir, name)));
|
|
17
|
+
if (missing.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
const gen = await import('./helpers-generator.js');
|
|
20
|
+
const content = {
|
|
21
|
+
'session.cjs': gen.generateSessionManager,
|
|
22
|
+
'memory.cjs': gen.generateMemoryHelper,
|
|
23
|
+
};
|
|
24
|
+
const written = [];
|
|
25
|
+
for (const name of missing) {
|
|
26
|
+
try {
|
|
27
|
+
fs.writeFileSync(path.join(helpersDir, name), content[name](), { encoding: 'utf-8', mode: 0o755 });
|
|
28
|
+
written.push(name);
|
|
29
|
+
}
|
|
30
|
+
catch { /* best-effort: hook-handler degrades gracefully without it */ }
|
|
31
|
+
}
|
|
32
|
+
return written;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=helper-companions.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface IntegrityResult {
|
|
2
|
+
/** Critical helper names whose on-disk hash doesn't match the signed manifest
|
|
3
|
+
* (or that are missing from disk despite being part of the signed set). */
|
|
4
|
+
tampered: string[];
|
|
5
|
+
/** Set only when the package's OWN signed manifest can't be verified at all —
|
|
6
|
+
* there's no ground truth to check the installed files against, distinct
|
|
7
|
+
* from a clean `tampered: []` result. */
|
|
8
|
+
blocked?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Re-verify each of `criticalHelpers` present on disk in `helpersDir` against
|
|
12
|
+
* `sourceDir`'s signed manifest. Read-only — safe to call without holding any
|
|
13
|
+
* refresh lock; callers only need to acquire one if `tampered` comes back
|
|
14
|
+
* non-empty and they intend to repair it.
|
|
15
|
+
*
|
|
16
|
+
* `sourceDir: null` (package source unresolvable) returns `{ tampered: [] }`
|
|
17
|
+
* — fails open rather than flagging every install as tampered when there is
|
|
18
|
+
* no ground truth at all to compare against.
|
|
19
|
+
*/
|
|
20
|
+
export declare function verifyInstalledCriticalHelpers(helpersDir: string, sourceDir: string | null, criticalHelpers: readonly string[], pubkeyPemOverride?: string): IntegrityResult;
|
|
21
|
+
//# sourceMappingURL=helper-integrity.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-install integrity re-verification for signed critical helpers (#3565).
|
|
3
|
+
*
|
|
4
|
+
* `helper-refresh.ts` verifies a helper's signature/hash once, at copy time,
|
|
5
|
+
* then stamps the target directory with the installed CLI version. Every
|
|
6
|
+
* later command short-circuited on "stamp already matches" with NO further
|
|
7
|
+
* verification — so a critical helper modified on disk after that point (a
|
|
8
|
+
* sibling package's postinstall, a stray older CLI process racing a write,
|
|
9
|
+
* direct tampering) stayed silently modified through any number of
|
|
10
|
+
* subsequent commands, forever. This module re-hashes the already-INSTALLED
|
|
11
|
+
* helpers against the signed manifest independent of the stamp, so callers
|
|
12
|
+
* can re-run it on every invocation rather than trusting a single past check.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately has no opinion on WHAT to do about tampering (re-copy, warn,
|
|
15
|
+
* refuse) — that policy lives in helper-refresh.ts, which already owns the
|
|
16
|
+
* fail-closed verify-then-copy path this result feeds into to heal.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { verifyHelpersManifest, sha256Hex, HELPERS_MANIFEST_FILE, } from './helper-signing.js';
|
|
21
|
+
/**
|
|
22
|
+
* Re-verify each of `criticalHelpers` present on disk in `helpersDir` against
|
|
23
|
+
* `sourceDir`'s signed manifest. Read-only — safe to call without holding any
|
|
24
|
+
* refresh lock; callers only need to acquire one if `tampered` comes back
|
|
25
|
+
* non-empty and they intend to repair it.
|
|
26
|
+
*
|
|
27
|
+
* `sourceDir: null` (package source unresolvable) returns `{ tampered: [] }`
|
|
28
|
+
* — fails open rather than flagging every install as tampered when there is
|
|
29
|
+
* no ground truth at all to compare against.
|
|
30
|
+
*/
|
|
31
|
+
export function verifyInstalledCriticalHelpers(helpersDir, sourceDir, criticalHelpers, pubkeyPemOverride) {
|
|
32
|
+
if (!sourceDir)
|
|
33
|
+
return { tampered: [] };
|
|
34
|
+
let trusted = null;
|
|
35
|
+
try {
|
|
36
|
+
trusted = verifyHelpersManifest(fs.readFileSync(path.join(sourceDir, HELPERS_MANIFEST_FILE), 'utf-8'), pubkeyPemOverride);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
trusted = null;
|
|
40
|
+
}
|
|
41
|
+
if (!trusted)
|
|
42
|
+
return { tampered: [], blocked: 'signed helpers manifest missing or signature invalid' };
|
|
43
|
+
const tampered = [];
|
|
44
|
+
for (const name of criticalHelpers) {
|
|
45
|
+
const expected = trusted.files[name];
|
|
46
|
+
// Not part of THIS version's signed set (or source doesn't ship it) —
|
|
47
|
+
// nothing to compare the installed copy against.
|
|
48
|
+
if (!expected || !fs.existsSync(path.join(sourceDir, name)))
|
|
49
|
+
continue;
|
|
50
|
+
let actual = null;
|
|
51
|
+
try {
|
|
52
|
+
actual = sha256Hex(fs.readFileSync(path.join(helpersDir, name)));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
actual = null;
|
|
56
|
+
}
|
|
57
|
+
if (actual !== expected)
|
|
58
|
+
tampered.push(name);
|
|
59
|
+
}
|
|
60
|
+
return { tampered };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=helper-integrity.js.map
|