@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
|
@@ -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) {
|
|
@@ -30,20 +32,38 @@ export function helperScopeFor(targetDir) {
|
|
|
30
32
|
return 'project';
|
|
31
33
|
return canonical(targetDir) === canonical(os.homedir()) ? 'user' : 'project';
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Windows hook command (#4045). It must not go through `cmd /c`: under Git Bash
|
|
37
|
+
* MSYS rewrites `/c` into a path, cmd starts interactively and executes the
|
|
38
|
+
* hook's stdin JSON, and any `>` in that text creates a file. A single
|
|
39
|
+
* `node -e` has no shell syntax to misread (no cmd, no `%VAR%`, no `<>|&$` and
|
|
40
|
+
* no inner double quotes), so cmd, PowerShell and Git Bash pass it through
|
|
41
|
+
* unchanged. The script and its subcommand stay in one string, because the
|
|
42
|
+
* ruflo-mods ownership rule matches `hook-handler.cjs <subcommand>`. Project
|
|
43
|
+
* scope prefers the project's helper and falls back to the user profile's
|
|
44
|
+
* (#1943); user scope never looks at the project. The helper runs as a child
|
|
45
|
+
* with inherited stdio so stdin, stdout and the exit status pass through (a
|
|
46
|
+
* `require` would skip the helper's `require.main === module` entry point).
|
|
47
|
+
*/
|
|
48
|
+
function windowsHookCommand(script, subcommand, scope) {
|
|
49
|
+
// Ternaries, not `||`: the command carries no pipe or ampersand at all.
|
|
50
|
+
const home = "(e.USERPROFILE?e.USERPROFILE:e.HOME?e.HOME:'.')";
|
|
51
|
+
const pick = scope === 'user'
|
|
52
|
+
? `f=p.join(${home},a[0]);`
|
|
53
|
+
: "f=p.join(e.CLAUDE_PROJECT_DIR?e.CLAUDE_PROJECT_DIR:'.',a[0]);" +
|
|
54
|
+
`if(!fs.existsSync(f))f=p.join(${home},a[0]);`;
|
|
55
|
+
const js = `var a='${[script, subcommand].filter(Boolean).join(' ')}'.split(' '),e=process.env,fs=require('fs'),p=require('path'),f;` + pick +
|
|
56
|
+
"var r=require('child_process').spawnSync(process.execPath,[f].concat(a.slice(1)),{stdio:'inherit'});" +
|
|
57
|
+
'process.exit(r.status===null?1:r.status)';
|
|
58
|
+
return `node -e "${js}"`;
|
|
59
|
+
}
|
|
33
60
|
/** Command that runs `node <script> <subcommand>` for a hook. */
|
|
34
61
|
export function helperHookCommand(script, subcommand, scope, windows = IS_WINDOWS) {
|
|
35
|
-
const winScript = script.replace(/\//g, '\\');
|
|
36
62
|
const arg = subcommand ? ` ${subcommand}` : '';
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
if (windows) {
|
|
43
|
-
// cmd.exe equivalent of the sh probe below. `IF EXIST` checks the
|
|
44
|
-
// project-local path; falls back to %USERPROFILE% if missing.
|
|
45
|
-
return `cmd /c "IF EXIST \"%CLAUDE_PROJECT_DIR%\\${winScript}\" (node \"%CLAUDE_PROJECT_DIR%\\${winScript}\"${arg}) ELSE (node \"%USERPROFILE%\\${winScript}\"${arg})"`;
|
|
46
|
-
}
|
|
63
|
+
if (windows)
|
|
64
|
+
return windowsHookCommand(script, subcommand, scope);
|
|
65
|
+
if (scope === 'user')
|
|
66
|
+
return `sh -c 'exec node "\${HOME}/${script}"${arg}'`;
|
|
47
67
|
// POSIX sh: prefer project-local helpers, fall back to $HOME/.claude/.
|
|
48
68
|
// eslint-disable-next-line no-template-curly-in-string
|
|
49
69
|
const projVar = '${CLAUDE_PROJECT_DIR:-.}';
|
|
@@ -102,6 +122,17 @@ const IF_EXIST_PREAMBLE = /"IF EXIST "%CLAUDE_PROJECT_DIR%\\\.claude\\helpers\\[
|
|
|
102
122
|
* commands (other scripts, redirects, chains) are left alone.
|
|
103
123
|
*/
|
|
104
124
|
export function pinToUserHelpers(command, windows = IS_WINDOWS) {
|
|
125
|
+
// The #4045 Windows form: recognised only when it is exactly what ruflo generates.
|
|
126
|
+
const generated = /^node -e "var a='\.claude\/helpers\/([\w.-]+)((?: [\w:.=-]+)*)'\.split\(' '\)/.exec(command);
|
|
127
|
+
if (generated) {
|
|
128
|
+
if (!RUFLO_HELPERS.includes(generated[1]))
|
|
129
|
+
return null;
|
|
130
|
+
const script = `.claude/helpers/${generated[1]}`;
|
|
131
|
+
const subcommand = generated[2].trim();
|
|
132
|
+
return command === helperHookCommand(script, subcommand, 'project', true)
|
|
133
|
+
? helperHookCommand(script, subcommand, 'user', true)
|
|
134
|
+
: null;
|
|
135
|
+
}
|
|
105
136
|
const refs = [...command.matchAll(HELPER_REF)];
|
|
106
137
|
if (refs.length === 0)
|
|
107
138
|
return null;
|
|
@@ -1306,7 +1306,10 @@ function doStatus() {
|
|
|
1306
1306
|
// Suppress unhandled rejection warnings from dynamic import() failures
|
|
1307
1307
|
process.on('unhandledRejection', () => {});
|
|
1308
1308
|
|
|
1309
|
-
|
|
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:
|
|
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:
|
|
309
|
+
timeout: AUTO_MEMORY_HOOK_TIMEOUT_S,
|
|
310
310
|
},
|
|
311
311
|
],
|
|
312
312
|
},
|
|
@@ -44,7 +44,6 @@ const DOWNLOADER_WORDS = ['curl', 'wget'];
|
|
|
44
44
|
const STANDALONE_RISK_PATTERNS = [
|
|
45
45
|
/\bInvoke-Expression\b/i,
|
|
46
46
|
/\biex\s*\(/i,
|
|
47
|
-
/powershell(\.exe)?\s+.*-e(nc(odedcommand)?)?\s+\S/i,
|
|
48
47
|
/\/dev\/tcp\//i,
|
|
49
48
|
/\bnc\s+-e\b/i,
|
|
50
49
|
/base64\s+(-d|--decode)\b/i,
|
|
@@ -54,17 +53,37 @@ const STANDALONE_RISK_PATTERNS = [
|
|
|
54
53
|
// own the way `rm`/`chmod` are — pre-approving them removes the human
|
|
55
54
|
// review step for exactly the commands download-and-execute attacks use.
|
|
56
55
|
const RISKY_PREAPPROVE_WORDS = [...DANGEROUS_COMMAND_WORDS, ...SHELL_EXEC_WORDS, ...DOWNLOADER_WORDS];
|
|
56
|
+
// Encoded PowerShell (`powershell[.exe] ... -e|-enc|-encodedcommand <payload>`),
|
|
57
|
+
// matched on whitespace-separated tokens in ONE linear pass. The earlier regex
|
|
58
|
+
// (`powershell\\s+(?:\\S+\\s+)*?-e...`) is quadratic on repeated "powershell "
|
|
59
|
+
// tokens (every start rescans the tail: 256KB measured ~2.5s, 1MB ~40s) because
|
|
60
|
+
// the lazy token loop re-runs from each start. Same semantics: a token ending in
|
|
61
|
+
// powershell/powershell.exe, then LATER a token that is exactly -e/-enc/
|
|
62
|
+
// -encodedcommand, then at least one more token.
|
|
63
|
+
function hasEncodedPowershell(command) {
|
|
64
|
+
let seenPs = false;
|
|
65
|
+
let pendingEnc = false;
|
|
66
|
+
for (const raw of command.split(/\s+/)) {
|
|
67
|
+
if (!raw)
|
|
68
|
+
continue;
|
|
69
|
+
if (pendingEnc)
|
|
70
|
+
return true;
|
|
71
|
+
const t = raw.toLowerCase();
|
|
72
|
+
if (seenPs && (t === '-e' || t === '-enc' || t === '-encodedcommand'))
|
|
73
|
+
pendingEnc = true;
|
|
74
|
+
else if (t.endsWith('powershell') || t.endsWith('powershell.exe'))
|
|
75
|
+
seenPs = true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
57
79
|
function wordBoundaryMatches(text, word) {
|
|
58
80
|
return new RegExp(`\\b${word}\\b`, 'i').test(text);
|
|
59
81
|
}
|
|
60
82
|
/** Reasons a single hook `command` string looks risky (empty = clean). */
|
|
61
83
|
export function scanCommandStringForRisk(command) {
|
|
62
84
|
const reasons = [];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
reasons.push('matches a known-dangerous execution pattern (encoded/remote command execution)');
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
85
|
+
if (hasEncodedPowershell(command) || STANDALONE_RISK_PATTERNS.some((pattern) => pattern.test(command))) {
|
|
86
|
+
reasons.push('matches a known-dangerous execution pattern (encoded/remote command execution)');
|
|
68
87
|
}
|
|
69
88
|
const hasDownloader = DOWNLOADER_WORDS.some((w) => wordBoundaryMatches(command, w));
|
|
70
89
|
const hasShellExec = SHELL_EXEC_WORDS.some((w) => wordBoundaryMatches(command, w));
|
package/dist/src/mcp-server.js
CHANGED
|
@@ -619,7 +619,7 @@ export class MCPServerManager extends EventEmitter {
|
|
|
619
619
|
version: VERSION,
|
|
620
620
|
capabilities: {
|
|
621
621
|
tools: { listChanged: true },
|
|
622
|
-
resources: {
|
|
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: {
|
|
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 \`${
|
|
1067
|
-
metadata: { hook: 'post-command', command:
|
|
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;
|
|
@@ -3305,19 +3327,23 @@ export const hooksTrajectoryEnd = {
|
|
|
3305
3327
|
}
|
|
3306
3328
|
catch { /* intelligence module not loadable — keep sona-only behaviour */ }
|
|
3307
3329
|
}
|
|
3308
|
-
// #2351: when an agent calls trajectory-end with
|
|
3309
|
-
//
|
|
3310
|
-
//
|
|
3311
|
-
//
|
|
3312
|
-
//
|
|
3313
|
-
//
|
|
3314
|
-
//
|
|
3315
|
-
//
|
|
3316
|
-
//
|
|
3330
|
+
// #2351: when an agent calls trajectory-end with a non-empty `feedback`
|
|
3331
|
+
// string, the feedback was previously dropped on the floor —
|
|
3332
|
+
// `patternsExtracted` reported 0 and `pattern-search` never surfaced it.
|
|
3333
|
+
// Route it through the same store + embed path that pattern-store uses
|
|
3334
|
+
// so it becomes searchable. Best-effort: failures here must not turn the
|
|
3335
|
+
// trajectory-end call itself into a failure — the trajectory record was
|
|
3336
|
+
// already persisted above.
|
|
3337
|
+
//
|
|
3338
|
+
// #3662: #2351's original fix gated this on `!hasSteps` (step-less
|
|
3339
|
+
// trajectories only). The common start -> step -> end path (the one real
|
|
3340
|
+
// agents actually use) therefore recorded SONA state while leaving the
|
|
3341
|
+
// feedback unindexed. Non-empty feedback is promoted regardless of
|
|
3342
|
+
// whether steps were recorded — a trajectory's steps and its distilled
|
|
3343
|
+
// feedback are not mutually exclusive evidence.
|
|
3317
3344
|
let feedbackDistilled = { stored: false };
|
|
3318
|
-
const hasSteps = !!trajectory && trajectory.steps.length > 0;
|
|
3319
3345
|
const trimmedFeedback = typeof feedback === 'string' ? feedback.trim() : '';
|
|
3320
|
-
if (trajectory &&
|
|
3346
|
+
if (trajectory && trimmedFeedback.length > 0) {
|
|
3321
3347
|
const distilledPatternId = `pattern-feedback-${trajectoryId}-${Date.now()}`;
|
|
3322
3348
|
const patternMetadata = {
|
|
3323
3349
|
sourceTrajectoryId: trajectoryId,
|
|
@@ -1402,7 +1402,8 @@ export const memoryTools = [
|
|
|
1402
1402
|
if (!v.valid)
|
|
1403
1403
|
throw new Error(v.error);
|
|
1404
1404
|
}
|
|
1405
|
-
|
|
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;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
/** TTL is a read-time visibility rule, independent of background cleanup. */
|
|
2
|
-
export declare function liveMemoryRowSql(
|
|
2
|
+
export declare function liveMemoryRowSql(opts?: {
|
|
3
|
+
includeExpired?: boolean;
|
|
4
|
+
}): string;
|
|
3
5
|
//# sourceMappingURL=live-memory-row.d.ts.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** TTL is a read-time visibility rule, independent of background cleanup. */
|
|
2
|
-
export function liveMemoryRowSql() {
|
|
2
|
+
export function liveMemoryRowSql(opts = {}) {
|
|
3
|
+
if (opts.includeExpired)
|
|
4
|
+
return `(status = 'active' OR status IS NULL)`;
|
|
3
5
|
return `(status = 'active' OR status IS NULL) AND (expires_at IS NULL OR expires_at > ${Date.now()})`;
|
|
4
6
|
}
|
|
5
7
|
//# sourceMappingURL=live-memory-row.js.map
|
|
@@ -142,6 +142,8 @@ export declare function bridgeListEntries(options: {
|
|
|
142
142
|
provenanceFilter?: string[];
|
|
143
143
|
/** ADR-472: include each entry's embedding as int8+scale (`embeddingQ8`); at most MAX_LIST_EMBEDDINGS rows. */
|
|
144
144
|
includeEmbedding?: boolean;
|
|
145
|
+
/** #4056: also return rows whose TTL has passed (with `expiresAt`). For cleanup only. */
|
|
146
|
+
includeExpired?: boolean;
|
|
145
147
|
}): Promise<{
|
|
146
148
|
success: boolean;
|
|
147
149
|
entries: {
|
|
@@ -152,6 +154,8 @@ export declare function bridgeListEntries(options: {
|
|
|
152
154
|
accessCount: number;
|
|
153
155
|
createdAt: string;
|
|
154
156
|
updatedAt: string;
|
|
157
|
+
/** #4056: epoch ms the row expires at; null when it never does. */
|
|
158
|
+
expiresAt?: number | null;
|
|
155
159
|
hasEmbedding: boolean;
|
|
156
160
|
/** #2073: Present when `includeContent: true` was requested. */
|
|
157
161
|
content?: string;
|
|
@@ -1453,7 +1453,7 @@ export async function bridgeListEntries(options) {
|
|
|
1453
1453
|
// the `status = 'active'` filter matched zero. Treat NULL as
|
|
1454
1454
|
// "legacy-active" — the safe default for any entry that predates the
|
|
1455
1455
|
// status column.
|
|
1456
|
-
const statusFilter = liveMemoryRowSql();
|
|
1456
|
+
const statusFilter = liveMemoryRowSql({ includeExpired: options.includeExpired });
|
|
1457
1457
|
// Count
|
|
1458
1458
|
let total = 0;
|
|
1459
1459
|
try {
|
|
@@ -1468,7 +1468,7 @@ export async function bridgeListEntries(options) {
|
|
|
1468
1468
|
const entries = [];
|
|
1469
1469
|
try {
|
|
1470
1470
|
const stmt = ctx.db.prepare(`
|
|
1471
|
-
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type
|
|
1471
|
+
SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type, expires_at
|
|
1472
1472
|
FROM memory_entries
|
|
1473
1473
|
WHERE ${statusFilter} ${extraFilter}
|
|
1474
1474
|
ORDER BY updated_at DESC
|
|
@@ -1486,6 +1486,7 @@ export async function bridgeListEntries(options) {
|
|
|
1486
1486
|
accessCount: row.access_count ?? 0,
|
|
1487
1487
|
createdAt: row.created_at || new Date().toISOString(),
|
|
1488
1488
|
updatedAt: row.updated_at || new Date().toISOString(),
|
|
1489
|
+
expiresAt: row.expires_at ?? null,
|
|
1489
1490
|
hasEmbedding: !!(row.embedding && String(row.embedding).length > 10),
|
|
1490
1491
|
provenanceType: row.provenance_type || 'unknown',
|
|
1491
1492
|
};
|
|
@@ -511,6 +511,8 @@ export declare function listEntries(options: {
|
|
|
511
511
|
provenanceFilter?: string[];
|
|
512
512
|
/** ADR-472: include each entry's embedding as int8+scale (`embeddingQ8`); at most MAX_LIST_EMBEDDINGS rows. Read-only. */
|
|
513
513
|
includeEmbedding?: boolean;
|
|
514
|
+
/** #4056: also return rows whose TTL has passed (with `expiresAt`). For cleanup only; normal reads hide them. */
|
|
515
|
+
includeExpired?: boolean;
|
|
514
516
|
}): Promise<{
|
|
515
517
|
success: boolean;
|
|
516
518
|
entries: {
|
|
@@ -521,6 +523,8 @@ export declare function listEntries(options: {
|
|
|
521
523
|
accessCount: number;
|
|
522
524
|
createdAt: string;
|
|
523
525
|
updatedAt: string;
|
|
526
|
+
/** #4056: epoch ms the row expires at; null when it never does. */
|
|
527
|
+
expiresAt?: number | null;
|
|
524
528
|
hasEmbedding: boolean;
|
|
525
529
|
/** #2073: Present when `includeContent: true` was requested. */
|
|
526
530
|
content?: string;
|
|
@@ -6,7 +6,7 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
9
|
-
import { readFileMaybeEncrypted,
|
|
9
|
+
import { readFileMaybeEncrypted, writeDbImagePreservingFormat, writeFileRestricted } from '../fs-secure.js';
|
|
10
10
|
import { restoreMemoryDbFromBackup } from '../services/memory-backup.js';
|
|
11
11
|
import { validateIdentifier } from '../mcp-tools/validate-input.js';
|
|
12
12
|
/**
|
|
@@ -1248,7 +1248,7 @@ export async function ensureSchemaColumns(dbPath, options = {}) {
|
|
|
1248
1248
|
if (modified) {
|
|
1249
1249
|
// Save updated database
|
|
1250
1250
|
const data = db.export();
|
|
1251
|
-
|
|
1251
|
+
writeDbImagePreservingFormat(dbPath, data, { encrypt: options.encryptWrites ?? true });
|
|
1252
1252
|
}
|
|
1253
1253
|
db.close();
|
|
1254
1254
|
return { success: true, columnsAdded };
|
|
@@ -1975,7 +1975,7 @@ export async function applyTemporalDecay(dbPath) {
|
|
|
1975
1975
|
return await withMemoryDbLock(path_, async () => {
|
|
1976
1976
|
const initSqlJs = (await import('sql.js')).default;
|
|
1977
1977
|
const SQL = await initSqlJs();
|
|
1978
|
-
const fileBuffer =
|
|
1978
|
+
const fileBuffer = readFileMaybeEncrypted(path_, null);
|
|
1979
1979
|
const db = new SQL.Database(fileBuffer);
|
|
1980
1980
|
// Apply decay: confidence *= exp(-decay_rate * days_since_last_use)
|
|
1981
1981
|
const now = Date.now();
|
|
@@ -1992,7 +1992,7 @@ export async function applyTemporalDecay(dbPath) {
|
|
|
1992
1992
|
const changes = db.getRowsModified();
|
|
1993
1993
|
// Save (atomic — issue #2584: a torn full-image flush corrupts the store)
|
|
1994
1994
|
const data = db.export();
|
|
1995
|
-
|
|
1995
|
+
writeDbImagePreservingFormat(path_, data);
|
|
1996
1996
|
db.close();
|
|
1997
1997
|
return {
|
|
1998
1998
|
success: true,
|
|
@@ -2692,7 +2692,7 @@ export async function storeEntry(options) {
|
|
|
2692
2692
|
}
|
|
2693
2693
|
// Save
|
|
2694
2694
|
const data = db.export();
|
|
2695
|
-
|
|
2695
|
+
writeDbImagePreservingFormat(dbPath, data);
|
|
2696
2696
|
db.close();
|
|
2697
2697
|
});
|
|
2698
2698
|
// Add to HNSW index for faster future searches
|
|
@@ -3019,7 +3019,7 @@ export async function listEntries(options) {
|
|
|
3019
3019
|
// that predate the status column may have NULL after migration.
|
|
3020
3020
|
// See memory-bridge.ts:bridgeListEntries for full context.
|
|
3021
3021
|
// Get total count
|
|
3022
|
-
const whereClauses = [liveMemoryRowSql()];
|
|
3022
|
+
const whereClauses = [liveMemoryRowSql({ includeExpired: options.includeExpired })];
|
|
3023
3023
|
const whereParams = [];
|
|
3024
3024
|
if (namespace) {
|
|
3025
3025
|
whereClauses.push('namespace = ?');
|
|
@@ -3044,7 +3044,7 @@ export async function listEntries(options) {
|
|
|
3044
3044
|
const safeLimit = Math.min(parseInt(String(limit), 10) || 100, options.includeEmbedding ? MAX_LIST_EMBEDDINGS : Number.MAX_SAFE_INTEGER);
|
|
3045
3045
|
const safeOffset = parseInt(String(offset), 10) || 0;
|
|
3046
3046
|
// #2120 — same NULL-as-active acceptance as the count above.
|
|
3047
|
-
const listStmt = db.prepare(`SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type
|
|
3047
|
+
const listStmt = db.prepare(`SELECT id, key, namespace, content, embedding, access_count, created_at, updated_at, provenance_type, expires_at
|
|
3048
3048
|
FROM memory_entries WHERE ${whereSql}
|
|
3049
3049
|
ORDER BY updated_at DESC LIMIT ? OFFSET ?`);
|
|
3050
3050
|
listStmt.bind([...whereParams, safeLimit, safeOffset]);
|
|
@@ -3057,7 +3057,7 @@ export async function listEntries(options) {
|
|
|
3057
3057
|
const entries = [];
|
|
3058
3058
|
if (result[0]?.values) {
|
|
3059
3059
|
for (const row of result[0].values) {
|
|
3060
|
-
const [id, key, ns, content, embedding, accessCount, createdAt, updatedAt, provenanceTypeVal] = row;
|
|
3060
|
+
const [id, key, ns, content, embedding, accessCount, createdAt, updatedAt, provenanceTypeVal, expiresAtVal] = row;
|
|
3061
3061
|
const entry = {
|
|
3062
3062
|
// #2073: don't truncate id when content is requested — callers
|
|
3063
3063
|
// (notably memory_export) need the full id to round-trip via import.
|
|
@@ -3068,6 +3068,7 @@ export async function listEntries(options) {
|
|
|
3068
3068
|
accessCount: accessCount || 0,
|
|
3069
3069
|
createdAt: createdAt || new Date().toISOString(),
|
|
3070
3070
|
updatedAt: updatedAt || new Date().toISOString(),
|
|
3071
|
+
expiresAt: expiresAtVal ?? null,
|
|
3071
3072
|
hasEmbedding: !!embedding && embedding.length > 10,
|
|
3072
3073
|
provenanceType: provenanceTypeVal || 'unknown'
|
|
3073
3074
|
};
|
|
@@ -3166,7 +3167,7 @@ export async function getEntry(options) {
|
|
|
3166
3167
|
`, [String(id)]);
|
|
3167
3168
|
// Save updated database
|
|
3168
3169
|
const data = db.export();
|
|
3169
|
-
|
|
3170
|
+
writeDbImagePreservingFormat(dbPath, data);
|
|
3170
3171
|
db.close();
|
|
3171
3172
|
let tags = [];
|
|
3172
3173
|
if (tagsJson) {
|
|
@@ -3302,7 +3303,7 @@ export async function deleteEntry(options) {
|
|
|
3302
3303
|
const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
|
|
3303
3304
|
// Save updated database
|
|
3304
3305
|
const data = db.export();
|
|
3305
|
-
|
|
3306
|
+
writeDbImagePreservingFormat(dbPath, data, { encrypt: options.encryptWrites ?? true });
|
|
3306
3307
|
db.close();
|
|
3307
3308
|
// Clean up in-memory HNSW index so ghost vectors don't appear in searches.
|
|
3308
3309
|
// Remove the entry from the HNSW entries map and invalidate the index.
|
|
@@ -3472,7 +3473,7 @@ export async function purgeNamespace(options) {
|
|
|
3472
3473
|
const countResult = db.exec(`SELECT COUNT(*) FROM memory_entries WHERE status = 'active'`);
|
|
3473
3474
|
const remainingEntries = countResult[0]?.values?.[0]?.[0] || 0;
|
|
3474
3475
|
const data = db.export();
|
|
3475
|
-
|
|
3476
|
+
writeDbImagePreservingFormat(dbPath, data, { encrypt: options.encryptWrites ?? true });
|
|
3476
3477
|
db.close();
|
|
3477
3478
|
if (deletedCount > 0 && hnswIndex?.entries) {
|
|
3478
3479
|
for (const [id, entry] of hnswIndex.entries) {
|
|
@@ -69,13 +69,17 @@ export function judgeInstalls(installs, enableEnvSet) {
|
|
|
69
69
|
: '';
|
|
70
70
|
const runs = `${first.path} (${first.version ?? 'version unknown'})`;
|
|
71
71
|
if (!first.version)
|
|
72
|
-
return { status: 'warn', message: `claude resolves to ${runs}${mixed}` };
|
|
72
|
+
return { status: 'warn', message: `claude resolves to ${runs}${mixed}`, fix: `run \`${first.path} --version\` and reinstall Claude Code if it prints nothing` };
|
|
73
73
|
if (!versionLess(first.version, MODS_DEFAULT_ON)) {
|
|
74
|
-
return
|
|
74
|
+
return others.length
|
|
75
|
+
? { status: 'warn', message: `claude resolves to ${runs}: mods on by default${mixed}`, fix: `put the claude you want first on PATH, or remove: ${others.map((i) => i.path).join(' ')}` }
|
|
76
|
+
: { status: 'pass', message: `claude resolves to ${runs}: mods on by default` };
|
|
75
77
|
}
|
|
76
78
|
if (!versionLess(first.version, MODS_FIRST)) {
|
|
77
79
|
return enableEnvSet
|
|
78
|
-
?
|
|
80
|
+
? (others.length
|
|
81
|
+
? { status: 'warn', message: `claude resolves to ${runs}: mods load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (set)${mixed}`, fix: `put the claude you want first on PATH, or remove: ${others.map((i) => i.path).join(' ')}` }
|
|
82
|
+
: { status: 'pass', message: `claude resolves to ${runs}: mods load with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (set)` })
|
|
79
83
|
: { status: 'warn', message: `claude resolves to ${runs}: below ${MODS_DEFAULT_ON}, mods need CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 (not set)${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON}, or export CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1` };
|
|
80
84
|
}
|
|
81
85
|
return { status: 'warn', message: `claude resolves to ${runs}: predates mods (< ${MODS_FIRST}); classic hooks handle every event${mixed}`, fix: `upgrade to >= ${MODS_DEFAULT_ON} and remove the stale install from PATH` };
|
package/dist/src/mods/install.js
CHANGED
|
@@ -107,18 +107,38 @@ export function installMod(projectRoot, scope, dryRun = false, plugins = MOD_PLU
|
|
|
107
107
|
backup = `${settingsFile}.bak-ruflo-mods-${Date.now()}`;
|
|
108
108
|
copyFileSync(settingsFile, backup);
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
110
|
+
// One operation: the settings and the record that lets uninstall take them back are written together.
|
|
111
|
+
// If the record cannot be written, the settings go back as they were (or the file ruflo created goes
|
|
112
|
+
// away), so nothing is left half-installed with no record to remove it by.
|
|
113
|
+
try {
|
|
114
|
+
writeJson(settingsFile, next);
|
|
115
|
+
// What ruflo added once is still ruflo's to remove: merge into the record.
|
|
116
|
+
const record = readRecord(projectRoot) ?? { version: 2, installedAt: '', files: {} };
|
|
117
|
+
const before = record.files[settingsFile] ?? { plugins: [], marketplace: false, env: false };
|
|
118
|
+
record.files[settingsFile] = {
|
|
119
|
+
plugins: [...new Set([...before.plugins, ...added.plugins])],
|
|
120
|
+
marketplace: before.marketplace || added.marketplace,
|
|
121
|
+
env: before.env || added.env,
|
|
122
|
+
claudeInstalled: before.claudeInstalled ?? [],
|
|
123
|
+
};
|
|
124
|
+
record.installedAt = new Date().toISOString();
|
|
125
|
+
writeJson(join(resolve(projectRoot), INSTALL_RECORD), record);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
try {
|
|
129
|
+
if (backup) {
|
|
130
|
+
copyFileSync(backup, settingsFile);
|
|
131
|
+
unlinkSync(backup);
|
|
132
|
+
}
|
|
133
|
+
else if (existsSync(settingsFile)) {
|
|
134
|
+
unlinkSync(settingsFile);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// the restore itself failed: the original error below is the one to report
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
122
142
|
return { settingsFile, backup, added, dryRun, next };
|
|
123
143
|
}
|
|
124
144
|
/** The install record, v1 (3.50.0: one file, ruflo-mods only) read as v2. */
|
|
@@ -160,7 +160,9 @@ export function resolveFindings(projectRoot, scope, configDir, fs = nodeReadFs,
|
|
|
160
160
|
else if (!market.location) {
|
|
161
161
|
findings.push(installed.installed
|
|
162
162
|
? { name, status: 'pass', message: `installed (${installed.scope} scope, ${installed.installPath})` }
|
|
163
|
-
:
|
|
163
|
+
: plugin.required
|
|
164
|
+
? { name, status: 'warn', message: 'loads once Claude Code has cloned the marketplace (a headless `claude -p` run on a fresh config does not clone it, so the mod is absent there)', fix }
|
|
165
|
+
: { name, status: 'warn', message: 'pending: not released in the ruflo marketplace yet' });
|
|
164
166
|
}
|
|
165
167
|
else if (!plugin.required) {
|
|
166
168
|
findings.push({ name, status: 'warn', message: `pending: not in the ruflo marketplace yet (${market.location})${also}` });
|