@claude-flow/cli 3.56.6 → 3.56.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +109 -10
  2. package/.claude/helpers/helpers.manifest.json +4 -4
  3. package/.claude/helpers/intelligence.cjs +57 -4
  4. package/bin/cli.js +11 -1
  5. package/bin/mcp-server.js +11 -1
  6. package/catalog-manifest.json +2 -2
  7. package/dist/src/commands/doctor-native-addon.d.ts +23 -0
  8. package/dist/src/commands/doctor-native-addon.js +135 -0
  9. package/dist/src/commands/doctor.js +3 -0
  10. package/dist/src/commands/hooks.js +8 -1
  11. package/dist/src/commands/index.js +2 -0
  12. package/dist/src/commands/init.d.ts +2 -0
  13. package/dist/src/commands/init.js +37 -5
  14. package/dist/src/commands/mods.d.ts +3 -0
  15. package/dist/src/commands/mods.js +19 -5
  16. package/dist/src/commands/tui.d.ts +12 -0
  17. package/dist/src/commands/tui.js +98 -0
  18. package/dist/src/fs-secure.d.ts +25 -0
  19. package/dist/src/fs-secure.js +51 -2
  20. package/dist/src/init/executor.js +11 -14
  21. package/dist/src/init/helper-commands.d.ts +2 -0
  22. package/dist/src/init/helper-commands.js +2 -0
  23. package/dist/src/init/helpers-generator.js +4 -1
  24. package/dist/src/init/settings-generator.js +3 -3
  25. package/dist/src/mcp-server.js +10 -2
  26. package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
  27. package/dist/src/mcp-tools/capability-brain.js +1 -1
  28. package/dist/src/mcp-tools/hooks-tools.js +26 -4
  29. package/dist/src/mcp-tools/memory-tools.js +2 -1
  30. package/dist/src/memory/live-memory-row.d.ts +3 -1
  31. package/dist/src/memory/live-memory-row.js +3 -1
  32. package/dist/src/memory/memory-bridge.d.ts +4 -0
  33. package/dist/src/memory/memory-bridge.js +3 -2
  34. package/dist/src/memory/memory-initializer.d.ts +4 -0
  35. package/dist/src/memory/memory-initializer.js +12 -11
  36. package/dist/src/mods/claude-installs.js +7 -3
  37. package/dist/src/mods/install.js +32 -12
  38. package/dist/src/mods/plugin-resolve.js +3 -1
  39. package/dist/src/mods/probe.js +92 -4
  40. package/dist/src/services/command-recording.d.ts +17 -0
  41. package/dist/src/services/command-recording.js +91 -0
  42. package/node_modules/@claude-flow/codex/README.md +20 -3
  43. package/node_modules/@claude-flow/codex/dist/cli.js +7 -0
  44. package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
  45. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  46. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  47. package/node_modules/@claude-flow/codex/dist/initializer.d.ts +8 -3
  48. package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
  49. package/node_modules/@claude-flow/codex/dist/initializer.js +75 -13
  50. package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
  51. package/node_modules/@claude-flow/codex/dist/types.d.ts +8 -0
  52. package/node_modules/@claude-flow/codex/dist/types.d.ts.map +1 -1
  53. package/node_modules/@claude-flow/codex/package.json +1 -1
  54. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  55. package/package.json +2 -2
  56. package/plugins/ruflo-console/dist/tui.mjs +271 -0
@@ -76,6 +76,15 @@ async function resolveCodexInitializer(cwd) {
76
76
  }
77
77
  return undefined;
78
78
  }
79
+ // Trust level for a newly generated .codex/config.toml. Safe by default: only
80
+ // an explicit `--codex-trust full` (or RUFLO_CODEX_TRUST=full, read by the
81
+ // adapter) restores danger-full-access. Anything else is rejected here.
82
+ export function readCodexTrustFlag(flags) {
83
+ const raw = flags.codexTrust ?? flags['codex-trust'];
84
+ if (raw === undefined || raw === null || raw === false || raw === '')
85
+ return undefined;
86
+ return raw === 'standard' || raw === 'full' ? raw : null;
87
+ }
79
88
  // Keep Codex out of the CLI dependency graph so cold `npx ruflo --version`
80
89
  // remains fast (#2561). An explicit `init --codex` may fetch the small,
81
90
  // stable adapter on demand when it is not already installed by an umbrella
@@ -89,6 +98,7 @@ export function runCodexInitializerCli(cwd, options) {
89
98
  options.template,
90
99
  ...(options.force ? ['--force'] : []),
91
100
  ...(options.dual ? ['--dual'] : []),
101
+ ...(options.codexTrust ? ['--codex-trust', options.codexTrust] : []),
92
102
  ];
93
103
  const result = process.platform === 'win32'
94
104
  ? spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', ['npx', ...npxArgs].join(' ')], { cwd, stdio: 'inherit', windowsHide: true })
@@ -106,6 +116,7 @@ export function runCodexInitializerCli(cwd, options) {
106
116
  // init) and scripted `--format json` output.
107
117
  async function maybeAutoDetectCodex(ctx, options) {
108
118
  try {
119
+ const codexTrust = readCodexTrustFlag(ctx.flags) ?? undefined;
109
120
  // The CLI parser turns `--no-codex-detect` into `codexDetect: false`.
110
121
  // Retain the literal key for programmatic CommandContext callers.
111
122
  if (ctx.flags.codexDetect === false || ctx.flags['no-codex-detect'] === true)
@@ -127,6 +138,7 @@ async function maybeAutoDetectCodex(ctx, options) {
127
138
  template: (options.minimal ? 'minimal' : options.full ? 'full' : 'default'),
128
139
  force: options.force,
129
140
  dual: false, // Claude Code files were already written by the main init flow above
141
+ codexTrust,
130
142
  });
131
143
  if (!result.success)
132
144
  return; // best-effort — never fail the primary init over this
@@ -352,6 +364,11 @@ async function maybeInstallSkillsSh(ctx) {
352
364
  // Codex initialization action
353
365
  async function initCodexAction(ctx, options) {
354
366
  const { force, minimal, full, dualMode } = options;
367
+ const codexTrust = readCodexTrustFlag(ctx.flags);
368
+ if (codexTrust === null) {
369
+ output.printError(`Invalid --codex-trust: ${String(ctx.flags.codexTrust ?? ctx.flags['codex-trust'])} (valid: standard, full)`);
370
+ return { success: false, exitCode: 1 };
371
+ }
355
372
  output.writeln();
356
373
  output.writeln(output.bold('Initializing RuFlo V3 for OpenAI Codex'));
357
374
  output.writeln();
@@ -364,7 +381,7 @@ async function initCodexAction(ctx, options) {
364
381
  if (!CodexInitializer) {
365
382
  spinner.stop();
366
383
  output.printInfo('Fetching the stable Codex adapter for this initialization...');
367
- const success = runCodexInitializerCli(ctx.cwd, { template, force, dual: dualMode });
384
+ const success = runCodexInitializerCli(ctx.cwd, { template, force, dual: dualMode, codexTrust });
368
385
  if (!success) {
369
386
  output.printError('Codex initialization failed while running @claude-flow/codex@latest.');
370
387
  return { success: false, exitCode: 1 };
@@ -377,6 +394,7 @@ async function initCodexAction(ctx, options) {
377
394
  template: template,
378
395
  force,
379
396
  dual: dualMode,
397
+ codexTrust,
380
398
  });
381
399
  if (!result.success) {
382
400
  spinner.fail('Codex initialization failed');
@@ -404,14 +422,23 @@ async function initCodexAction(ctx, options) {
404
422
  dualMode ? `CLAUDE.md: Claude Code compatibility` : '',
405
423
  ].filter(Boolean).join('\n'), 'OpenAI Codex Integration');
406
424
  output.writeln();
425
+ // The trust level of .codex/config.toml is always shown (never truncated
426
+ // behind other warnings): it decides what Codex may do without asking.
427
+ const isTrustNote = (w) => w.startsWith('.codex/config.toml');
428
+ const trustNotes = (result.warnings ?? []).filter(isTrustNote);
429
+ for (const note of trustNotes)
430
+ output.printInfo(note);
431
+ if (trustNotes.length > 0)
432
+ output.writeln();
407
433
  // Warnings
408
- if (result.warnings && result.warnings.length > 0) {
434
+ const otherWarnings = (result.warnings ?? []).filter((w) => !isTrustNote(w));
435
+ if (otherWarnings.length > 0) {
409
436
  output.printWarning('Warnings:');
410
- for (const warning of result.warnings.slice(0, 5)) {
437
+ for (const warning of otherWarnings.slice(0, 5)) {
411
438
  output.printInfo(` • ${warning}`);
412
439
  }
413
- if (result.warnings.length > 5) {
414
- output.printInfo(` ... and ${result.warnings.length - 5} more`);
440
+ if (otherWarnings.length > 5) {
441
+ output.printInfo(` ... and ${otherWarnings.length - 5} more`);
415
442
  }
416
443
  output.writeln();
417
444
  }
@@ -1577,6 +1604,11 @@ export const initCommand = {
1577
1604
  type: 'boolean',
1578
1605
  default: false,
1579
1606
  },
1607
+ {
1608
+ name: 'codex-trust',
1609
+ description: 'Trust level of the generated .codex/config.toml: standard (default: workspace-write sandbox, on-request approvals, cached web search) or full (danger-full-access, no approvals, live web search). An existing file is never rewritten',
1610
+ type: 'string',
1611
+ },
1580
1612
  {
1581
1613
  name: 'no-codex-detect',
1582
1614
  description: 'Skip auto-detecting the OpenAI Codex CLI and configuring its MCP server + skills',
@@ -8,6 +8,9 @@
8
8
  * are never removed: they stay the default and the fallback.
9
9
  */
10
10
  import type { Command } from '../types.js';
11
+ import { type Finding } from '../mods/probe.js';
12
+ /** The doctor's exit gate: a failure always fails; with --strict a warning does too. */
13
+ export declare function hasFailureOrStrictWarning(findings: readonly Finding[], strict: boolean): boolean;
11
14
  export declare const modsCommand: Command;
12
15
  export default modsCommand;
13
16
  //# sourceMappingURL=mods.d.ts.map
@@ -105,19 +105,32 @@ const uninstallSub = {
105
105
  return { success: result.failed.length === 0, exitCode: result.failed.length ? 1 : 0, data: result };
106
106
  },
107
107
  };
108
+ /** The doctor's exit gate: a failure always fails; with --strict a warning does too. */
109
+ export function hasFailureOrStrictWarning(findings, strict) {
110
+ return findings.some((f) => f.status === 'fail' || (strict && f.status === 'warn'));
111
+ }
108
112
  function findingsCommand(name, description) {
109
113
  return {
110
114
  name,
111
115
  description,
112
- options: [rootOption, { name: 'json', description: 'Output as JSON', type: 'boolean', default: false }],
116
+ options: [
117
+ rootOption,
118
+ { name: 'json', description: 'Output as JSON', type: 'boolean', default: false },
119
+ ...(name === 'doctor' ? [{ name: 'strict', description: 'Exit 1 on a warning too (a mod that is not fully loaded is a failure for a CI gate)', type: 'boolean', default: false }] : []),
120
+ ],
113
121
  action: async (ctx) => {
114
122
  const findings = probeMods({ projectRoot: projectRoot(ctx) });
115
123
  if (ctx.flags.json)
116
124
  output.printJson(findings);
117
125
  else
118
126
  printFindings(findings);
119
- const failed = findings.some((f) => f.status === 'fail');
120
- // status reports (exit 0); doctor gates on a failure (warnings are expected while early access is off).
127
+ const strict = name === 'doctor' && ctx.flags.strict === true;
128
+ const failed = hasFailureOrStrictWarning(findings, strict);
129
+ if (failed && !ctx.flags.json) {
130
+ const first = findings.find((f) => f.status === 'fail') ?? findings.find((f) => f.status === 'warn');
131
+ output.writeln(output.error(`doctor: ${findings.filter((f) => f.status === 'fail').length} failure(s), ${findings.filter((f) => f.status === 'warn').length} warning(s)${first?.fix ? `; start with: ${first.fix}` : ''}`));
132
+ }
133
+ // status reports (exit 0); doctor gates on a failure, and with --strict on a warning.
121
134
  return name === 'doctor' && failed ? { success: false, exitCode: 1, data: findings } : { success: true, data: findings };
122
135
  },
123
136
  };
@@ -153,7 +166,7 @@ export const modsCommand = {
153
166
  installSub,
154
167
  uninstallSub,
155
168
  statusSub,
156
- findingsCommand('doctor', 'Check the mod path; exits 1 only on a failure'),
169
+ findingsCommand('doctor', 'Check the mod path; exits 1 on a failure (--strict: on a warning too)'),
157
170
  syncPolicySub,
158
171
  ],
159
172
  examples: [
@@ -161,7 +174,8 @@ export const modsCommand = {
161
174
  { command: 'ruflo mods install --scope project', description: 'Same, in the committed settings.json (what ruflo init does)' },
162
175
  { command: 'ruflo mods install --no-plugin-install', description: 'Only write settings; run no claude command' },
163
176
  { command: 'ruflo mods install --source local', description: 'Dogfood: load the mods live from this ruflo checkout (directory marketplace)' },
164
- { command: 'ruflo mods doctor', description: 'Marketplace fresh? Plugins loadable? Function hooks on? Refused by policy?' },
177
+ { command: 'ruflo mods doctor', description: 'Marketplace fresh? Plugins loadable? Function hooks on? Refused by policy? Each problem prints a one-line fix' },
178
+ { command: 'ruflo mods doctor --strict', description: 'Same, and exit 1 when anything is not fully working (for CI)' },
165
179
  { command: 'ruflo mods uninstall', description: 'Remove only what install added' },
166
180
  ],
167
181
  action: statusSub.action,
@@ -0,0 +1,12 @@
1
+ import type { Command } from '../types.js';
2
+ /** `<own install>/plugins/ruflo-console/dist/tui.mjs`, the first that exists. */
3
+ export declare function locateTuiBundle(startDir?: string): string | null;
4
+ /** The AI page's settings from the flags (and one environment variable for the endpoint list). Absent flags leave the defaults; a bad number is ignored, never passed on. */
5
+ export declare function aiPage(flags: Record<string, unknown> | undefined): {
6
+ model?: string;
7
+ maxSpendUsd?: number;
8
+ convoTargets?: string;
9
+ };
10
+ export declare const tuiCommand: Command;
11
+ export default tuiCommand;
12
+ //# sourceMappingURL=tui.d.ts.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `ruflo tui` — the ruflo console in a terminal of its own (ADR-489).
3
+ *
4
+ * Runs the same views the Claude Code mod (plugins/ruflo-console) draws, over a Node host instead of the mod host. Read-only in
5
+ * the pages: Overview, Cost, Swarm, Events and Missions are read-only; AI (key 6) asks Codex (read-only, the pinned 0.162.0) or OpenRouter
6
+ * (https, openrouter.ai or a convoTargets host), and every question shows a card first. The bundle is built from the plugin's source (scripts/build-tui.mjs) and loaded from this
7
+ * CLI's OWN install only (never from the working directory: opening a repository must not run code that repository supplies).
8
+ * No bundle is a message and exit 1, never a crash; ruflo stays operational without it. Without a terminal (a pipe, CI) the live view cannot run:
9
+ * a one-line hint and exit 2, unless --plain asks for one frame as plain text and exit 0 (ADR-489 decision E).
10
+ *
11
+ * npx ruflo tui # interactive, in a terminal
12
+ * npx ruflo tui --view swarm # start on the Swarm page (keys 1-5 or o c s e m switch pages)
13
+ * npx ruflo tui --plain | head # one plain frame, exit 0 (scripts, CI)
14
+ * npx ruflo tui | cat # not a terminal and no --plain: a hint on stderr, exit 2
15
+ */
16
+ import { existsSync } from 'node:fs';
17
+ import { dirname, join } from 'node:path';
18
+ import { fileURLToPath, pathToFileURL } from 'node:url';
19
+ import { output } from '../output.js';
20
+ import { ownPluginDirs } from '../plugins/own-install.js';
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ const VIEWS = ['overview', 'cost', 'swarm', 'events', 'missions', 'ai'];
23
+ /** `<own install>/plugins/ruflo-console/dist/tui.mjs`, the first that exists. */
24
+ export function locateTuiBundle(startDir = __dirname) {
25
+ for (const dir of ownPluginDirs(startDir, 'ruflo-console', 'dist')) {
26
+ const file = join(dir, 'tui.mjs');
27
+ if (existsSync(file))
28
+ return file;
29
+ }
30
+ return null;
31
+ }
32
+ /** The running CLI's entry file, so the console's read-only probes run this CLI and never download another. */
33
+ function cliEntry() {
34
+ const entry = join(__dirname, '..', '..', '..', 'bin', 'cli.js');
35
+ return existsSync(entry) ? entry : undefined;
36
+ }
37
+ /** The AI page's settings from the flags (and one environment variable for the endpoint list). Absent flags leave the defaults; a bad number is ignored, never passed on. */
38
+ export function aiPage(flags) {
39
+ const model = typeof flags?.model === 'string' && flags.model !== '' ? flags.model : undefined;
40
+ const cap = flags?.maxSpendUsd ?? flags?.['max-spend-usd'];
41
+ const maxSpendUsd = typeof cap === 'number' && Number.isFinite(cap) && cap >= 0 ? cap : typeof cap === 'string' && /^\d+(\.\d+)?$/.test(cap) ? Number(cap) : undefined;
42
+ const targets = flags?.convoTargets ?? flags?.['convo-targets'] ?? process.env.RUFLO_TUI_CONVO_TARGETS;
43
+ return { ...(model !== undefined && { model }), ...(maxSpendUsd !== undefined && { maxSpendUsd }), ...(typeof targets === 'string' && targets !== '' && { convoTargets: targets.slice(0, 2000) }) };
44
+ }
45
+ export const tuiCommand = {
46
+ name: 'tui',
47
+ description: 'The ruflo console in your terminal: Overview, Cost, Swarm, Events, Missions (read-only) and an AI page that asks Codex or OpenRouter, each question after a card.',
48
+ options: [
49
+ { name: 'view', description: `Page to open: ${VIEWS.join(' | ')}`, type: 'string', default: 'overview' },
50
+ { name: 'ai', description: 'The AI page (ask Codex read-only or OpenRouter, each question after a confirm card). --no-ai turns it off', type: 'boolean', default: true },
51
+ { name: 'model', description: 'OpenRouter model for this session (default openrouter/auto)', type: 'string' },
52
+ { name: 'max-spend-usd', description: 'Stop sending to OpenRouter once the provider-reported spend reaches this (default 0.5)', type: 'number' },
53
+ { name: 'convo-targets', description: 'Extra OpenAI-compatible endpoints, the console convoTargets syntax: endpoint:<name>=<https-base-url>|<KEY_ENV>|<model>;... (a key is named by its environment variable). Also RUFLO_TUI_CONVO_TARGETS', type: 'string' },
54
+ { name: 'plain', description: 'Print one frame as plain text and exit 0 (scripts and CI). Without it, a non-terminal gets a hint and exit 2', type: 'boolean', default: false },
55
+ ],
56
+ examples: [
57
+ { command: 'npx ruflo tui', description: 'Open the console in this terminal' },
58
+ { command: 'npx ruflo tui --view swarm', description: 'Start on the Swarm page (keys 1-5 switch pages, tab cycles, q quits)' },
59
+ { command: 'npx ruflo tui --view ai --model openai/gpt-4o-mini --max-spend-usd 0.1', description: 'The AI page on a cheap OpenRouter model, stopping at $0.10 reported (key in $OPENROUTER_API_KEY, never shown)' },
60
+ { command: 'npx ruflo tui --plain | head -40', description: 'One plain frame, exit 0: for scripts and CI (no terminal needed)' },
61
+ ],
62
+ async action(context) {
63
+ const ctx = context;
64
+ const wanted = ctx.flags?.view ?? ctx.args?.find(arg => !arg.startsWith('-')) ?? 'overview';
65
+ if (!VIEWS.includes(wanted)) {
66
+ output.writeln(output.error(`tui: unknown view "${String(wanted).slice(0, 40)}". Use one of: ${VIEWS.join(', ')}`));
67
+ return { success: false, exitCode: 2, data: { error: 'unknown-view' } };
68
+ }
69
+ const bundle = locateTuiBundle();
70
+ if (bundle === null) {
71
+ output.writeln(output.warning('tui: the terminal console is not available in this install (plugins/ruflo-console/dist/tui.mjs not found).'));
72
+ output.writeln('From a ruflo checkout: node plugins/ruflo-console/scripts/build-tui.mjs');
73
+ return { success: false, exitCode: 1, data: { error: 'bundle-missing' } };
74
+ }
75
+ let mod;
76
+ try {
77
+ mod = (await import(pathToFileURL(bundle).href));
78
+ }
79
+ catch (error) {
80
+ output.writeln(output.error(`tui: could not load the console bundle: ${error instanceof Error ? error.message.slice(0, 160) : 'unknown error'}`));
81
+ return { success: false, exitCode: 1, data: { error: 'bundle-load-failed' } };
82
+ }
83
+ const stop = new AbortController();
84
+ const onSignal = () => stop.abort();
85
+ process.once('SIGTERM', onSignal);
86
+ process.once('SIGHUP', onSignal);
87
+ try {
88
+ const code = await mod.runTui({ cwd: ctx.cwd ?? process.cwd(), view: wanted, ...(ctx.flags?.plain === true && { plain: true }), ...(ctx.flags?.ai !== false && { aiPage: aiPage(ctx.flags) }), ...(cliEntry() !== undefined && { cliEntry: cliEntry() }), ...(process.env.CLAUDE_CONFIG_DIR !== undefined && { configDir: process.env.CLAUDE_CONFIG_DIR }) }, { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, env: process.env, signal: stop.signal });
89
+ return { success: code === 0, exitCode: code };
90
+ }
91
+ finally {
92
+ process.off('SIGTERM', onSignal);
93
+ process.off('SIGHUP', onSignal);
94
+ }
95
+ },
96
+ };
97
+ export default tuiCommand;
98
+ //# sourceMappingURL=tui.js.map
@@ -66,6 +66,31 @@ export interface WriteOptions {
66
66
  * writeFileRestricted(path, data, { encrypt: true }) // opt-in encryption
67
67
  */
68
68
  export declare function writeFileRestricted(path: string, data: string | Buffer, optsOrEncoding?: BufferEncoding | WriteOptions): void;
69
+ /**
70
+ * The at-rest format of an existing database file: `true` = RFE1-encrypted,
71
+ * `false` = plaintext, `null` = missing or unreadable (nothing to preserve).
72
+ * Reads four bytes only.
73
+ */
74
+ export declare function existingFileIsEncrypted(path: string): boolean | null;
75
+ /**
76
+ * Write a whole-file database image back in the format it was read in (#4057).
77
+ *
78
+ * A read-modify-write of a database image (sql.js loads the file, mutates it,
79
+ * exports it) must not change what the file IS. With encryption at rest on, a
80
+ * plain `{ encrypt: true }` write turned a plaintext native SQLite store into
81
+ * an RFE1 blob that the native bridge and every SQLite reader then rejected,
82
+ * while the command reported success. And the reverse, a plain
83
+ * `writeFileAtomic` of a decrypted image, silently downgraded an encrypted
84
+ * store to plaintext.
85
+ *
86
+ * existing file RFE1 -> RFE1 (encrypted)
87
+ * existing file plaintext -> plaintext
88
+ * no existing file -> `encrypt` (default true: the env gate decides)
89
+ * `encrypt: false` -> plaintext, always (explicit caller choice)
90
+ */
91
+ export declare function writeDbImagePreservingFormat(path: string, data: Uint8Array | Buffer, opts?: {
92
+ encrypt?: boolean;
93
+ }): void;
69
94
  /**
70
95
  * Read a file and transparently decrypt if it carries the RFE1 magic.
71
96
  *
@@ -16,9 +16,9 @@
16
16
  * sniff so legacy plaintext files keep working unchanged during the
17
17
  * incremental migration.
18
18
  */
19
- import { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from 'node:fs';
19
+ import { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, renameSync, unlinkSync, writeSync, } from 'node:fs';
20
20
  import { basename, dirname, join } from 'node:path';
21
- import { decryptBuffer, encryptBuffer, getKey, isEncryptedBlob, isEncryptionEnabled, } from './encryption/vault.js';
21
+ import { decryptBuffer, encryptBuffer, getKey, isEncryptedBlob, isEncryptionEnabled, MAGIC, } from './encryption/vault.js';
22
22
  /**
23
23
  * Crash-safe atomic file write (issue #2584).
24
24
  *
@@ -119,6 +119,55 @@ export function writeFileRestricted(path, data, optsOrEncoding = 'utf-8') {
119
119
  const buf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, encoding);
120
120
  writeFileAtomic(path, buf, { mode: 0o600 });
121
121
  }
122
+ /**
123
+ * The at-rest format of an existing database file: `true` = RFE1-encrypted,
124
+ * `false` = plaintext, `null` = missing or unreadable (nothing to preserve).
125
+ * Reads four bytes only.
126
+ */
127
+ export function existingFileIsEncrypted(path) {
128
+ let fd;
129
+ try {
130
+ fd = openSync(path, 'r');
131
+ const head = Buffer.alloc(4);
132
+ const n = readSync(fd, head, 0, 4, 0);
133
+ if (n < 4)
134
+ return null;
135
+ return head.equals(MAGIC);
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ finally {
141
+ if (fd !== undefined) {
142
+ try {
143
+ closeSync(fd);
144
+ }
145
+ catch { /* best effort */ }
146
+ }
147
+ }
148
+ }
149
+ /**
150
+ * Write a whole-file database image back in the format it was read in (#4057).
151
+ *
152
+ * A read-modify-write of a database image (sql.js loads the file, mutates it,
153
+ * exports it) must not change what the file IS. With encryption at rest on, a
154
+ * plain `{ encrypt: true }` write turned a plaintext native SQLite store into
155
+ * an RFE1 blob that the native bridge and every SQLite reader then rejected,
156
+ * while the command reported success. And the reverse, a plain
157
+ * `writeFileAtomic` of a decrypted image, silently downgraded an encrypted
158
+ * store to plaintext.
159
+ *
160
+ * existing file RFE1 -> RFE1 (encrypted)
161
+ * existing file plaintext -> plaintext
162
+ * no existing file -> `encrypt` (default true: the env gate decides)
163
+ * `encrypt: false` -> plaintext, always (explicit caller choice)
164
+ */
165
+ export function writeDbImagePreservingFormat(path, data, opts = {}) {
166
+ const requested = opts.encrypt ?? true;
167
+ const existing = existingFileIsEncrypted(path);
168
+ const encrypt = requested === false ? false : (existing ?? requested);
169
+ writeFileRestricted(path, Buffer.isBuffer(data) ? data : Buffer.from(data), { encrypt });
170
+ }
122
171
  export function readFileMaybeEncrypted(path, encoding = 'utf-8') {
123
172
  const raw = readFileSync(path);
124
173
  let plain;
@@ -20,7 +20,7 @@ import { generateClaudeMd } from './claudemd-generator.js';
20
20
  import { recordMemoryPackagePath } from './memory-package-resolver.js';
21
21
  import { ensureCommonJsCompanions } from './helper-companions.js';
22
22
  import { scanSettingsForRisk, formatRiskFindingsAsWarnings } from './settings-risk-scanner.js';
23
- import { helperHookCommand, helperScopeFor, helperStatusLineCommand, pinToUserHelpers } from './helper-commands.js';
23
+ import { AUTO_MEMORY_HOOK_TIMEOUT_S, helperHookCommand, helperScopeFor, helperStatusLineCommand, pinToUserHelpers } from './helper-commands.js';
24
24
  /**
25
25
  * Skills to copy based on configuration
26
26
  */
@@ -307,18 +307,15 @@ export function mergeSettingsForUpgrade(existing, targetDir) {
307
307
  // 2. Merge hooks (preserve existing, add new Agent Teams + auto-memory hooks)
308
308
  const existingHooks = existing.hooks || {};
309
309
  merged.hooks = { ...existingHooks };
310
- // Cross-platform auto-memory hook commands that resolve paths via git root.
311
- // Uses node -e with git rev-parse so hooks work regardless of CWD (#1259, #1284).
312
- const gitRootResolver = "var c=require('child_process'),p=require('path'),u=require('url'),r;"
313
- + "try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}"
314
- + 'catch(e){r=process.cwd()}';
310
+ // Auto-memory hook commands. They go through helperHookCommand like every other
311
+ // helper hook (#4058): the previous `node -e "…import(file)" import` form left the
312
+ // subcommand in process.argv[1], the helper read argv[2], and both hooks only
313
+ // printed the status banner. Hooks run in the project directory with
314
+ // CLAUDE_PROJECT_DIR set, so the shared probe resolves the helper without a
315
+ // `git rev-parse` of its own.
315
316
  const autoMemoryScript = '.claude/helpers/auto-memory-hook.mjs';
316
- const autoMemoryImportCmd = scope === 'user'
317
- ? helperHookCommand(autoMemoryScript, 'import', 'user')
318
- : `node -e "${gitRootResolver}var f=p.join(r,'${autoMemoryScript}');import(u.pathToFileURL(f).href)" import`;
319
- const autoMemorySyncCmd = scope === 'user'
320
- ? helperHookCommand(autoMemoryScript, 'sync', 'user')
321
- : `node -e "${gitRootResolver}var f=p.join(r,'${autoMemoryScript}');import(u.pathToFileURL(f).href)" sync`;
317
+ const autoMemoryImportCmd = helperHookCommand(autoMemoryScript, 'import', scope);
318
+ const autoMemorySyncCmd = helperHookCommand(autoMemoryScript, 'sync', scope);
322
319
  // Add auto-memory import to SessionStart (if not already present)
323
320
  const sessionStartHooks = existingHooks.SessionStart;
324
321
  const hasAutoMemoryImport = sessionStartHooks?.some(group => group.hooks?.some(h => h.command?.includes('auto-memory-hook')));
@@ -333,7 +330,7 @@ export function mergeSettingsForUpgrade(existing, targetDir) {
333
330
  startGroup.hooks.push({
334
331
  type: 'command',
335
332
  command: autoMemoryImportCmd,
336
- timeout: 6000,
333
+ timeout: AUTO_MEMORY_HOOK_TIMEOUT_S,
337
334
  continueOnError: true,
338
335
  });
339
336
  }
@@ -352,7 +349,7 @@ export function mergeSettingsForUpgrade(existing, targetDir) {
352
349
  endGroup.hooks.unshift({
353
350
  type: 'command',
354
351
  command: autoMemorySyncCmd,
355
- timeout: 8000,
352
+ timeout: AUTO_MEMORY_HOOK_TIMEOUT_S,
356
353
  continueOnError: true,
357
354
  });
358
355
  }
@@ -1,4 +1,6 @@
1
1
  export type HelperScope = 'project' | 'user';
2
+ /** Claude Code reads a hook's `timeout` in SECONDS; the auto-memory hooks wrote 6000-10000, i.e. up to 100 minutes (#4058). */
3
+ export declare const AUTO_MEMORY_HOOK_TIMEOUT_S = 30;
2
4
  /** Settings written into the user's home directory are user-level settings. */
3
5
  export declare function helperScopeFor(targetDir: string | undefined): HelperScope;
4
6
  /** Command that runs `node <script> <subcommand>` for a hook. */
@@ -14,6 +14,8 @@
14
14
  import * as fs from 'fs';
15
15
  import * as os from 'os';
16
16
  import * as path from 'path';
17
+ /** Claude Code reads a hook's `timeout` in SECONDS; the auto-memory hooks wrote 6000-10000, i.e. up to 100 minutes (#4058). */
18
+ export const AUTO_MEMORY_HOOK_TIMEOUT_S = 30;
17
19
  const IS_WINDOWS = process.platform === 'win32';
18
20
  const STATUSLINE_SCRIPT = '.claude/helpers/statusline.cjs';
19
21
  function canonical(dir) {
@@ -1306,7 +1306,10 @@ function doStatus() {
1306
1306
  // Suppress unhandled rejection warnings from dynamic import() failures
1307
1307
  process.on('unhandledRejection', () => {});
1308
1308
 
1309
- const command = process.argv[2] || 'status';
1309
+ // Older settings.json files ran node -e with the helper imported, which puts the
1310
+ // subcommand in argv[1] (#4058); accept that shape as well as script import.
1311
+ const COMMANDS = ['import', 'sync', 'status'];
1312
+ const command = process.argv[2] || (COMMANDS.includes(process.argv[1]) ? process.argv[1] : 'status');
1310
1313
 
1311
1314
  try {
1312
1315
  switch (command) {
@@ -3,7 +3,7 @@
3
3
  * Creates .claude/settings.json with V3-optimized hook configurations
4
4
  */
5
5
  import { detectPlatform } from './types.js';
6
- import { helperHookCommand, helperScopeFor, helperStatusLineCommand } from './helper-commands.js';
6
+ import { AUTO_MEMORY_HOOK_TIMEOUT_S, helperHookCommand, helperScopeFor, helperStatusLineCommand } from './helper-commands.js';
7
7
  /**
8
8
  * Generate the complete settings.json content
9
9
  */
@@ -278,7 +278,7 @@ function generateHooksConfig(config, scope) {
278
278
  {
279
279
  type: 'command',
280
280
  command: autoMemoryCmd('import', scope),
281
- timeout: 8000,
281
+ timeout: AUTO_MEMORY_HOOK_TIMEOUT_S,
282
282
  },
283
283
  ],
284
284
  },
@@ -306,7 +306,7 @@ function generateHooksConfig(config, scope) {
306
306
  {
307
307
  type: 'command',
308
308
  command: autoMemoryCmd('sync', scope),
309
- timeout: 10000,
309
+ timeout: AUTO_MEMORY_HOOK_TIMEOUT_S,
310
310
  },
311
311
  ],
312
312
  },
@@ -619,7 +619,7 @@ export class MCPServerManager extends EventEmitter {
619
619
  version: VERSION,
620
620
  capabilities: {
621
621
  tools: { listChanged: true },
622
- resources: { subscribe: true, listChanged: true },
622
+ resources: {},
623
623
  },
624
624
  },
625
625
  },
@@ -696,7 +696,7 @@ export class MCPServerManager extends EventEmitter {
696
696
  serverInfo: { name: 'ruflo', version: '3.0.0' },
697
697
  capabilities: {
698
698
  tools: { listChanged: true },
699
- resources: { subscribe: true, listChanged: true },
699
+ resources: {},
700
700
  },
701
701
  },
702
702
  };
@@ -755,6 +755,14 @@ export class MCPServerManager extends EventEmitter {
755
755
  },
756
756
  };
757
757
  }
758
+ // The server advertises `resources` (so clients such as Codex offer list_mcp_resources), and a declared capability must
759
+ // be served: it exposes no resources, so the answers are empty, and a read is the spec's "resource not found" (ADR-490).
760
+ case 'resources/list':
761
+ return { jsonrpc: '2.0', id: message.id, result: { resources: [] } };
762
+ case 'resources/templates/list':
763
+ return { jsonrpc: '2.0', id: message.id, result: { resourceTemplates: [] } };
764
+ case 'resources/read':
765
+ return { jsonrpc: '2.0', id: message.id, error: { code: -32002, message: 'Resource not found' } };
758
766
  case 'notifications/initialized':
759
767
  // Client notification - no response needed
760
768
  console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Client initialized`);
@@ -97,7 +97,7 @@ export interface CapabilityBrain {
97
97
  };
98
98
  }
99
99
  /** Top-level commands from the CLI command loader. Kept as a checked manifest. */
100
- export declare const RUFLO_CLI_COMMANDS: readonly ["init", "start", "status", "task", "session", "agent", "swarm", "memory", "mcp", "config", "migrate", "hooks", "workflow", "hive-mind", "process", "daemon", "version", "neural", "security", "performance", "providers", "plugins", "deployment", "claims", "policy", "embeddings", "completions", "doctor", "verify", "analyze", "route", "progress", "issues", "update", "ruvector", "benchmark", "guidance", "appliance", "appliance-advanced", "transfer-store", "cleanup", "autopilot", "gaia-bench", "metaharness", "eject", "funnel", "settings", "auth", "proxy", "advisor", "spinner", "announcements"];
100
+ export declare const RUFLO_CLI_COMMANDS: readonly ["init", "start", "status", "task", "session", "agent", "swarm", "memory", "mcp", "config", "migrate", "hooks", "workflow", "hive-mind", "process", "daemon", "version", "neural", "security", "performance", "providers", "plugins", "deployment", "claims", "policy", "embeddings", "completions", "doctor", "verify", "analyze", "route", "progress", "issues", "update", "ruvector", "benchmark", "guidance", "appliance", "appliance-advanced", "transfer-store", "cleanup", "autopilot", "gaia-bench", "metaharness", "eject", "funnel", "settings", "auth", "proxy", "advisor", "spinner", "announcements", "tui"];
101
101
  export interface ImplementationLoopStep {
102
102
  id: string;
103
103
  name: string;
@@ -16,7 +16,7 @@ export const RUFLO_CLI_COMMANDS = [
16
16
  'ruvector', 'benchmark', 'guidance', 'appliance', 'appliance-advanced',
17
17
  'transfer-store', 'cleanup', 'autopilot', 'gaia-bench', 'metaharness',
18
18
  'eject', 'funnel', 'settings', 'auth', 'proxy', 'advisor', 'spinner',
19
- 'announcements',
19
+ 'announcements', 'tui',
20
20
  ];
21
21
  export const IMPLEMENTATION_LOOP = [
22
22
  {
@@ -8,6 +8,7 @@ import * as nodeFs from 'fs';
8
8
  import { dirname, join, resolve } from 'path';
9
9
  import { getProjectCwd } from './types.js';
10
10
  import { validateIdentifier, validateText, validatePath } from './validate-input.js';
11
+ import { commandRecordingDisabled, redactCommandForStorage } from '../services/command-recording.js';
11
12
  import { checkCommandLoop, recordCommandOutcome } from './tool-loop-guardrail.js';
12
13
  import { buildLearnedRoutingPatterns, } from '../services/learned-routing.js';
13
14
  import { applyTypesafeRouting, getTypesafeRouter } from '../ruvector/typesafe-router.js';
@@ -1014,13 +1015,34 @@ export const hooksPostCommand = {
1014
1015
  // #6: feed the tool-loop circuit breaker so pre-command can warn/block on
1015
1016
  // repeated consecutive failures of the same command.
1016
1017
  recordCommandOutcome(command, success);
1018
+ // #4056: command history is opt-out (CLAUDE_FLOW_HOOKS_RECORD_COMMANDS=0,
1019
+ // CLAUDE_FLOW_HOOKS_ENABLED=false, or hooks.recordCommands=false in the
1020
+ // config). The circuit breaker above is in-memory only; nothing below
1021
+ // runs when recording is off, so no command text reaches the store or
1022
+ // the trajectory pipeline.
1023
+ if (commandRecordingDisabled(process.env, getProjectCwd())) {
1024
+ return {
1025
+ recorded: false,
1026
+ disabled: true,
1027
+ command: undefined,
1028
+ exitCode,
1029
+ success,
1030
+ timestamp: new Date().toISOString(),
1031
+ _storedIn: 'none',
1032
+ learningPath: 'recorded-only',
1033
+ trajectoriesDelta: 0,
1034
+ note: 'Command recording is switched off (CLAUDE_FLOW_HOOKS_RECORD_COMMANDS / CLAUDE_FLOW_HOOKS_ENABLED / hooks.recordCommands); nothing was stored.',
1035
+ };
1036
+ }
1037
+ // What is stored is the redacted, length-capped form, never the raw text.
1038
+ const storedCommand = redactCommandForStorage(command);
1017
1039
  // Persist command outcome via AgentDB
1018
1040
  let _storedIn = 'none';
1019
1041
  try {
1020
1042
  const bridge = await import('../memory/memory-bridge.js');
1021
1043
  const stored = await bridge.bridgeStoreEntry({
1022
1044
  key: `cmd-${Date.now()}`,
1023
- value: JSON.stringify({ command, exitCode, success }),
1045
+ value: JSON.stringify({ command: storedCommand, exitCode, success }),
1024
1046
  namespace: 'commands',
1025
1047
  tags: [success ? 'success' : 'error'],
1026
1048
  ttl,
@@ -1042,7 +1064,7 @@ export const hooksPostCommand = {
1042
1064
  }
1043
1065
  const key = `cmd-${now}`;
1044
1066
  store.entries[key] = {
1045
- key, value: JSON.stringify({ command, exitCode, success }), namespace: 'commands',
1067
+ key, value: JSON.stringify({ command: storedCommand, exitCode, success }), namespace: 'commands',
1046
1068
  storedAt: new Date(now).toISOString(), lastAccessed: new Date(now).toISOString(), accessCount: 0,
1047
1069
  expiresAt: new Date(now + ttl * 1000).toISOString(),
1048
1070
  };
@@ -1063,8 +1085,8 @@ export const hooksPostCommand = {
1063
1085
  const before = intel.getIntelligenceStats().trajectoriesRecorded;
1064
1086
  await intel.recordTrajectory([{
1065
1087
  type: 'action',
1066
- content: `Command \`${command.slice(0, 200)}\` exited ${exitCode} (${success ? 'success' : 'failure'})`,
1067
- metadata: { hook: 'post-command', command: command.slice(0, 500), exitCode, success },
1088
+ content: `Command \`${storedCommand.slice(0, 200)}\` exited ${exitCode} (${success ? 'success' : 'failure'})`,
1089
+ metadata: { hook: 'post-command', command: storedCommand.slice(0, 500), exitCode, success },
1068
1090
  timestamp: Date.now(),
1069
1091
  }], success ? 'success' : 'failure');
1070
1092
  trajectoriesDelta = intel.getIntelligenceStats().trajectoriesRecorded - before;
@@ -1402,7 +1402,8 @@ export const memoryTools = [
1402
1402
  if (!v.valid)
1403
1403
  throw new Error(v.error);
1404
1404
  }
1405
- const all = await listEntries({ limit: 100000, namespace });
1405
+ // #4056: TTL hides expired rows from normal reads, so the cleanup has to ask for them.
1406
+ const all = await listEntries({ limit: 100000, namespace, includeExpired: true });
1406
1407
  const now = Date.now();
1407
1408
  const expired = all.entries.filter(e => {
1408
1409
  const exp = e.expiresAt;