@claude-flow/cli 3.56.5 → 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.
- package/.claude/helpers/auto-memory-hook.mjs +109 -10
- package/.claude/helpers/helpers.manifest.json +5 -5
- package/.claude/helpers/hook-handler.cjs +198 -1
- package/.claude/helpers/intelligence.cjs +57 -4
- package/bin/cli.js +11 -1
- package/bin/mcp-server.js +11 -1
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/doctor-native-addon.d.ts +23 -0
- package/dist/src/commands/doctor-native-addon.js +135 -0
- package/dist/src/commands/doctor.js +3 -0
- package/dist/src/commands/hooks.js +8 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init.d.ts +2 -0
- package/dist/src/commands/init.js +37 -5
- package/dist/src/commands/mods.d.ts +3 -0
- package/dist/src/commands/mods.js +19 -5
- package/dist/src/commands/tui.d.ts +12 -0
- package/dist/src/commands/tui.js +98 -0
- package/dist/src/fs-secure.d.ts +25 -0
- package/dist/src/fs-secure.js +51 -2
- package/dist/src/init/executor.js +11 -14
- package/dist/src/init/helper-commands.d.ts +2 -0
- package/dist/src/init/helper-commands.js +42 -11
- package/dist/src/init/helpers-generator.js +4 -1
- package/dist/src/init/settings-generator.js +3 -3
- package/dist/src/init/settings-risk-scanner.js +25 -6
- package/dist/src/mcp-server.js +10 -2
- package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
- package/dist/src/mcp-tools/capability-brain.js +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +41 -15
- package/dist/src/mcp-tools/memory-tools.js +2 -1
- package/dist/src/memory/live-memory-row.d.ts +3 -1
- package/dist/src/memory/live-memory-row.js +3 -1
- package/dist/src/memory/memory-bridge.d.ts +4 -0
- package/dist/src/memory/memory-bridge.js +3 -2
- package/dist/src/memory/memory-initializer.d.ts +4 -0
- package/dist/src/memory/memory-initializer.js +12 -11
- package/dist/src/mods/claude-installs.js +7 -3
- package/dist/src/mods/install.js +32 -12
- package/dist/src/mods/plugin-resolve.js +3 -1
- package/dist/src/mods/probe.js +92 -4
- package/dist/src/services/command-recording.d.ts +17 -0
- package/dist/src/services/command-recording.js +91 -0
- package/node_modules/@claude-flow/codex/README.md +20 -3
- package/node_modules/@claude-flow/codex/dist/cli.js +7 -0
- package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
- package/node_modules/@claude-flow/codex/dist/index.js +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts +8 -3
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.js +75 -13
- package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/types.d.ts +8 -0
- package/node_modules/@claude-flow/codex/dist/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/package.json +4 -4
- package/plugins/ruflo-console/dist/tui.mjs +271 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4055: native addons compiled against Node >= 24.19 headers abort when a
|
|
3
|
+
* garbage-collected ObjectWrap (a better-sqlite3 Statement) calls
|
|
4
|
+
* RemoveEnvironmentCleanupHook() with no Environment (nodejs/node#65446).
|
|
5
|
+
* The fix (abe1b673e9b, PR #65943) was in no release through v24.21.0.
|
|
6
|
+
*
|
|
7
|
+
* Only copies that were COMPILED LOCALLY carry a build/config.gypi naming the
|
|
8
|
+
* headers they were built against; prebuilt binaries (prebuild-install) do not,
|
|
9
|
+
* so they are never flagged. The check is read-only and never loads an addon.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
12
|
+
import { dirname, join, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
const NAME = 'Native addon headers (Node 24)';
|
|
15
|
+
function parse(v) {
|
|
16
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
17
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
18
|
+
}
|
|
19
|
+
/** Node 24.19.0 up to, but not including, 24.22.0. Re-check this bound when a release ships the fix. */
|
|
20
|
+
export function isAffectedNodeVersion(version) {
|
|
21
|
+
const v = parse(version);
|
|
22
|
+
if (!v || v[0] !== 24)
|
|
23
|
+
return false;
|
|
24
|
+
return v[1] >= 19 && v[1] <= 21;
|
|
25
|
+
}
|
|
26
|
+
/** The Node version of the headers a copy was built against, from its config.gypi. */
|
|
27
|
+
export function headersVersionFromGypi(gypi) {
|
|
28
|
+
const field = /["']node_version["']\s*:\s*["']v?(\d+\.\d+\.\d+)["']/.exec(gypi);
|
|
29
|
+
if (field)
|
|
30
|
+
return field[1];
|
|
31
|
+
const dir = /["']nodedir["']\s*:\s*["']([^"']*)["']/.exec(gypi);
|
|
32
|
+
if (!dir)
|
|
33
|
+
return null;
|
|
34
|
+
const all = [...dir[1].matchAll(/(\d+)\.(\d+)\.(\d+)/g)];
|
|
35
|
+
const last = all[all.length - 1];
|
|
36
|
+
return last ? `${last[1]}.${last[2]}.${last[3]}` : null;
|
|
37
|
+
}
|
|
38
|
+
function readJson(path) {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function listDir(path) {
|
|
47
|
+
try {
|
|
48
|
+
return readdirSync(path);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** `better-sqlite3` copies in node_modules next to, or one level under, each ancestor of the start dirs. */
|
|
55
|
+
export function findBetterSqlite3Copies(startDirs, maxDepth = 12) {
|
|
56
|
+
const found = new Set();
|
|
57
|
+
const add = (dir) => {
|
|
58
|
+
if (!existsSync(join(dir, 'package.json')))
|
|
59
|
+
return;
|
|
60
|
+
try {
|
|
61
|
+
found.add(realpathSync(dir));
|
|
62
|
+
}
|
|
63
|
+
catch { /* unreadable */ }
|
|
64
|
+
};
|
|
65
|
+
const scanNodeModules = (nm) => {
|
|
66
|
+
add(join(nm, 'better-sqlite3'));
|
|
67
|
+
for (const entry of listDir(nm)) {
|
|
68
|
+
if (entry.startsWith('.') || entry === 'better-sqlite3')
|
|
69
|
+
continue;
|
|
70
|
+
if (entry.startsWith('@')) {
|
|
71
|
+
for (const sub of listDir(join(nm, entry)))
|
|
72
|
+
add(join(nm, entry, sub, 'node_modules', 'better-sqlite3'));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
add(join(nm, entry, 'node_modules', 'better-sqlite3'));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
for (const start of startDirs) {
|
|
80
|
+
let dir = resolve(start);
|
|
81
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
82
|
+
scanNodeModules(join(dir, 'node_modules'));
|
|
83
|
+
const parent = dirname(dir);
|
|
84
|
+
if (parent === dir)
|
|
85
|
+
break;
|
|
86
|
+
dir = parent;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return [...found].sort();
|
|
90
|
+
}
|
|
91
|
+
const FIX = [
|
|
92
|
+
'Use a Node release that contains nodejs/node#65943 (not in any release through 24.21.0), or Node 22 / 24.18.1;',
|
|
93
|
+
'or rebuild each listed copy against 24.18.1 headers:',
|
|
94
|
+
'cd <copy> && npx node-gyp rebuild --release --nodedir=<path to node 24.18.1 headers> ;',
|
|
95
|
+
'or install the 12.x prebuilt (npm install better-sqlite3@12 in the project).',
|
|
96
|
+
'ruflo itself opens AgentDB through its own prebuilt 12.x copy, so a flagged nested copy matters only to code that loads it directly.',
|
|
97
|
+
].join(' ');
|
|
98
|
+
/** Read-only: warn when a locally compiled better-sqlite3 was built against Node >= 24.19 headers on an affected runtime. */
|
|
99
|
+
export async function checkNativeAddonHeaders(deps = {}) {
|
|
100
|
+
const nodeVersion = deps.nodeVersion ?? process.versions.node;
|
|
101
|
+
if (!isAffectedNodeVersion(nodeVersion)) {
|
|
102
|
+
return { name: NAME, status: 'pass', message: `Node ${nodeVersion} is not in the affected range (24.19.0-24.21.x)` };
|
|
103
|
+
}
|
|
104
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
105
|
+
const copies = deps.copies ?? findBetterSqlite3Copies(deps.startDirs ?? [here, process.cwd()]);
|
|
106
|
+
if (copies.length === 0) {
|
|
107
|
+
return { name: NAME, status: 'pass', message: `Node ${nodeVersion}: no better-sqlite3 copy found to inspect` };
|
|
108
|
+
}
|
|
109
|
+
const bad = [];
|
|
110
|
+
for (const copy of copies) {
|
|
111
|
+
let gypi;
|
|
112
|
+
try {
|
|
113
|
+
gypi = readFileSync(join(copy, 'build', 'config.gypi'), 'utf8');
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const headers = headersVersionFromGypi(gypi);
|
|
119
|
+
const parsed = headers ? parse(headers) : null;
|
|
120
|
+
if (!parsed || parsed[0] !== 24 || parsed[1] < 19)
|
|
121
|
+
continue;
|
|
122
|
+
const version = readJson(join(copy, 'package.json'))?.version;
|
|
123
|
+
bad.push(`${copy} (better-sqlite3 ${typeof version === 'string' ? version : '?'}, built against Node ${headers} headers)`);
|
|
124
|
+
}
|
|
125
|
+
if (bad.length === 0) {
|
|
126
|
+
return { name: NAME, status: 'pass', message: `Node ${nodeVersion}: ${copies.length} better-sqlite3 cop${copies.length === 1 ? 'y' : 'ies'} checked, none built against 24.19+ headers` };
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
name: NAME,
|
|
130
|
+
status: 'warn',
|
|
131
|
+
message: `Node ${nodeVersion} can abort ("Assertion failed: (env) != nullptr", nodejs/node#65446) in: ${bad.join('; ')}`,
|
|
132
|
+
fix: FIX,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=doctor-native-addon.js.map
|
|
@@ -10,6 +10,7 @@ import { join, dirname } from 'path';
|
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
11
11
|
import { createHash } from 'crypto';
|
|
12
12
|
import { ownInstallAncestors } from '../plugins/own-install.js';
|
|
13
|
+
import { checkNativeAddonHeaders } from './doctor-native-addon.js';
|
|
13
14
|
import { execSync, exec, execFile } from 'child_process';
|
|
14
15
|
import { promisify } from 'util';
|
|
15
16
|
import { decodeKey, isEncryptionEnabled } from '../encryption/vault.js';
|
|
@@ -2607,6 +2608,7 @@ export const doctorCommand = {
|
|
|
2607
2608
|
];
|
|
2608
2609
|
},
|
|
2609
2610
|
'memory-package': checkMemoryPackageVersion, // #3392
|
|
2611
|
+
'native-addon': () => checkNativeAddonHeaders(), // #4055
|
|
2610
2612
|
'learning': checkLearningBridge, // #2545
|
|
2611
2613
|
'learning-bridge': checkLearningBridge, // #2545
|
|
2612
2614
|
'api': checkApiKeys,
|
|
@@ -2722,6 +2724,7 @@ export const doctorCommand = {
|
|
|
2722
2724
|
checkMemoryPersistenceDriver, // #2968/#3321 — read-only native capability probe
|
|
2723
2725
|
checkLearningBridge, // #2545 — can the auto-memory hook actually load @claude-flow/memory?
|
|
2724
2726
|
checkMemoryPackageVersion, // #3392 — loaded @claude-flow/memory must satisfy the CLI's declared range
|
|
2727
|
+
() => checkNativeAddonHeaders(), // #4055 — better-sqlite3 built against Node 24.19+ headers on 24.19-24.21
|
|
2725
2728
|
checkApiKeys,
|
|
2726
2729
|
checkMcpServers,
|
|
2727
2730
|
checkMcpSchemaOverhead, // #2726 — fixed tools/list prompt cost
|
|
@@ -599,9 +599,16 @@ const postCommandCommand = {
|
|
|
599
599
|
ttl: ctx.flags.ttl,
|
|
600
600
|
timestamp: Date.now(),
|
|
601
601
|
});
|
|
602
|
+
// #4056: recording switched off on purpose is not a failure.
|
|
603
|
+
const recordingOff = result.disabled === true;
|
|
602
604
|
if (ctx.flags.format === 'json') {
|
|
603
605
|
output.printJson(result);
|
|
604
|
-
|
|
606
|
+
const ok = result.recorded || recordingOff;
|
|
607
|
+
return { success: ok, exitCode: ok ? 0 : 1, data: result };
|
|
608
|
+
}
|
|
609
|
+
if (recordingOff) {
|
|
610
|
+
output.printInfo('Command recording is switched off (CLAUDE_FLOW_HOOKS_RECORD_COMMANDS / CLAUDE_FLOW_HOOKS_ENABLED / hooks.recordCommands); nothing was stored.');
|
|
611
|
+
return { success: true, data: result };
|
|
605
612
|
}
|
|
606
613
|
if (!result.recorded) {
|
|
607
614
|
output.printError('Command outcome could not be recorded');
|
|
@@ -90,6 +90,8 @@ const commandLoaders = {
|
|
|
90
90
|
spinner: () => import('./spinner.js'),
|
|
91
91
|
// ruflo as a Claude Code mod: function hooks, early access (ADR-404)
|
|
92
92
|
mods: () => import('./mods.js'),
|
|
93
|
+
// ADR-489: the console in a terminal of its own (read-only Overview and Cost); loads a bundle from this CLI's own install.
|
|
94
|
+
tui: () => import('./tui.js'),
|
|
93
95
|
// ADR-406 command catalog and mission control (interim homes until mods.ts follow-up)
|
|
94
96
|
catalog: () => import('./catalog.js'),
|
|
95
97
|
mission: () => import('./mission.js'),
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* Comprehensive initialization for Claude Flow with Claude Code integration
|
|
4
4
|
*/
|
|
5
5
|
import type { Command } from '../types.js';
|
|
6
|
+
export declare function readCodexTrustFlag(flags: Record<string, unknown>): 'standard' | 'full' | undefined | null;
|
|
6
7
|
export declare function runCodexInitializerCli(cwd: string, options: {
|
|
7
8
|
template: string;
|
|
8
9
|
force: boolean;
|
|
9
10
|
dual: boolean;
|
|
11
|
+
codexTrust?: 'standard' | 'full';
|
|
10
12
|
}): boolean;
|
|
11
13
|
export declare const initCommand: Command;
|
|
12
14
|
export default initCommand;
|
|
@@ -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
|
-
|
|
434
|
+
const otherWarnings = (result.warnings ?? []).filter((w) => !isTrustNote(w));
|
|
435
|
+
if (otherWarnings.length > 0) {
|
|
409
436
|
output.printWarning('Warnings:');
|
|
410
|
-
for (const warning of
|
|
437
|
+
for (const warning of otherWarnings.slice(0, 5)) {
|
|
411
438
|
output.printInfo(` • ${warning}`);
|
|
412
439
|
}
|
|
413
|
-
if (
|
|
414
|
-
output.printInfo(` ... and ${
|
|
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: [
|
|
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
|
|
120
|
-
|
|
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
|
|
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
|
package/dist/src/fs-secure.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/src/fs-secure.js
CHANGED
|
@@ -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
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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 =
|
|
317
|
-
|
|
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:
|
|
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:
|
|
352
|
+
timeout: AUTO_MEMORY_HOOK_TIMEOUT_S,
|
|
356
353
|
continueOnError: true,
|
|
357
354
|
});
|
|
358
355
|
}
|