claude-flow 3.48.0 → 3.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/.proven-config-version +1 -0
- package/.claude/helpers/hook-handler.cjs +20 -4
- package/.claude/helpers/memory.cjs +1 -1
- package/.claude/helpers/router.cjs +1 -1
- package/.claude/helpers/session.cjs +1 -1
- package/.claude/proven-config.json +42 -0
- package/.claude-plugin/marketplace.json +26 -1
- package/README.md +1 -53
- package/README.zh-CN.md +1 -53
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +2 -6
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +35 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +18 -0
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +2 -2
- package/v3/@claude-flow/cli/README.md +3 -53
- package/v3/@claude-flow/cli/catalog-manifest.json +4 -4
- package/v3/@claude-flow/cli/dist/src/commands/doctor.d.ts +19 -1
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +88 -9
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +7 -4
- package/v3/@claude-flow/cli/dist/src/commands/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/commands/init.js +20 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +30 -7
- package/v3/@claude-flow/cli/dist/src/commands/mods.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/commands/mods.js +126 -0
- package/v3/@claude-flow/cli/dist/src/commands/plugins.js +44 -7
- package/v3/@claude-flow/cli/dist/src/commands/policy.js +5 -2
- package/v3/@claude-flow/cli/dist/src/commands/session.js +128 -21
- package/v3/@claude-flow/cli/dist/src/commands/swarm.js +11 -11
- package/v3/@claude-flow/cli/dist/src/index.js +10 -1
- package/v3/@claude-flow/cli/dist/src/init/executor.js +11 -5
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +34 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-integrity.d.ts +21 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-integrity.js +62 -0
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +18 -11
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +56 -13
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +22 -10
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +16 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +87 -14
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +43 -6
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +236 -50
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +6 -3
- package/v3/@claude-flow/cli/dist/src/mods/claude-installs.d.ts +29 -0
- package/v3/@claude-flow/cli/dist/src/mods/claude-installs.js +83 -0
- package/v3/@claude-flow/cli/dist/src/mods/install.d.ts +64 -0
- package/v3/@claude-flow/cli/dist/src/mods/install.js +135 -0
- package/v3/@claude-flow/cli/dist/src/mods/policy-projection.d.ts +39 -0
- package/v3/@claude-flow/cli/dist/src/mods/policy-projection.js +65 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/mods/probe.js +118 -0
- package/v3/@claude-flow/cli/dist/src/plugins/manager.d.ts +37 -10
- package/v3/@claude-flow/cli/dist/src/plugins/manager.js +106 -20
- package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.d.ts +61 -0
- package/v3/@claude-flow/cli/dist/src/plugins/trust-policy.js +84 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +6 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +42 -2
- package/v3/@claude-flow/cli/package.json +2 -2
|
@@ -10,6 +10,20 @@ interface RefreshOptions {
|
|
|
10
10
|
lockRetryMsOverride?: number;
|
|
11
11
|
malformedLockStaleMsOverride?: number;
|
|
12
12
|
}
|
|
13
|
+
interface RefreshResult {
|
|
14
|
+
refreshed: boolean;
|
|
15
|
+
from?: string;
|
|
16
|
+
to?: string;
|
|
17
|
+
blocked?: string;
|
|
18
|
+
/** Set when `refreshed` was caused by #3565 healing, not a version bump —
|
|
19
|
+
* the stamp already matched but one or more critical helpers failed
|
|
20
|
+
* on-disk integrity verification and were re-copied from the verified
|
|
21
|
+
* source. */
|
|
22
|
+
healed?: boolean;
|
|
23
|
+
/** Critical helper names that failed integrity verification, present only
|
|
24
|
+
* alongside `healed: true`. */
|
|
25
|
+
tampered?: string[];
|
|
26
|
+
}
|
|
13
27
|
/**
|
|
14
28
|
* ruflo-owned helpers that carry hook logic (or the render surface for the
|
|
15
29
|
* funnel disclosure row) and must track the package version. Adding to this
|
|
@@ -19,6 +33,8 @@ interface RefreshOptions {
|
|
|
19
33
|
export declare const CRITICAL_HELPERS: string[];
|
|
20
34
|
/** Installed @claude-flow/cli version — the value the helpers are stamped with. */
|
|
21
35
|
export declare function getInstalledCliVersion(): string;
|
|
36
|
+
/** Locate the in-package `.claude/helpers` dir (the copy source). Null if not found. */
|
|
37
|
+
export declare function findPackageHelpersDir(): string | null;
|
|
22
38
|
/**
|
|
23
39
|
* On CLI startup, refresh critical helpers if their stamp is older than the
|
|
24
40
|
* installed CLI version. Two passes:
|
|
@@ -70,17 +86,8 @@ export declare function getInstalledCliVersion(): string;
|
|
|
70
86
|
* compat with pre-3.31.3 callers). If the global pass ran, its own result is
|
|
71
87
|
* carried in the optional `global` field.
|
|
72
88
|
*/
|
|
73
|
-
export declare function autoRefreshHelpersIfStale(cwd: string, opts?: RefreshOptions): Promise<{
|
|
74
|
-
|
|
75
|
-
from?: string;
|
|
76
|
-
to?: string;
|
|
77
|
-
blocked?: string;
|
|
78
|
-
global?: {
|
|
79
|
-
refreshed: boolean;
|
|
80
|
-
from?: string;
|
|
81
|
-
to?: string;
|
|
82
|
-
blocked?: string;
|
|
83
|
-
};
|
|
89
|
+
export declare function autoRefreshHelpersIfStale(cwd: string, opts?: RefreshOptions): Promise<RefreshResult & {
|
|
90
|
+
global?: RefreshResult;
|
|
84
91
|
}>;
|
|
85
92
|
export {};
|
|
86
93
|
//# sourceMappingURL=helper-refresh.d.ts.map
|
|
@@ -20,6 +20,8 @@ import { fileURLToPath } from 'url';
|
|
|
20
20
|
import { createRequire } from 'module';
|
|
21
21
|
import * as semver from 'semver';
|
|
22
22
|
import { verifyHelpersManifest, sha256Hex, HELPERS_MANIFEST_FILE, } from './helper-signing.js';
|
|
23
|
+
import { ensureCommonJsCompanions } from './helper-companions.js';
|
|
24
|
+
import { verifyInstalledCriticalHelpers } from './helper-integrity.js';
|
|
23
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
26
|
/**
|
|
25
27
|
* Walk up from `startDir` to the nearest ancestor whose `package.json` names
|
|
@@ -71,10 +73,11 @@ export const CRITICAL_HELPERS = [
|
|
|
71
73
|
// statusline.cjs is here so the funnel disclosure row (ADR-301) reaches
|
|
72
74
|
// existing installs on the next `ruflo` command, not only fresh `ruflo init`.
|
|
73
75
|
'statusline.cjs',
|
|
74
|
-
// router.
|
|
76
|
+
// router.cjs is loaded by hook-handler.cjs to label each prompt with an agent.
|
|
75
77
|
// Without it here, installs kept the pre-#2257 substring router forever
|
|
76
|
-
// ("latest" -> tester). ADR-389 / #3401.
|
|
77
|
-
|
|
78
|
+
// ("latest" -> tester). ADR-389 / #3401. It is CommonJS, so it ships as
|
|
79
|
+
// `.cjs`: a `.js` copy cannot load in a `"type":"module"` project (#3555).
|
|
80
|
+
'router.cjs',
|
|
78
81
|
];
|
|
79
82
|
function errorCode(error) {
|
|
80
83
|
return typeof error === 'object' && error !== null && 'code' in error
|
|
@@ -230,7 +233,7 @@ export function getInstalledCliVersion() {
|
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
/** Locate the in-package `.claude/helpers` dir (the copy source). Null if not found. */
|
|
233
|
-
function findPackageHelpersDir() {
|
|
236
|
+
export function findPackageHelpersDir() {
|
|
234
237
|
const candidates = [];
|
|
235
238
|
try {
|
|
236
239
|
const esmRequire = createRequire(import.meta.url);
|
|
@@ -313,7 +316,7 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
|
|
|
313
316
|
'hook-handler.cjs': gen.generateHookHandler(),
|
|
314
317
|
'intelligence.cjs': gen.generateIntelligenceStub(),
|
|
315
318
|
'auto-memory-hook.mjs': gen.generateAutoMemoryHook(),
|
|
316
|
-
'router.
|
|
319
|
+
'router.cjs': gen.generateAgentRouter(), // ADR-389 / #3555
|
|
317
320
|
// Fallback needs the same generator inputs `ruflo init` uses. We match the
|
|
318
321
|
// hardcoded default (maxAgents 15) because the fallback fires when the
|
|
319
322
|
// installed package is unresolvable — no way to read the user's project
|
|
@@ -339,9 +342,12 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
|
|
|
339
342
|
}
|
|
340
343
|
/**
|
|
341
344
|
* On CLI startup: if an initialized project's critical helpers are stamped older
|
|
342
|
-
* than the installed CLI version, silently re-copy them. Fast path is a
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
+
* than the installed CLI version, silently re-copy them. Fast path is a stamp
|
|
346
|
+
* read + string compare, PLUS a re-hash of each installed critical helper
|
|
347
|
+
* against the signed manifest (#3565) — still sub-ms for the handful of small
|
|
348
|
+
* files involved, and lock-free unless a mismatch is found. The copy runs at
|
|
349
|
+
* most once per version bump; the heal-on-tamper path can run on any call.
|
|
350
|
+
* Best-effort, never throws. No-op outside a ruflo project (requires an
|
|
345
351
|
* existing hook-handler.cjs — never creates files in an unrelated directory).
|
|
346
352
|
*
|
|
347
353
|
* FORWARD-ONLY (never downgrades): refreshing on any mere INEQUALITY, rather
|
|
@@ -365,6 +371,9 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
|
|
|
365
371
|
* `pubkeyPemOverride` let a test build its own tiny, throwaway-keypair-
|
|
366
372
|
* signed fixture and get real, deterministic coverage of the verify → hash →
|
|
367
373
|
* copy logic without depending on that.
|
|
374
|
+
*
|
|
375
|
+
* #3565: also re-verifies on every stamp-match call (see helper-integrity.ts)
|
|
376
|
+
* and heals a mismatch via this same verify-then-copy path.
|
|
368
377
|
*/
|
|
369
378
|
async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
|
|
370
379
|
if (!fs.existsSync(path.join(helpersDir, 'hook-handler.cjs')))
|
|
@@ -377,7 +386,9 @@ async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
|
|
|
377
386
|
// on this repo (CLAUDE.md "Concurrent-session helper corruption"). The
|
|
378
387
|
// existing semver.gte guard below still fires for normal installs — this
|
|
379
388
|
// is the escape hatch for the small set of users editing helpers directly.
|
|
380
|
-
// Applies to whichever dir this call is refreshing (project or global)
|
|
389
|
+
// Applies to whichever dir this call is refreshing (project or global),
|
|
390
|
+
// and also exempts it from the #3565 integrity re-check below — a
|
|
391
|
+
// deliberate local edit is not "tampering".
|
|
381
392
|
if (fs.existsSync(path.join(helpersDir, '.LOCKED'))) {
|
|
382
393
|
return { refreshed: false, blocked: '.LOCKED marker present — refresh skipped (delete to re-enable)' };
|
|
383
394
|
}
|
|
@@ -386,8 +397,28 @@ async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
|
|
|
386
397
|
stamped = fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim();
|
|
387
398
|
}
|
|
388
399
|
catch { /* pre-feature: unstamped */ }
|
|
389
|
-
if (stamped === version)
|
|
390
|
-
|
|
400
|
+
if (stamped === version) {
|
|
401
|
+
const source = opts.sourceDirOverride ?? findPackageHelpersDir();
|
|
402
|
+
const integrity = verifyInstalledCriticalHelpers(helpersDir, source, CRITICAL_HELPERS, opts.pubkeyPemOverride);
|
|
403
|
+
if (integrity.blocked)
|
|
404
|
+
return { refreshed: false, blocked: integrity.blocked };
|
|
405
|
+
if (integrity.tampered.length === 0)
|
|
406
|
+
return { refreshed: false }; // up to date AND verified intact
|
|
407
|
+
// Tampering detected post-install — heal via the same fail-closed
|
|
408
|
+
// verify-then-copy path a version-bump refresh already uses.
|
|
409
|
+
await opts.beforeWriteOverride?.();
|
|
410
|
+
const healRes = await writeCriticalHelpers(helpersDir, version, {
|
|
411
|
+
sourceDirOverride: opts.sourceDirOverride,
|
|
412
|
+
pubkeyPemOverride: opts.pubkeyPemOverride,
|
|
413
|
+
});
|
|
414
|
+
if (healRes.blocked)
|
|
415
|
+
return { refreshed: false, blocked: healRes.blocked };
|
|
416
|
+
if (healRes.wrote)
|
|
417
|
+
await ensureCommonJsCompanions(helpersDir); // #3555
|
|
418
|
+
return healRes.wrote
|
|
419
|
+
? { refreshed: true, healed: true, tampered: integrity.tampered, from: stamped, to: version }
|
|
420
|
+
: { refreshed: false };
|
|
421
|
+
}
|
|
391
422
|
if (stamped && semver.valid(stamped) && semver.valid(version) && semver.gte(stamped, version)) {
|
|
392
423
|
// Stamped version is already >= what this binary reports — refreshing
|
|
393
424
|
// would silently DOWNGRADE the helpers. Skip, untouched.
|
|
@@ -400,6 +431,8 @@ async function refreshOneHelpersDirLocked(helpersDir, version, opts) {
|
|
|
400
431
|
});
|
|
401
432
|
if (res.blocked)
|
|
402
433
|
return { refreshed: false, blocked: res.blocked };
|
|
434
|
+
if (res.wrote)
|
|
435
|
+
await ensureCommonJsCompanions(helpersDir); // #3555
|
|
403
436
|
return res.wrote ? { refreshed: true, from: stamped || '(unstamped)', to: version } : { refreshed: false };
|
|
404
437
|
}
|
|
405
438
|
async function refreshOneHelpersDir(helpersDir, version, opts) {
|
|
@@ -412,8 +445,18 @@ async function refreshOneHelpersDir(helpersDir, version, opts) {
|
|
|
412
445
|
return { refreshed: false, blocked: '.LOCKED marker present — refresh skipped (delete to re-enable)' };
|
|
413
446
|
}
|
|
414
447
|
try {
|
|
415
|
-
if (fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim() === version)
|
|
416
|
-
|
|
448
|
+
if (fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim() === version) {
|
|
449
|
+
// #3565: re-hash before trusting a stamp match (helper-integrity.ts).
|
|
450
|
+
// Read-only — no lock needed unless something's actually wrong; the
|
|
451
|
+
// (rare) heal path below re-verifies under lock.
|
|
452
|
+
const source = opts.sourceDirOverride ?? findPackageHelpersDir();
|
|
453
|
+
const integrity = verifyInstalledCriticalHelpers(helpersDir, source, CRITICAL_HELPERS, opts.pubkeyPemOverride);
|
|
454
|
+
if (integrity.blocked)
|
|
455
|
+
return { refreshed: false, blocked: integrity.blocked };
|
|
456
|
+
if (integrity.tampered.length === 0)
|
|
457
|
+
return { refreshed: false };
|
|
458
|
+
// else: tampering found — fall through to acquire the lock and heal.
|
|
459
|
+
}
|
|
417
460
|
}
|
|
418
461
|
catch { /* unstamped: continue to the locked path */ }
|
|
419
462
|
const releaseLock = await acquireRefreshLock(helpersDir, opts);
|
|
@@ -242,7 +242,7 @@ const [,, command, ...args] = process.argv;
|
|
|
242
242
|
if (command && commands[command]) {
|
|
243
243
|
commands[command](...args);
|
|
244
244
|
} else {
|
|
245
|
-
console.log('Usage: session.
|
|
245
|
+
console.log('Usage: session.cjs <start|restore|end|status|update|metric> [args]');
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
module.exports = commands;
|
|
@@ -336,7 +336,7 @@ if (require.main === module) {
|
|
|
336
336
|
const result = routeTask(task);
|
|
337
337
|
console.log(JSON.stringify(result, null, 2));
|
|
338
338
|
} else {
|
|
339
|
-
console.log('Usage: router.
|
|
339
|
+
console.log('Usage: router.cjs <task description>');
|
|
340
340
|
console.log('\\nAvailable agents:', Object.keys(AGENT_CAPABILITIES).join(', '));
|
|
341
341
|
}
|
|
342
342
|
}
|
|
@@ -427,7 +427,7 @@ const value = valueParts.join(' ');
|
|
|
427
427
|
if (command && commands[command]) {
|
|
428
428
|
commands[command](key, value);
|
|
429
429
|
} else {
|
|
430
|
-
console.log('Usage: memory.
|
|
430
|
+
console.log('Usage: memory.cjs <get|set|delete|clear|keys> [key] [value]');
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
module.exports = commands;
|
|
@@ -504,9 +504,9 @@ export function generateHookHandler() {
|
|
|
504
504
|
' return null;',
|
|
505
505
|
'}',
|
|
506
506
|
'',
|
|
507
|
-
"const router = safeRequire(path.join(helpersDir, 'router.
|
|
508
|
-
"const session = safeRequire(path.join(helpersDir, 'session.
|
|
509
|
-
"const memory = safeRequire(path.join(helpersDir, 'memory.
|
|
507
|
+
"const router = safeRequire(path.join(helpersDir, 'router.cjs'));",
|
|
508
|
+
"const session = safeRequire(path.join(helpersDir, 'session.cjs'));",
|
|
509
|
+
"const memory = safeRequire(path.join(helpersDir, 'memory.cjs'));",
|
|
510
510
|
"const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs'));",
|
|
511
511
|
'',
|
|
512
512
|
'const [,, command, ...args] = process.argv;',
|
|
@@ -530,7 +530,19 @@ export function generateHookHandler() {
|
|
|
530
530
|
' });',
|
|
531
531
|
'}',
|
|
532
532
|
'',
|
|
533
|
+
// ADR-404: same handshake as the shipped hook-handler.cjs, so a helper
|
|
534
|
+
// regenerated by the refresh fallback still hands route/post-edit to the mod.
|
|
535
|
+
'// ADR-404: the ruflo mod names the events it runs in-process in',
|
|
536
|
+
'// RUFLO_MODS_OWNS; only side-effect events hand over, guards always run.',
|
|
537
|
+
"const MOD_OWNABLE_EVENTS = new Set(['route', 'post-edit']);",
|
|
538
|
+
'function ownedByMod(cmd, env) {',
|
|
539
|
+
' env = env || process.env;',
|
|
540
|
+
' if (!MOD_OWNABLE_EVENTS.has(cmd)) return false;',
|
|
541
|
+
" return String(env.RUFLO_MODS_OWNS || '').split(',').some(function (owned) { return owned.trim() === cmd; });",
|
|
542
|
+
'}',
|
|
543
|
+
'',
|
|
533
544
|
'async function main() {',
|
|
545
|
+
' if (ownedByMod(command)) return;',
|
|
534
546
|
' let stdinData = "";',
|
|
535
547
|
' try { stdinData = await readStdin(); } catch (e) { /* ignore */ }',
|
|
536
548
|
' let hookInput = {};',
|
|
@@ -1358,7 +1370,7 @@ const [,, command, ...args] = process.argv;
|
|
|
1358
1370
|
if (command && commands[command]) {
|
|
1359
1371
|
commands[command](...args);
|
|
1360
1372
|
} else {
|
|
1361
|
-
console.log('Usage: session.
|
|
1373
|
+
console.log('Usage: session.cjs <start|restore|end|status>');
|
|
1362
1374
|
console.log(\`Platform: \${platform}\`);
|
|
1363
1375
|
console.log(\`Data dir: \${SESSION_DIR}\`);
|
|
1364
1376
|
}
|
|
@@ -1376,9 +1388,9 @@ export function generateHelpers(options) {
|
|
|
1376
1388
|
helpers['pre-commit'] = generatePreCommitHook();
|
|
1377
1389
|
helpers['post-commit'] = generatePostCommitHook();
|
|
1378
1390
|
// Cross-platform Node.js scripts
|
|
1379
|
-
helpers['session.
|
|
1380
|
-
helpers['router.
|
|
1381
|
-
helpers['memory.
|
|
1391
|
+
helpers['session.cjs'] = generateCrossPlatformSessionManager();
|
|
1392
|
+
helpers['router.cjs'] = generateAgentRouter();
|
|
1393
|
+
helpers['memory.cjs'] = generateMemoryHelper();
|
|
1382
1394
|
// Windows-specific scripts
|
|
1383
1395
|
helpers['daemon-manager.ps1'] = generateWindowsDaemonManager();
|
|
1384
1396
|
helpers['daemon-manager.cmd'] = generateWindowsBatchWrapper();
|
|
@@ -12,11 +12,24 @@ import { type RouterEmbedderKind } from '../ruvector/router-embedder.js';
|
|
|
12
12
|
export declare function scrubReasoningBlocks(text: string): string;
|
|
13
13
|
/** Test hook: drop the cached semantic index so the next route rebuilds it. */
|
|
14
14
|
export declare function resetSemanticRouterForTests(): void;
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Confidence for a task nothing matched (#3567). It must rank below every real
|
|
17
|
+
* signal: keyword hits are >= 0.8, learned outcomes >= 0.7, and a semantic
|
|
18
|
+
* match is only eligible above 0.4. Matches the helper router's 0.3 fall-through.
|
|
19
|
+
*/
|
|
20
|
+
export declare const NO_MATCH_CONFIDENCE = 0.3;
|
|
21
|
+
export interface TaskAgentSuggestion {
|
|
17
22
|
agents: string[];
|
|
18
23
|
confidence: number;
|
|
19
|
-
|
|
24
|
+
/** false when no keyword or learned pattern matched; the agents are a default, not a decision. */
|
|
25
|
+
matched: boolean;
|
|
26
|
+
reason?: 'no-match-default';
|
|
27
|
+
note?: string;
|
|
28
|
+
}
|
|
29
|
+
/** True when the task has at least one letter or digit in any script. */
|
|
30
|
+
export declare function hasRoutableText(task: string): boolean;
|
|
31
|
+
/** Exported for tests. */
|
|
32
|
+
export declare function suggestAgentsForTask(task: string): TaskAgentSuggestion;
|
|
20
33
|
export declare const hooksPreEdit: MCPTool;
|
|
21
34
|
export declare const hooksPostEdit: MCPTool;
|
|
22
35
|
export declare const hooksPreCommand: MCPTool;
|
|
@@ -722,12 +722,41 @@ const KEYWORD_MATCHERS = Object.entries(KEYWORD_PATTERNS).map(([keyword, result]
|
|
|
722
722
|
const body = /[\s/]/.test(keyword) ? escaped : `${escaped}(?:s|es|ing|ed)?`;
|
|
723
723
|
return { regex: new RegExp(`\\b${body}\\b`, 'i'), result };
|
|
724
724
|
});
|
|
725
|
+
/**
|
|
726
|
+
* Confidence for a task nothing matched (#3567). It must rank below every real
|
|
727
|
+
* signal: keyword hits are >= 0.8, learned outcomes >= 0.7, and a semantic
|
|
728
|
+
* match is only eligible above 0.4. Matches the helper router's 0.3 fall-through.
|
|
729
|
+
*/
|
|
730
|
+
export const NO_MATCH_CONFIDENCE = 0.3;
|
|
731
|
+
/** True when the task has at least one letter or digit in any script. */
|
|
732
|
+
export function hasRoutableText(task) {
|
|
733
|
+
return /[\p{L}\p{N}]/u.test(task);
|
|
734
|
+
}
|
|
735
|
+
// Keyword matchers are English regexes; letters outside Latin script can never hit them.
|
|
736
|
+
const NON_LATIN_LETTER = /(?![A-Za-zÀ-ɏ])\p{L}/u;
|
|
737
|
+
function noMatchNote(task) {
|
|
738
|
+
if (!hasRoutableText(task))
|
|
739
|
+
return 'Task has no words to match.';
|
|
740
|
+
if (NON_LATIN_LETTER.test(task)) {
|
|
741
|
+
return 'Keyword matchers are English-only, so non-English text cannot match them.';
|
|
742
|
+
}
|
|
743
|
+
return 'No keyword or learned pattern matched.';
|
|
744
|
+
}
|
|
725
745
|
/** Exported for tests. */
|
|
726
746
|
export function suggestAgentsForTask(task) {
|
|
747
|
+
const noMatch = () => ({
|
|
748
|
+
agents: ['coder', 'researcher', 'tester'],
|
|
749
|
+
confidence: NO_MATCH_CONFIDENCE,
|
|
750
|
+
matched: false,
|
|
751
|
+
reason: 'no-match-default',
|
|
752
|
+
note: noMatchNote(task),
|
|
753
|
+
});
|
|
754
|
+
if (!hasRoutableText(task))
|
|
755
|
+
return noMatch();
|
|
727
756
|
// Check static keyword patterns first
|
|
728
757
|
for (const { regex, result } of KEYWORD_MATCHERS) {
|
|
729
758
|
if (regex.test(task)) {
|
|
730
|
-
return result;
|
|
759
|
+
return { ...result, matched: true };
|
|
731
760
|
}
|
|
732
761
|
}
|
|
733
762
|
// Check runtime-learned patterns from successful task outcomes
|
|
@@ -747,11 +776,10 @@ export function suggestAgentsForTask(task) {
|
|
|
747
776
|
}
|
|
748
777
|
// Require at least 2 keyword overlap to prevent false positives
|
|
749
778
|
if (bestAgent && bestOverlap >= 2) {
|
|
750
|
-
return { agents: [bestAgent], confidence: Math.min(0.6 + bestOverlap * 0.05, 0.85) };
|
|
779
|
+
return { agents: [bestAgent], confidence: Math.min(0.6 + bestOverlap * 0.05, 0.85), matched: true };
|
|
751
780
|
}
|
|
752
781
|
}
|
|
753
|
-
|
|
754
|
-
return { agents: ['coder', 'researcher', 'tester'], confidence: 0.7 };
|
|
782
|
+
return noMatch();
|
|
755
783
|
}
|
|
756
784
|
function assessCommandRisk(command) {
|
|
757
785
|
const warnings = [];
|
|
@@ -1205,6 +1233,7 @@ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride
|
|
|
1205
1233
|
let agents;
|
|
1206
1234
|
let confidence;
|
|
1207
1235
|
let matchedPattern = '';
|
|
1236
|
+
let noMatchDetail = null;
|
|
1208
1237
|
// Both static and learned patterns are gated on the same similarity
|
|
1209
1238
|
// score. Learned patterns additionally require support/reliability as a
|
|
1210
1239
|
// quality guard, but do NOT need a higher score bar — a learned pattern
|
|
@@ -1212,7 +1241,9 @@ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride
|
|
|
1212
1241
|
// (#2864: a 25pp higher threshold made a top-scoring learned-researcher
|
|
1213
1242
|
// match at 0.57 lose to a static match at 0.52, discarding the learned
|
|
1214
1243
|
// store's output on the majority of routes).
|
|
1215
|
-
|
|
1244
|
+
// A task with no letters or digits has nothing for the embedder to mean;
|
|
1245
|
+
// any similarity it scores is noise, so it never becomes a semantic match.
|
|
1246
|
+
const eligibleSemantic = !hasRoutableText(task) ? undefined : semanticResult.find((match) => {
|
|
1216
1247
|
if (match.score <= 0.4)
|
|
1217
1248
|
return false;
|
|
1218
1249
|
const learned = match.intent.startsWith('learned-') || match.metadata.source === 'learned';
|
|
@@ -1232,9 +1263,16 @@ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride
|
|
|
1232
1263
|
const suggestion = suggestAgentsForTask(task);
|
|
1233
1264
|
agents = suggestion.agents;
|
|
1234
1265
|
confidence = suggestion.confidence;
|
|
1235
|
-
matchedPattern = 'keyword-fallback';
|
|
1236
1266
|
routingMethod = 'keyword';
|
|
1237
|
-
|
|
1267
|
+
if (suggestion.matched) {
|
|
1268
|
+
matchedPattern = 'keyword-fallback';
|
|
1269
|
+
backendInfo = 'keyword matching';
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
matchedPattern = 'no-match-default';
|
|
1273
|
+
backendInfo = 'keyword matching (no match)';
|
|
1274
|
+
noMatchDetail = suggestion.note ?? 'No pattern matched.';
|
|
1275
|
+
}
|
|
1238
1276
|
}
|
|
1239
1277
|
// Determine complexity
|
|
1240
1278
|
const taskLower = task.toLowerCase();
|
|
@@ -1252,6 +1290,9 @@ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride
|
|
|
1252
1290
|
throughput: routingLatencyMs > 0 ? `${Math.round(1000 / routingLatencyMs)} routes/s` : 'N/A',
|
|
1253
1291
|
},
|
|
1254
1292
|
matchedPattern,
|
|
1293
|
+
// #3567: callers branch on `matched`; a no-match result is a default, not a decision.
|
|
1294
|
+
matched: noMatchDetail === null,
|
|
1295
|
+
...(noMatchDetail !== null ? { reason: 'no-match-default', note: noMatchDetail } : {}),
|
|
1255
1296
|
semanticMatches: semanticResult.slice(0, 3).map(r => ({
|
|
1256
1297
|
pattern: r.intent,
|
|
1257
1298
|
score: Math.round(r.score * 100) / 100,
|
|
@@ -1261,15 +1302,18 @@ async function routeTaskLocal(task, context, useSemanticRouter, embedderOverride
|
|
|
1261
1302
|
confidence: Math.round(confidence * 100) / 100,
|
|
1262
1303
|
reason: routingMethod.startsWith('semantic')
|
|
1263
1304
|
? `Semantic similarity to "${matchedPattern}" pattern (${Math.round(confidence * 100)}%)`
|
|
1264
|
-
:
|
|
1305
|
+
: noMatchDetail !== null
|
|
1306
|
+
? `Nothing matched; default suggestion only. ${noMatchDetail}`
|
|
1307
|
+
: `Task contains keywords matching ${agents[0]} specialization`,
|
|
1265
1308
|
},
|
|
1266
1309
|
alternativeAgents: agents.slice(1).map((agent, i) => ({
|
|
1267
1310
|
type: agent,
|
|
1268
|
-
confidence: Math.round((confidence - (0.1 * (i + 1))) * 100) / 100,
|
|
1269
|
-
reason: `Alternative agent for ${agent} capabilities`,
|
|
1311
|
+
confidence: Math.max(0, Math.round((confidence - (0.1 * (i + 1))) * 100) / 100),
|
|
1312
|
+
reason: noMatchDetail !== null ? 'Default suggestion (nothing matched)' : `Alternative agent for ${agent} capabilities`,
|
|
1270
1313
|
})),
|
|
1271
1314
|
estimatedMetrics: {
|
|
1272
|
-
|
|
1315
|
+
// No pattern means no basis for a success estimate.
|
|
1316
|
+
successProbability: noMatchDetail !== null ? null : Math.round(confidence * 100) / 100,
|
|
1273
1317
|
estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
|
|
1274
1318
|
complexity,
|
|
1275
1319
|
},
|
|
@@ -1506,12 +1550,15 @@ export const hooksPreTask = {
|
|
|
1506
1550
|
return {
|
|
1507
1551
|
taskId,
|
|
1508
1552
|
description,
|
|
1553
|
+
agentsMatched: suggestion.matched,
|
|
1509
1554
|
suggestedAgents: suggestion.agents.map((agent, i) => ({
|
|
1510
1555
|
type: agent,
|
|
1511
1556
|
confidence: suggestion.confidence - (0.05 * i),
|
|
1512
|
-
reason:
|
|
1513
|
-
?
|
|
1514
|
-
:
|
|
1557
|
+
reason: !suggestion.matched
|
|
1558
|
+
? 'Default suggestion (nothing matched)'
|
|
1559
|
+
: i === 0
|
|
1560
|
+
? `Primary agent for ${agent} tasks based on learned patterns`
|
|
1561
|
+
: `Alternative agent with ${agent} capabilities`,
|
|
1515
1562
|
})),
|
|
1516
1563
|
complexity,
|
|
1517
1564
|
estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min',
|
|
@@ -1856,8 +1903,34 @@ export const hooksExplain = {
|
|
|
1856
1903
|
catch {
|
|
1857
1904
|
// File unreadable; leave as null
|
|
1858
1905
|
}
|
|
1906
|
+
// #3567: when nothing matched, say so instead of explaining a match that did not happen.
|
|
1907
|
+
if (!suggestion.matched) {
|
|
1908
|
+
return {
|
|
1909
|
+
task,
|
|
1910
|
+
matched: false,
|
|
1911
|
+
reason: 'no-match-default',
|
|
1912
|
+
explanation: `No keyword or learned pattern matched this task. ${suggestion.note ?? ''} ` +
|
|
1913
|
+
`"${suggestion.agents[0]}" is a default suggestion, not a routing decision.`.trim(),
|
|
1914
|
+
factors: [
|
|
1915
|
+
{ factor: 'Keyword Match', weight: 0.4, value: null, impact: 'No keyword matched' },
|
|
1916
|
+
{ factor: 'Historical Success', weight: 0.3, value: historicalSuccess, impact: historicalNote },
|
|
1917
|
+
{ factor: 'Agent Availability', weight: 0.2, value: null, impact: 'Agent availability tracking not implemented' },
|
|
1918
|
+
{ factor: 'Task Complexity', weight: 0.1, value: task.length > 100 ? 0.8 : 0.3, impact: 'Complexity assessment' },
|
|
1919
|
+
],
|
|
1920
|
+
patterns: matchedPatterns,
|
|
1921
|
+
decision: {
|
|
1922
|
+
agent: suggestion.agents[0],
|
|
1923
|
+
confidence: suggestion.confidence,
|
|
1924
|
+
reasoning: [
|
|
1925
|
+
'No pattern matched; the agent is a default suggestion',
|
|
1926
|
+
`Confidence ${(suggestion.confidence * 100).toFixed(0)}% is below every real match`,
|
|
1927
|
+
],
|
|
1928
|
+
},
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1859
1931
|
return {
|
|
1860
1932
|
task,
|
|
1933
|
+
matched: true,
|
|
1861
1934
|
explanation: `The routing decision was made based on keyword analysis of the task description. ` +
|
|
1862
1935
|
`The task contains keywords that match the "${suggestion.agents[0]}" specialization with ${(suggestion.confidence * 100).toFixed(0)}% confidence.`,
|
|
1863
1936
|
factors: [
|
|
@@ -10,5 +10,12 @@
|
|
|
10
10
|
* @module v3/cli/mcp-tools/memory-tools
|
|
11
11
|
*/
|
|
12
12
|
import type { MCPTool } from './types.js';
|
|
13
|
+
export declare const DANGEROUS_KEY_PATTERN: RegExp;
|
|
14
|
+
/**
|
|
15
|
+
* #3570: the one key rule for every memory write path (MCP store, CLI store,
|
|
16
|
+
* import). Plain `/` stays legal (`probe/x`); traversal and shell metacharacters
|
|
17
|
+
* do not. Returns the error message, or null when the key is acceptable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function memoryKeyError(key: string): string | null;
|
|
13
20
|
export declare const memoryTools: MCPTool[];
|
|
14
21
|
//# sourceMappingURL=memory-tools.d.ts.map
|
|
@@ -42,7 +42,15 @@ const MAX_QUERY_LENGTH = 4096;
|
|
|
42
42
|
// validateMemoryInput. Imported by sanitizeMemoryKey so write-side sanitization
|
|
43
43
|
// and read-side rejection can never drift apart (the symmetry bug behind #1884).
|
|
44
44
|
const DANGEROUS_KEY_CHARS = /[;&|`$(){}[\]<>!#\\\0]|\.\.[/\\]/g;
|
|
45
|
-
const DANGEROUS_KEY_PATTERN = /[;&|`$(){}[\]<>!#\\\0]|\.\.[/\\]/;
|
|
45
|
+
export const DANGEROUS_KEY_PATTERN = /[;&|`$(){}[\]<>!#\\\0]|\.\.[/\\]/;
|
|
46
|
+
/**
|
|
47
|
+
* #3570: the one key rule for every memory write path (MCP store, CLI store,
|
|
48
|
+
* import). Plain `/` stays legal (`probe/x`); traversal and shell metacharacters
|
|
49
|
+
* do not. Returns the error message, or null when the key is acceptable.
|
|
50
|
+
*/
|
|
51
|
+
export function memoryKeyError(key) {
|
|
52
|
+
return DANGEROUS_KEY_PATTERN.test(key) ? 'Key contains disallowed characters' : null;
|
|
53
|
+
}
|
|
46
54
|
function validateMemoryInput(key, value, query, namespace) {
|
|
47
55
|
if (key && key.length > MAX_KEY_LENGTH) {
|
|
48
56
|
throw new Error(`Key exceeds maximum length of ${MAX_KEY_LENGTH} characters`);
|
|
@@ -54,9 +62,9 @@ function validateMemoryInput(key, value, query, namespace) {
|
|
|
54
62
|
throw new Error(`Query exceeds maximum length of ${MAX_QUERY_LENGTH} characters`);
|
|
55
63
|
}
|
|
56
64
|
// Reject path traversal and shell metacharacters in keys/namespaces (#1425)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
const keyError = key ? memoryKeyError(key) : null;
|
|
66
|
+
if (keyError)
|
|
67
|
+
throw new Error(keyError);
|
|
60
68
|
if (namespace && DANGEROUS_KEY_PATTERN.test(namespace)) {
|
|
61
69
|
throw new Error('Namespace contains disallowed characters');
|
|
62
70
|
}
|
|
@@ -413,6 +421,11 @@ export const memoryTools = [
|
|
|
413
421
|
};
|
|
414
422
|
}
|
|
415
423
|
validateMemoryInput(key, value, undefined, namespace);
|
|
424
|
+
// #3570: a namespace written here must be exportable and purgeable, so it
|
|
425
|
+
// passes the same validator export and purge use.
|
|
426
|
+
const vNs = validateIdentifier(namespace, 'namespace');
|
|
427
|
+
if (!vNs.valid)
|
|
428
|
+
throw new Error(vNs.error);
|
|
416
429
|
const startTime = performance.now();
|
|
417
430
|
try {
|
|
418
431
|
const result = await storeEntry({
|
|
@@ -1542,6 +1555,9 @@ export const memoryTools = [
|
|
|
1542
1555
|
await ensureInitialized(dbPath);
|
|
1543
1556
|
const { storeEntry } = await getMemoryFunctions();
|
|
1544
1557
|
const t0 = Date.now();
|
|
1558
|
+
// Values are re-embedded on import; count the vectors actually written
|
|
1559
|
+
// rather than reporting a constant 0 next to entries that show a vector.
|
|
1560
|
+
let vectors = 0;
|
|
1545
1561
|
const inputPath = String(input.inputPath ?? '');
|
|
1546
1562
|
if (!inputPath || !existsSync(inputPath))
|
|
1547
1563
|
return { error: `File not found: ${inputPath || '(empty)'}` };
|
|
@@ -1559,6 +1575,24 @@ export const memoryTools = [
|
|
|
1559
1575
|
if (!v.valid)
|
|
1560
1576
|
throw new Error(v.error);
|
|
1561
1577
|
}
|
|
1578
|
+
// #3570: validate every entry's namespace up front so a bad file writes nothing.
|
|
1579
|
+
if (!nsOverride) {
|
|
1580
|
+
for (const e of entries) {
|
|
1581
|
+
if (e && typeof e.key === 'string' && e.namespace !== undefined) {
|
|
1582
|
+
const v = validateIdentifier(String(e.namespace), 'namespace');
|
|
1583
|
+
if (!v.valid)
|
|
1584
|
+
throw new Error(v.error);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
// #3570 follow-up: keys get the same up-front, all-or-nothing check.
|
|
1589
|
+
for (const e of entries) {
|
|
1590
|
+
if (e && typeof e.key === 'string') {
|
|
1591
|
+
const keyError = memoryKeyError(e.key);
|
|
1592
|
+
if (keyError)
|
|
1593
|
+
throw new Error(`${keyError}: ${JSON.stringify(e.key)}`);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1562
1596
|
let imported = 0;
|
|
1563
1597
|
let skipped = 0;
|
|
1564
1598
|
for (const e of entries) {
|
|
@@ -1569,8 +1603,11 @@ export const memoryTools = [
|
|
|
1569
1603
|
const value = typeof e.value === 'string' ? e.value : JSON.stringify(e.value ?? null);
|
|
1570
1604
|
try {
|
|
1571
1605
|
const result = await storeEntry({ key: e.key, value, namespace: nsOverride ?? e.namespace ?? 'default', upsert: input.merge !== false, dbPath });
|
|
1572
|
-
if (result.success)
|
|
1606
|
+
if (result.success) {
|
|
1573
1607
|
imported++;
|
|
1608
|
+
if (result.embedding)
|
|
1609
|
+
vectors++;
|
|
1610
|
+
}
|
|
1574
1611
|
else
|
|
1575
1612
|
skipped++;
|
|
1576
1613
|
}
|
|
@@ -1580,7 +1617,7 @@ export const memoryTools = [
|
|
|
1580
1617
|
}
|
|
1581
1618
|
return {
|
|
1582
1619
|
inputPath,
|
|
1583
|
-
imported: { entries: imported, vectors
|
|
1620
|
+
imported: { entries: imported, vectors, patterns: 0 },
|
|
1584
1621
|
skipped,
|
|
1585
1622
|
duration: Date.now() - t0,
|
|
1586
1623
|
};
|
|
@@ -4,5 +4,20 @@
|
|
|
4
4
|
* Tool definitions for session management with file persistence.
|
|
5
5
|
*/
|
|
6
6
|
import { type MCPTool } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* #3573: what session_save learned about memory, so callers can say
|
|
9
|
+
* "not included" or "no store" instead of printing an invented 0.
|
|
10
|
+
*/
|
|
11
|
+
export interface MemoryCaptureReport {
|
|
12
|
+
requested: boolean;
|
|
13
|
+
status: 'not-requested' | 'captured' | 'no-store' | 'error';
|
|
14
|
+
entries: number;
|
|
15
|
+
sources: {
|
|
16
|
+
memoryDb: number;
|
|
17
|
+
agentdb: number;
|
|
18
|
+
legacyJson: number;
|
|
19
|
+
};
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
7
22
|
export declare const sessionTools: MCPTool[];
|
|
8
23
|
//# sourceMappingURL=session-tools.d.ts.map
|