@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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ruflo mods` — opt into running ruflo as a Claude Code mod (ADR-404,
|
|
3
|
+
* Claude Code function hooks, early access).
|
|
4
|
+
*
|
|
5
|
+
* install/uninstall edit one settings file and record what they added;
|
|
6
|
+
* status/doctor report what can be known from outside a session; sync-policy
|
|
7
|
+
* rewrites the policy projection the mod's tool check reads. Classic hooks
|
|
8
|
+
* are never removed: they stay the default and the fallback.
|
|
9
|
+
*/
|
|
10
|
+
import { output } from '../output.js';
|
|
11
|
+
import { installMod, uninstallMod } from '../mods/install.js';
|
|
12
|
+
import { probeMods } from '../mods/probe.js';
|
|
13
|
+
function projectRoot(ctx) {
|
|
14
|
+
return ctx.flags.projectRoot ?? ctx.flags['project-root'] ?? ctx.cwd ?? process.cwd();
|
|
15
|
+
}
|
|
16
|
+
function printFindings(findings) {
|
|
17
|
+
for (const f of findings) {
|
|
18
|
+
const mark = f.status === 'pass' ? output.success('✓') : f.status === 'warn' ? output.warning('!') : output.error('✗');
|
|
19
|
+
output.writeln(`${mark} ${f.name}: ${f.message}`);
|
|
20
|
+
if (f.fix && f.status !== 'pass')
|
|
21
|
+
output.writeln(output.dim(` fix: ${f.fix}`));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const rootOption = { name: 'project-root', description: 'Project root (default: current directory)', type: 'string' };
|
|
25
|
+
const installSub = {
|
|
26
|
+
name: 'install',
|
|
27
|
+
description: 'Enable the ruflo-mods plugin for this project (opt-in; classic hooks stay as fallback)',
|
|
28
|
+
options: [
|
|
29
|
+
rootOption,
|
|
30
|
+
{ name: 'scope', description: 'local (.claude/settings.local.json, default) | project (.claude/settings.json)', type: 'string', default: 'local' },
|
|
31
|
+
{ name: 'dry-run', description: 'Show the settings that would be written', type: 'boolean', default: false },
|
|
32
|
+
],
|
|
33
|
+
action: async (ctx) => {
|
|
34
|
+
const scope = ctx.flags.scope ?? 'local';
|
|
35
|
+
if (scope !== 'local' && scope !== 'project') {
|
|
36
|
+
output.printError(`--scope must be local or project, got ${scope}`);
|
|
37
|
+
return { success: false, exitCode: 1 };
|
|
38
|
+
}
|
|
39
|
+
const root = projectRoot(ctx);
|
|
40
|
+
const result = installMod(root, scope, (ctx.flags.dryRun === true || ctx.flags['dry-run'] === true));
|
|
41
|
+
if (result.dryRun) {
|
|
42
|
+
output.writeln(`Would write ${result.settingsFile}:`);
|
|
43
|
+
output.printJson(result.next);
|
|
44
|
+
return { success: true, data: result };
|
|
45
|
+
}
|
|
46
|
+
await syncPolicy(root, true);
|
|
47
|
+
output.printSuccess(`ruflo-mods enabled in ${result.settingsFile}${result.backup ? ` (backup: ${result.backup})` : ''}`);
|
|
48
|
+
output.writeln('Restart Claude Code, then run /ruflo-mods in a session to see what the mod owns.');
|
|
49
|
+
output.writeln(output.dim('Early access: Claude Code loads it only where function hooks are on. Run `ruflo mods doctor`.'));
|
|
50
|
+
return { success: true, data: result };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const uninstallSub = {
|
|
54
|
+
name: 'uninstall',
|
|
55
|
+
description: 'Remove what `ruflo mods install` added (classic hooks take every event back)',
|
|
56
|
+
options: [rootOption, { name: 'dry-run', description: 'Show what would be removed', type: 'boolean', default: false }],
|
|
57
|
+
action: async (ctx) => {
|
|
58
|
+
const result = uninstallMod(projectRoot(ctx), (ctx.flags.dryRun === true || ctx.flags['dry-run'] === true));
|
|
59
|
+
if (!result.removed) {
|
|
60
|
+
output.printWarning('No install record (.claude-flow/mods/install.json): nothing ruflo added to remove.');
|
|
61
|
+
return { success: true, data: result };
|
|
62
|
+
}
|
|
63
|
+
output.printSuccess(`${result.dryRun ? 'Would remove' : 'Removed'} ruflo-mods from ${result.settingsFile}`);
|
|
64
|
+
return { success: true, data: result };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
function findingsCommand(name, description) {
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
description,
|
|
71
|
+
options: [rootOption, { name: 'json', description: 'Output as JSON', type: 'boolean', default: false }],
|
|
72
|
+
action: async (ctx) => {
|
|
73
|
+
const findings = probeMods({ projectRoot: projectRoot(ctx) });
|
|
74
|
+
if (ctx.flags.json)
|
|
75
|
+
output.printJson(findings);
|
|
76
|
+
else
|
|
77
|
+
printFindings(findings);
|
|
78
|
+
const failed = findings.some((f) => f.status === 'fail');
|
|
79
|
+
// status reports; doctor gates (warnings are expected while early access is off).
|
|
80
|
+
return { success: !failed, exitCode: name === 'doctor' && failed ? 1 : 0, data: findings };
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function syncPolicy(root, quiet) {
|
|
85
|
+
try {
|
|
86
|
+
const { loadPolicyState } = await import('../services/policy-runtime.js');
|
|
87
|
+
const { syncPolicyProjection } = await import('../mods/policy-projection.js');
|
|
88
|
+
const result = syncPolicyProjection(root, loadPolicyState(root));
|
|
89
|
+
if (!quiet || result.action !== 'unchanged')
|
|
90
|
+
output.writeln(`policy projection: ${result.action} (${result.path})`);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
output.printWarning(`policy projection not synced: ${error.message}`);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const syncPolicySub = {
|
|
99
|
+
name: 'sync-policy',
|
|
100
|
+
description: 'Rewrite the Claude Code policy projection from .claude-flow/policy/state.json',
|
|
101
|
+
options: [rootOption],
|
|
102
|
+
action: async (ctx) => {
|
|
103
|
+
const ok = await syncPolicy(projectRoot(ctx), false);
|
|
104
|
+
return { success: ok, exitCode: ok ? 0 : 1 };
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const statusSub = findingsCommand('status', 'Show whether the mod is enabled, can load, and what it owns');
|
|
108
|
+
export const modsCommand = {
|
|
109
|
+
name: 'mods',
|
|
110
|
+
description: 'Run ruflo as a Claude Code mod (function hooks, early access, ADR-404)',
|
|
111
|
+
subcommands: [
|
|
112
|
+
installSub,
|
|
113
|
+
uninstallSub,
|
|
114
|
+
statusSub,
|
|
115
|
+
findingsCommand('doctor', 'Check the mod path; exits 1 only on a failure'),
|
|
116
|
+
syncPolicySub,
|
|
117
|
+
],
|
|
118
|
+
examples: [
|
|
119
|
+
{ command: 'ruflo mods install', description: 'Enable for this project (settings.local.json)' },
|
|
120
|
+
{ command: 'ruflo mods doctor', description: 'Function hooks on? Refused by policy? Handshake supported?' },
|
|
121
|
+
{ command: 'ruflo mods uninstall', description: 'Remove only what install added' },
|
|
122
|
+
],
|
|
123
|
+
action: statusSub.action,
|
|
124
|
+
};
|
|
125
|
+
export default modsCommand;
|
|
126
|
+
//# sourceMappingURL=mods.js.map
|
|
@@ -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'));
|
package/dist/src/index.js
CHANGED
|
@@ -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(),
|