@claude-flow/cli 3.54.0 → 3.54.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/helpers.manifest.json +3 -3
- package/.claude/helpers/hook-handler.cjs +65 -3
- package/catalog-manifest.json +2 -2
- package/dist/src/blocking-stdio.d.ts +11 -0
- package/dist/src/blocking-stdio.js +23 -0
- package/dist/src/commands/session.js +11 -37
- package/dist/src/index.js +3 -0
- package/dist/src/init/executor.js +1 -0
- package/dist/src/init/helpers-generator.js +65 -3
- package/dist/src/mcp-tools/agentdb-tools.js +13 -1
- package/dist/src/mcp-tools/hooks-tools.js +1 -1
- package/dist/src/mcp-tools/session-tools.js +4 -1
- package/dist/src/memory/graph-edge-writer.js +3 -2
- package/dist/src/memory/lucene-bm25.js +11 -19
- package/dist/src/memory/memory-bridge.d.ts +1 -0
- package/dist/src/memory/memory-bridge.js +14 -1
- package/dist/src/memory/memory-initializer.js +5 -4
- package/dist/src/memory/shared-sqlite.d.ts +7 -0
- package/dist/src/memory/shared-sqlite.js +45 -0
- package/dist/src/missions/schemas.d.ts +125 -125
- package/dist/src/mods/command-registry/schema.d.ts +199 -199
- package/dist/src/mods/probe.d.ts +7 -0
- package/dist/src/mods/probe.js +29 -1
- package/dist/src/production/monitoring.d.ts +2 -0
- package/dist/src/production/monitoring.js +8 -3
- package/dist/src/production/rate-limiter.js +8 -5
- package/dist/src/production/retry.js +2 -0
- package/dist/src/services/git-workspace-identity.js +20 -3
- package/dist/src/services/worker-queue.d.ts +1 -0
- package/dist/src/services/worker-queue.js +9 -1
- package/package.json +2 -2
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.54.
|
|
3
|
+
"version": "3.54.1",
|
|
4
4
|
"files": {
|
|
5
5
|
"auto-memory-hook.mjs": "c8d218526a3c34034c2a69de23f507f0eb0f70d59c16426a8edc849e6ac22529",
|
|
6
|
-
"hook-handler.cjs": "
|
|
6
|
+
"hook-handler.cjs": "34d2aacfb59a73ff94b1467bb57ab9d9c4af89c0e470aadf3bd0d74a503a658d",
|
|
7
7
|
"intelligence.cjs": "ac5033a77bcea9796856629d5d319bde9ab6b7edf0dcd0d4af187ce30af90836",
|
|
8
8
|
"statusline.cjs": "4a48353b4f1566fa4379b00fd0321b6676a22b6cc91fbbd8380ac5183d619468",
|
|
9
9
|
"router.cjs": "16648b3c0d73b6681ac2d8d13def6a68286c57f5a11004de5b19a0fa088c833d"
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
|
-
"signature": "
|
|
12
|
+
"signature": "GORi4ozu4COfL048CD+GWNMiX4hdXVrn2/tvRXWcj9OioRiUknKlfhNwkL6PVtcNar+5xDn7J9BzGFBrPgblCA==",
|
|
13
13
|
"algorithm": "ed25519"
|
|
14
14
|
}
|
|
@@ -486,6 +486,7 @@ const handlers = {
|
|
|
486
486
|
// Join literal quote fragments and normalize root paths without shell evaluation.
|
|
487
487
|
function hasRootDelete(command, depth = 0) {
|
|
488
488
|
let word = '', quote = '', started = false, redirect = false
|
|
489
|
+
let ansiNul = false // a NUL inside $'...' ends that string, as in bash
|
|
489
490
|
let inRm = false, optionsEnded = false, recursive = false, force = false, root = false
|
|
490
491
|
const isRoot = (operand) => {
|
|
491
492
|
if (!operand.startsWith('/')) return false
|
|
@@ -497,22 +498,55 @@ const handlers = {
|
|
|
497
498
|
}
|
|
498
499
|
return parts.length === 0 || /[*?\[]/.test(parts[0])
|
|
499
500
|
}
|
|
501
|
+
// One backslash escape inside $'...', from the character after the backslash.
|
|
502
|
+
// Returns the text it stands for and the index of its last character. An unknown escape keeps its backslash.
|
|
503
|
+
// The command arrives lowercased, so an uppercase-U escape reads as a lowercase one: eight digits starting 0000 are one code point.
|
|
504
|
+
const ansiEscape = (s, at) => {
|
|
505
|
+
const c = s[at]
|
|
506
|
+
const simple = { a: '\x07', b: '\b', e: '\x1b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v', '\\': '\\', "'": "'", '"': '"', '?': '?' }
|
|
507
|
+
if (simple[c] !== undefined) return [simple[c], at]
|
|
508
|
+
const digits = (from, max, pattern) => pattern.exec(s.slice(from, from + max))?.[0]
|
|
509
|
+
const octal = digits(at, 3, /^[0-7]+/)
|
|
510
|
+
if (octal) return [String.fromCharCode(parseInt(octal, 8) & 255), at + octal.length - 1]
|
|
511
|
+
const hex = c === 'x' ? digits(at + 1, 2, /^[0-9a-f]+/) : undefined
|
|
512
|
+
if (hex) return [String.fromCharCode(parseInt(hex, 16)), at + hex.length]
|
|
513
|
+
const wide = c === 'u' ? (digits(at + 1, 8, /^0000[0-9a-f]{4}$/) ?? digits(at + 1, 4, /^[0-9a-f]+/)) : undefined
|
|
514
|
+
if (wide) return [String.fromCodePoint(parseInt(wide, 16)), at + wide.length]
|
|
515
|
+
if (c === 'c' && at + 1 < s.length) return [String.fromCharCode(s.charCodeAt(at + 1) & 31), at + 1]
|
|
516
|
+
return ['\\' + c, at]
|
|
517
|
+
}
|
|
500
518
|
const finishWord = () => {
|
|
501
519
|
if (!started) return false
|
|
502
520
|
// Literal shell strings (e.g. sh -c 'rm -rf /') also carried the old guard.
|
|
503
521
|
// Bound rescanning to four levels; beyond that retain its conservative check.
|
|
504
|
-
if (
|
|
522
|
+
if (/[\s;&|()\x60]/.test(word)) {
|
|
505
523
|
if (depth < 4 ? hasRootDelete(word, depth + 1) : word.includes('rm -rf /')) return true
|
|
506
524
|
}
|
|
507
525
|
if (!inRm) inRm = word === 'rm' || word.endsWith('/rm')
|
|
508
526
|
else if (!optionsEnded && word === '--') optionsEnded = true
|
|
509
527
|
else if (!optionsEnded && word.startsWith('-')) {
|
|
510
|
-
|
|
511
|
-
|
|
528
|
+
// GNU getopt accepts any unambiguous long-option prefix: --r, --recur, --forc.
|
|
529
|
+
recursive = recursive || (word.length > 2 && '--recursive'.startsWith(word)) || /^-[a-z]*r[a-z]*$/.test(word)
|
|
530
|
+
force = force || (word.length > 2 && '--force'.startsWith(word)) || /^-[a-z]*f[a-z]*$/.test(word)
|
|
512
531
|
} else root = root || isRoot(word)
|
|
513
532
|
word = ''; started = false
|
|
514
533
|
return inRm && recursive && force && root
|
|
515
534
|
}
|
|
535
|
+
// A command substitution's body, from just after its opening backtick. Bash removes a
|
|
536
|
+
// backslash only before $, a backtick or a backslash (and " inside double quotes) and
|
|
537
|
+
// drops backslash-newline; any other backslash stays for the nested scan.
|
|
538
|
+
const substitution = (from, inDouble) => {
|
|
539
|
+
let body = '', j = from
|
|
540
|
+
for (; j < command.length && command[j] !== '\x60'; j++) {
|
|
541
|
+
const next = command[j + 1]
|
|
542
|
+
if (command[j] === '\\' && j + 1 < command.length) {
|
|
543
|
+
if (next === '\n') { j++; continue }
|
|
544
|
+
if (next === '$' || next === '\x60' || next === '\\' || (inDouble && next === '"')) { body += next; j++; continue }
|
|
545
|
+
}
|
|
546
|
+
body += command[j]
|
|
547
|
+
}
|
|
548
|
+
return [j, depth < 4 ? hasRootDelete(body, depth + 1) : body.includes('rm -rf /')]
|
|
549
|
+
}
|
|
516
550
|
const finishCommand = () => {
|
|
517
551
|
const denied = inRm && recursive && force && root
|
|
518
552
|
inRm = optionsEnded = recursive = force = root = false
|
|
@@ -523,7 +557,25 @@ const handlers = {
|
|
|
523
557
|
const redirectionAmpersand = char === '&' && (redirect || command[i + 1] === '>')
|
|
524
558
|
redirect = false
|
|
525
559
|
if (quote) {
|
|
560
|
+
if (quote === "$'") {
|
|
561
|
+
if (char === "'") { quote = ''; ansiNul = false }
|
|
562
|
+
else if (char === '\\' && i + 1 < command.length) {
|
|
563
|
+
const [text, last] = ansiEscape(command, i + 1)
|
|
564
|
+
i = last
|
|
565
|
+
if (!ansiNul) { const nul = text.indexOf('\0'); if (nul < 0) word += text; else { word += text.slice(0, nul); ansiNul = true } }
|
|
566
|
+
} else if (!ansiNul) word += char
|
|
567
|
+
continue
|
|
568
|
+
}
|
|
526
569
|
if (char === quote) quote = ''
|
|
570
|
+
else if (quote === '"' && char === '\x60' && command.indexOf('\x60', i + 1) > i) {
|
|
571
|
+
const [end, denied] = substitution(i + 1, true)
|
|
572
|
+
if (denied) return true
|
|
573
|
+
// The substitution is gone from the word, as bash's empty output is (a quoted empty substitution glued to -rf leaves -rf).
|
|
574
|
+
// A # right after it is still inside the word: a placeholder keeps a rescan (sh -c "...")
|
|
575
|
+
// from reading it as the start of a comment.
|
|
576
|
+
if (command[end + 1] === '#') word += '\u0001'
|
|
577
|
+
i = end
|
|
578
|
+
}
|
|
527
579
|
else if (quote === '"' && char === '\\' && i + 1 < command.length &&
|
|
528
580
|
(command[i + 1] === '"' || command[i + 1] === '\\' || command[i + 1] === '$' ||
|
|
529
581
|
command.charCodeAt(i + 1) === 96 || command[i + 1] === '\n')) {
|
|
@@ -535,6 +587,16 @@ const handlers = {
|
|
|
535
587
|
if (char === '\\' && i + 1 < command.length) {
|
|
536
588
|
const next = command[++i]
|
|
537
589
|
if (next !== '\n') { word += next; started = true }
|
|
590
|
+
} else if (char === '\x60' && command.indexOf('\x60', i + 1) > i) {
|
|
591
|
+
// Command substitution: scan the body as its own command; the substitution
|
|
592
|
+
// stays inside the enclosing word, so the rm being parsed keeps its state,
|
|
593
|
+
// and the word has started (a # right after it is not a comment).
|
|
594
|
+
const [end, denied] = substitution(i + 1, false)
|
|
595
|
+
if (denied) return true
|
|
596
|
+
i = end; started = true
|
|
597
|
+
} else if (char === '$' && (command[i + 1] === "'" || command[i + 1] === '"')) {
|
|
598
|
+
// ANSI-C ($'...': escapes decoded) and locale ($"...") quoting: the $ is not part of the word.
|
|
599
|
+
quote = command[i + 1] === "'" ? "$'" : '"'; started = true; i++
|
|
538
600
|
} else if (char === '"' || char === "'") {
|
|
539
601
|
quote = char; started = true
|
|
540
602
|
} else if (char === '#' && !started) {
|
package/catalog-manifest.json
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3851: stdout/stderr writes to a pipe are asynchronous on POSIX, so the
|
|
3
|
+
* `process.exit()` calls that follow a command (bin wrappers, CLI.run error
|
|
4
|
+
* paths, commands that exit themselves) discard whatever the consumer has not
|
|
5
|
+
* read yet, i.e. everything past the pipe buffer (64 KiB). Switching the pipe
|
|
6
|
+
* handles to blocking mode makes every write complete before it returns, so
|
|
7
|
+
* every exit path is safe. Same technique as the MCP stdio server.
|
|
8
|
+
* Files and TTYs are already synchronous and are left alone.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ensureBlockingStdio(): void;
|
|
11
|
+
//# sourceMappingURL=blocking-stdio.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #3851: stdout/stderr writes to a pipe are asynchronous on POSIX, so the
|
|
3
|
+
* `process.exit()` calls that follow a command (bin wrappers, CLI.run error
|
|
4
|
+
* paths, commands that exit themselves) discard whatever the consumer has not
|
|
5
|
+
* read yet, i.e. everything past the pipe buffer (64 KiB). Switching the pipe
|
|
6
|
+
* handles to blocking mode makes every write complete before it returns, so
|
|
7
|
+
* every exit path is safe. Same technique as the MCP stdio server.
|
|
8
|
+
* Files and TTYs are already synchronous and are left alone.
|
|
9
|
+
*/
|
|
10
|
+
export function ensureBlockingStdio() {
|
|
11
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
12
|
+
const s = stream;
|
|
13
|
+
if (s.isTTY)
|
|
14
|
+
continue;
|
|
15
|
+
try {
|
|
16
|
+
s._handle?.setBlocking?.(true);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// best effort: never fail the CLI because stdio could not be switched
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=blocking-stdio.js.map
|
|
@@ -7,6 +7,8 @@ import { confirm, input, select } from '../prompt.js';
|
|
|
7
7
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
|
+
import { stringify as stringifyYaml } from 'yaml';
|
|
11
|
+
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
10
12
|
// Format date for display
|
|
11
13
|
function formatDate(dateStr) {
|
|
12
14
|
if (!dateStr)
|
|
@@ -561,7 +563,7 @@ const exportCommand = {
|
|
|
561
563
|
// Format output
|
|
562
564
|
let content;
|
|
563
565
|
if (exportFormat === 'yaml') {
|
|
564
|
-
content =
|
|
566
|
+
content = stringifyYaml(result.data);
|
|
565
567
|
}
|
|
566
568
|
else {
|
|
567
569
|
content = JSON.stringify(result.data, null, 2);
|
|
@@ -570,7 +572,8 @@ const exportCommand = {
|
|
|
570
572
|
const absolutePath = path.isAbsolute(outputPath)
|
|
571
573
|
? outputPath
|
|
572
574
|
: path.join(ctx.cwd, outputPath);
|
|
573
|
-
|
|
575
|
+
const bytes = compress ? gzipSync(content) : Buffer.from(content, 'utf-8');
|
|
576
|
+
fs.writeFileSync(absolutePath, bytes);
|
|
574
577
|
spinner.succeed('Session exported');
|
|
575
578
|
output.writeln();
|
|
576
579
|
// session_export returns the whole record under `data`; its stats live there.
|
|
@@ -587,14 +590,14 @@ const exportCommand = {
|
|
|
587
590
|
{ property: 'Agents', value: exportStats.agentCount ?? exportStats.agents ?? 0 },
|
|
588
591
|
{ property: 'Tasks', value: exportStats.taskCount ?? exportStats.tasks ?? 0 },
|
|
589
592
|
{ property: 'Memory Entries', value: exportStats.memoryEntries ?? 0 },
|
|
590
|
-
{ property: 'File Size', value: formatSize(
|
|
593
|
+
{ property: 'File Size', value: formatSize(bytes.length) }
|
|
591
594
|
]
|
|
592
595
|
});
|
|
593
596
|
output.writeln();
|
|
594
597
|
output.printSuccess(`Session exported to ${outputPath}`);
|
|
595
598
|
return {
|
|
596
599
|
success: true,
|
|
597
|
-
data: { sessionId, outputPath, format: exportFormat, size:
|
|
600
|
+
data: { sessionId, outputPath, format: exportFormat, size: bytes.length }
|
|
598
601
|
};
|
|
599
602
|
}
|
|
600
603
|
catch (error) {
|
|
@@ -647,9 +650,11 @@ const importCommand = {
|
|
|
647
650
|
try {
|
|
648
651
|
// The YAML written by `session export --format yaml` is display-only;
|
|
649
652
|
// there is no YAML parser here, so say so instead of failing obscurely.
|
|
650
|
-
if (
|
|
653
|
+
if (/\.ya?ml(?:\.gz)?$/i.test(absolutePath)) {
|
|
651
654
|
try {
|
|
652
|
-
|
|
655
|
+
const bytes = fs.readFileSync(absolutePath);
|
|
656
|
+
const content = bytes[0] === 0x1f && bytes[1] === 0x8b ? gunzipSync(bytes) : bytes;
|
|
657
|
+
JSON.parse(content.toString('utf-8'));
|
|
653
658
|
}
|
|
654
659
|
catch {
|
|
655
660
|
spinner.fail('Failed to import session');
|
|
@@ -783,37 +788,6 @@ function formatDuration(ms) {
|
|
|
783
788
|
}
|
|
784
789
|
return `${seconds}s`;
|
|
785
790
|
}
|
|
786
|
-
function toSimpleYaml(obj, indent = 0) {
|
|
787
|
-
// Simple YAML serializer (for basic types)
|
|
788
|
-
if (obj === null)
|
|
789
|
-
return 'null';
|
|
790
|
-
if (typeof obj === 'boolean')
|
|
791
|
-
return String(obj);
|
|
792
|
-
if (typeof obj === 'number')
|
|
793
|
-
return String(obj);
|
|
794
|
-
if (typeof obj === 'string')
|
|
795
|
-
return obj.includes(':') ? `"${obj}"` : obj;
|
|
796
|
-
const spaces = ' '.repeat(indent);
|
|
797
|
-
let result = '';
|
|
798
|
-
if (Array.isArray(obj)) {
|
|
799
|
-
for (const item of obj) {
|
|
800
|
-
result += `${spaces}- ${toSimpleYaml(item, indent + 1).trim()}\n`;
|
|
801
|
-
}
|
|
802
|
-
return result;
|
|
803
|
-
}
|
|
804
|
-
if (typeof obj === 'object') {
|
|
805
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
806
|
-
if (typeof value === 'object' && value !== null) {
|
|
807
|
-
result += `${spaces}${key}:\n${toSimpleYaml(value, indent + 1)}`;
|
|
808
|
-
}
|
|
809
|
-
else {
|
|
810
|
-
result += `${spaces}${key}: ${toSimpleYaml(value, indent)}\n`;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
return result;
|
|
814
|
-
}
|
|
815
|
-
return String(obj);
|
|
816
|
-
}
|
|
817
791
|
// Main session command
|
|
818
792
|
export const sessionCommand = {
|
|
819
793
|
name: 'session',
|
package/dist/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { output } from './output.js';
|
|
|
19
19
|
import { commands, getCommandsByCategory, getCommand, getCommandAsync, getCommandNames, getLazyCommandNames, hasCommand } from './commands/index.js';
|
|
20
20
|
import { suggestCommand } from './suggest.js';
|
|
21
21
|
import { runStartupUpdateCheck } from './update/index.js';
|
|
22
|
+
import { ensureBlockingStdio } from './blocking-stdio.js';
|
|
22
23
|
// Read version from package.json at runtime
|
|
23
24
|
function getPackageVersion() {
|
|
24
25
|
try {
|
|
@@ -66,6 +67,8 @@ export class CLI {
|
|
|
66
67
|
* Run the CLI with given arguments
|
|
67
68
|
*/
|
|
68
69
|
async run(args = process.argv.slice(2)) {
|
|
70
|
+
// #3851: keep piped output intact across the process.exit() calls below
|
|
71
|
+
ensureBlockingStdio();
|
|
69
72
|
try {
|
|
70
73
|
// #1791.2 — If the user invoked a lazy command (e.g. `hive-mind task`),
|
|
71
74
|
// pre-load it BEFORE parsing so the parser can build scoped flag
|
|
@@ -436,6 +436,7 @@ module.exports = commands;
|
|
|
436
436
|
// Literal-word root guard mirrored in the mod and both shipped classic helpers.
|
|
437
437
|
const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, depth = 0) {
|
|
438
438
|
let word = '', quote = '', started = false, redirect = false
|
|
439
|
+
let ansiNul = false // a NUL inside $'...' ends that string, as in bash
|
|
439
440
|
let inRm = false, optionsEnded = false, recursive = false, force = false, root = false
|
|
440
441
|
const isRoot = (operand) => {
|
|
441
442
|
if (!operand.startsWith('/')) return false
|
|
@@ -447,22 +448,55 @@ const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, dep
|
|
|
447
448
|
}
|
|
448
449
|
return parts.length === 0 || /[*?\[]/.test(parts[0])
|
|
449
450
|
}
|
|
451
|
+
// One backslash escape inside $'...', from the character after the backslash.
|
|
452
|
+
// Returns the text it stands for and the index of its last character. An unknown escape keeps its backslash.
|
|
453
|
+
// The command arrives lowercased, so an uppercase-U escape reads as a lowercase one: eight digits starting 0000 are one code point.
|
|
454
|
+
const ansiEscape = (s, at) => {
|
|
455
|
+
const c = s[at]
|
|
456
|
+
const simple = { a: '\x07', b: '\b', e: '\x1b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v', '\\': '\\', "'": "'", '"': '"', '?': '?' }
|
|
457
|
+
if (simple[c] !== undefined) return [simple[c], at]
|
|
458
|
+
const digits = (from, max, pattern) => pattern.exec(s.slice(from, from + max))?.[0]
|
|
459
|
+
const octal = digits(at, 3, /^[0-7]+/)
|
|
460
|
+
if (octal) return [String.fromCharCode(parseInt(octal, 8) & 255), at + octal.length - 1]
|
|
461
|
+
const hex = c === 'x' ? digits(at + 1, 2, /^[0-9a-f]+/) : undefined
|
|
462
|
+
if (hex) return [String.fromCharCode(parseInt(hex, 16)), at + hex.length]
|
|
463
|
+
const wide = c === 'u' ? (digits(at + 1, 8, /^0000[0-9a-f]{4}$/) ?? digits(at + 1, 4, /^[0-9a-f]+/)) : undefined
|
|
464
|
+
if (wide) return [String.fromCodePoint(parseInt(wide, 16)), at + wide.length]
|
|
465
|
+
if (c === 'c' && at + 1 < s.length) return [String.fromCharCode(s.charCodeAt(at + 1) & 31), at + 1]
|
|
466
|
+
return ['\\' + c, at]
|
|
467
|
+
}
|
|
450
468
|
const finishWord = () => {
|
|
451
469
|
if (!started) return false
|
|
452
470
|
// Literal shell strings (e.g. sh -c 'rm -rf /') also carried the old guard.
|
|
453
471
|
// Bound rescanning to four levels; beyond that retain its conservative check.
|
|
454
|
-
if (
|
|
472
|
+
if (/[\s;&|()\x60]/.test(word)) {
|
|
455
473
|
if (depth < 4 ? hasRootDelete(word, depth + 1) : word.includes('rm -rf /')) return true
|
|
456
474
|
}
|
|
457
475
|
if (!inRm) inRm = word === 'rm' || word.endsWith('/rm')
|
|
458
476
|
else if (!optionsEnded && word === '--') optionsEnded = true
|
|
459
477
|
else if (!optionsEnded && word.startsWith('-')) {
|
|
460
|
-
|
|
461
|
-
|
|
478
|
+
// GNU getopt accepts any unambiguous long-option prefix: --r, --recur, --forc.
|
|
479
|
+
recursive = recursive || (word.length > 2 && '--recursive'.startsWith(word)) || /^-[a-z]*r[a-z]*$/.test(word)
|
|
480
|
+
force = force || (word.length > 2 && '--force'.startsWith(word)) || /^-[a-z]*f[a-z]*$/.test(word)
|
|
462
481
|
} else root = root || isRoot(word)
|
|
463
482
|
word = ''; started = false
|
|
464
483
|
return inRm && recursive && force && root
|
|
465
484
|
}
|
|
485
|
+
// A command substitution's body, from just after its opening backtick. Bash removes a
|
|
486
|
+
// backslash only before $, a backtick or a backslash (and " inside double quotes) and
|
|
487
|
+
// drops backslash-newline; any other backslash stays for the nested scan.
|
|
488
|
+
const substitution = (from, inDouble) => {
|
|
489
|
+
let body = '', j = from
|
|
490
|
+
for (; j < command.length && command[j] !== '\x60'; j++) {
|
|
491
|
+
const next = command[j + 1]
|
|
492
|
+
if (command[j] === '\\' && j + 1 < command.length) {
|
|
493
|
+
if (next === '\n') { j++; continue }
|
|
494
|
+
if (next === '$' || next === '\x60' || next === '\\' || (inDouble && next === '"')) { body += next; j++; continue }
|
|
495
|
+
}
|
|
496
|
+
body += command[j]
|
|
497
|
+
}
|
|
498
|
+
return [j, depth < 4 ? hasRootDelete(body, depth + 1) : body.includes('rm -rf /')]
|
|
499
|
+
}
|
|
466
500
|
const finishCommand = () => {
|
|
467
501
|
const denied = inRm && recursive && force && root
|
|
468
502
|
inRm = optionsEnded = recursive = force = root = false
|
|
@@ -473,7 +507,25 @@ const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, dep
|
|
|
473
507
|
const redirectionAmpersand = char === '&' && (redirect || command[i + 1] === '>')
|
|
474
508
|
redirect = false
|
|
475
509
|
if (quote) {
|
|
510
|
+
if (quote === "$'") {
|
|
511
|
+
if (char === "'") { quote = ''; ansiNul = false }
|
|
512
|
+
else if (char === '\\' && i + 1 < command.length) {
|
|
513
|
+
const [text, last] = ansiEscape(command, i + 1)
|
|
514
|
+
i = last
|
|
515
|
+
if (!ansiNul) { const nul = text.indexOf('\0'); if (nul < 0) word += text; else { word += text.slice(0, nul); ansiNul = true } }
|
|
516
|
+
} else if (!ansiNul) word += char
|
|
517
|
+
continue
|
|
518
|
+
}
|
|
476
519
|
if (char === quote) quote = ''
|
|
520
|
+
else if (quote === '"' && char === '\x60' && command.indexOf('\x60', i + 1) > i) {
|
|
521
|
+
const [end, denied] = substitution(i + 1, true)
|
|
522
|
+
if (denied) return true
|
|
523
|
+
// The substitution is gone from the word, as bash's empty output is (a quoted empty substitution glued to -rf leaves -rf).
|
|
524
|
+
// A # right after it is still inside the word: a placeholder keeps a rescan (sh -c "...")
|
|
525
|
+
// from reading it as the start of a comment.
|
|
526
|
+
if (command[end + 1] === '#') word += '\u0001'
|
|
527
|
+
i = end
|
|
528
|
+
}
|
|
477
529
|
else if (quote === '"' && char === '\\' && i + 1 < command.length &&
|
|
478
530
|
(command[i + 1] === '"' || command[i + 1] === '\\' || command[i + 1] === '$' ||
|
|
479
531
|
command.charCodeAt(i + 1) === 96 || command[i + 1] === '\n')) {
|
|
@@ -485,6 +537,16 @@ const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, dep
|
|
|
485
537
|
if (char === '\\' && i + 1 < command.length) {
|
|
486
538
|
const next = command[++i]
|
|
487
539
|
if (next !== '\n') { word += next; started = true }
|
|
540
|
+
} else if (char === '\x60' && command.indexOf('\x60', i + 1) > i) {
|
|
541
|
+
// Command substitution: scan the body as its own command; the substitution
|
|
542
|
+
// stays inside the enclosing word, so the rm being parsed keeps its state,
|
|
543
|
+
// and the word has started (a # right after it is not a comment).
|
|
544
|
+
const [end, denied] = substitution(i + 1, false)
|
|
545
|
+
if (denied) return true
|
|
546
|
+
i = end; started = true
|
|
547
|
+
} else if (char === '$' && (command[i + 1] === "'" || command[i + 1] === '"')) {
|
|
548
|
+
// ANSI-C ($'...': escapes decoded) and locale ($"...") quoting: the $ is not part of the word.
|
|
549
|
+
quote = command[i + 1] === "'" ? "$'" : '"'; started = true; i++
|
|
488
550
|
} else if (char === '"' || char === "'") {
|
|
489
551
|
quote = char; started = true
|
|
490
552
|
} else if (char === '#' && !started) {
|
|
@@ -150,6 +150,10 @@ export const agentdbPatternStore = {
|
|
|
150
150
|
const confidence = validateScore(params.confidence, 0.8);
|
|
151
151
|
const bridge = await getBridge();
|
|
152
152
|
const result = await bridge.bridgeStorePattern({ pattern, type, confidence });
|
|
153
|
+
if (result && result.success === false) {
|
|
154
|
+
// #3691: the write was refused/not persisted — never acknowledge it.
|
|
155
|
+
return { success: false, error: `Pattern store failed: ${result.error ?? 'write not persisted'}`, controller: result.controller };
|
|
156
|
+
}
|
|
153
157
|
if (result) {
|
|
154
158
|
// #3288: `controller: 'reasoningBank'` is the ONLY label that means
|
|
155
159
|
// the healthy path ran. Every other label bridgeStorePattern can
|
|
@@ -176,12 +180,20 @@ export const agentdbPatternStore = {
|
|
|
176
180
|
const { storeEntry } = await import('../memory/memory-initializer.js');
|
|
177
181
|
const patternId = `pattern-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
178
182
|
const value = JSON.stringify({ pattern, type, confidence, _fallback: 'reasoningBank-unavailable' });
|
|
179
|
-
await storeEntry({
|
|
183
|
+
const stored = await storeEntry({
|
|
180
184
|
key: patternId,
|
|
181
185
|
value,
|
|
182
186
|
namespace: 'pattern',
|
|
183
187
|
tags: [type, 'reasoning-pattern', 'fallback'],
|
|
184
188
|
});
|
|
189
|
+
// #3691: storeEntry reports failure as {success:false}, not a throw.
|
|
190
|
+
if (!stored || stored.success !== true) {
|
|
191
|
+
return {
|
|
192
|
+
success: false,
|
|
193
|
+
error: `Pattern store failed: memory_store fallback did not persist (${stored?.error ?? 'no result'})`,
|
|
194
|
+
recommendation: 'Run agentdb_health to inspect controller registration and check that .swarm/memory.db is writable.',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
185
197
|
return {
|
|
186
198
|
success: true,
|
|
187
199
|
// #3288: a caller must not have to already know to distrust a
|
|
@@ -3406,7 +3406,7 @@ export const hooksPatternStore = {
|
|
|
3406
3406
|
}
|
|
3407
3407
|
// Fallback: persist using memory-initializer store
|
|
3408
3408
|
let storeResult = { success: false };
|
|
3409
|
-
if (!reasoningResult) {
|
|
3409
|
+
if (!reasoningResult?.success) {
|
|
3410
3410
|
const storeFn = await getRealStoreFunction();
|
|
3411
3411
|
if (storeFn) {
|
|
3412
3412
|
try {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readFileSync, readdirSync, unlinkSync, statSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { gunzipSync } from 'node:zlib';
|
|
8
9
|
import { join } from 'node:path';
|
|
9
10
|
import { getProjectCwd } from './types.js';
|
|
10
11
|
import { mkdirRestricted, readFileMaybeEncrypted, writeFileRestricted, } from '../fs-secure.js';
|
|
@@ -683,7 +684,9 @@ export const sessionTools = [
|
|
|
683
684
|
if (!existsSync(inputPath))
|
|
684
685
|
return { error: `File not found: ${inputPath}` };
|
|
685
686
|
try {
|
|
686
|
-
|
|
687
|
+
const bytes = readFileSync(inputPath);
|
|
688
|
+
const content = bytes[0] === 0x1f && bytes[1] === 0x8b ? gunzipSync(bytes) : bytes;
|
|
689
|
+
parsed = JSON.parse(content.toString('utf-8'));
|
|
687
690
|
}
|
|
688
691
|
catch (e) {
|
|
689
692
|
return { error: `Invalid session JSON: ${e.message}` };
|
|
@@ -34,6 +34,7 @@ import * as fs from 'fs';
|
|
|
34
34
|
import * as crypto from 'crypto';
|
|
35
35
|
import { getMemoryRoot } from './memory-initializer.js';
|
|
36
36
|
import { encodeEmbedding } from './embedding-quantization.js';
|
|
37
|
+
import { loadBetterSqlite3 } from './shared-sqlite.js';
|
|
37
38
|
// ============================================================================
|
|
38
39
|
// Lazy-cached better-sqlite3 db handle
|
|
39
40
|
// ============================================================================
|
|
@@ -152,8 +153,8 @@ export async function getBridgeDb(customDbPath, opts) {
|
|
|
152
153
|
// the try/catch below.
|
|
153
154
|
let BetterSqlite3;
|
|
154
155
|
try {
|
|
155
|
-
|
|
156
|
-
BetterSqlite3 =
|
|
156
|
+
// #3693: share AgentDB's better-sqlite3 so the idle close cannot detach its handle.
|
|
157
|
+
BetterSqlite3 = await loadBetterSqlite3();
|
|
157
158
|
}
|
|
158
159
|
catch {
|
|
159
160
|
return null;
|
|
@@ -159,34 +159,26 @@ function step3(w) {
|
|
|
159
159
|
}
|
|
160
160
|
return w;
|
|
161
161
|
}
|
|
162
|
+
// Longest suffixes first: Porter step 4 selects the longest matching suffix
|
|
163
|
+
// ONCE and applies its condition; a failed condition must not fall through to
|
|
164
|
+
// a shorter suffix (e.g. "argument": "ment" fails m>1, so "ent" is not tried).
|
|
162
165
|
const STEP4_SUFFIXES = [
|
|
163
|
-
'
|
|
164
|
-
'
|
|
166
|
+
'ement', 'ance', 'ence', 'able', 'ible', 'ment', 'ant', 'ent', 'ism',
|
|
167
|
+
'ate', 'iti', 'ous', 'ive', 'ize', 'ion', 'al', 'er', 'ic', 'ou',
|
|
165
168
|
];
|
|
166
169
|
function step4(w) {
|
|
167
170
|
for (const suf of STEP4_SUFFIXES) {
|
|
168
|
-
if (w.endsWith(suf))
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (suf === 'ion') {
|
|
172
|
-
const last = stem[stem.length - 1];
|
|
173
|
-
if (last === 's' || last === 't')
|
|
174
|
-
return stem;
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
return stem;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// ION special-case
|
|
183
|
-
if (w.endsWith('ion')) {
|
|
184
|
-
const stem = w.slice(0, -3);
|
|
171
|
+
if (!w.endsWith(suf))
|
|
172
|
+
continue;
|
|
173
|
+
const stem = w.slice(0, w.length - suf.length);
|
|
185
174
|
if (measure(stem) > 1) {
|
|
175
|
+
if (suf !== 'ion')
|
|
176
|
+
return stem;
|
|
186
177
|
const last = stem[stem.length - 1];
|
|
187
178
|
if (last === 's' || last === 't')
|
|
188
179
|
return stem;
|
|
189
180
|
}
|
|
181
|
+
return w;
|
|
190
182
|
}
|
|
191
183
|
return w;
|
|
192
184
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* @module v3/cli/memory-bridge
|
|
18
18
|
*/
|
|
19
19
|
import { liveMemoryRowSql } from './live-memory-row.js';
|
|
20
|
+
import { resolveAgentdbBetterSqlite3 } from './shared-sqlite.js';
|
|
20
21
|
import { encodeEmbeddingQ8, MAX_LIST_EMBEDDINGS } from './embedding-q8.js';
|
|
21
22
|
import * as path from 'path';
|
|
22
23
|
import * as crypto from 'crypto';
|
|
@@ -450,7 +451,8 @@ async function getRegistry(dbPath) {
|
|
|
450
451
|
try {
|
|
451
452
|
const attestationFile = path.join(adbDir, 'dist/src/security/AttestationLog.js');
|
|
452
453
|
if (fs.existsSync(attestationFile)) {
|
|
453
|
-
|
|
454
|
+
// #3693: same better-sqlite3 copy AgentDB uses, so closing this handle cannot disturb its WAL.
|
|
455
|
+
const Database = (resolveAgentdbBetterSqlite3() ?? cjsRequire('better-sqlite3'));
|
|
454
456
|
const swarmDir = path.resolve(process.cwd(), '.swarm');
|
|
455
457
|
if (!fs.existsSync(swarmDir))
|
|
456
458
|
fs.mkdirSync(swarmDir, { recursive: true });
|
|
@@ -2111,6 +2113,17 @@ export async function bridgeStorePattern(options) {
|
|
|
2111
2113
|
});
|
|
2112
2114
|
if (!result)
|
|
2113
2115
|
return null;
|
|
2116
|
+
// #3691: bridgeStoreEntry reports data-level failures (MutationGuard
|
|
2117
|
+
// rejection, nothing written) as a truthy {success:false}. Do not turn
|
|
2118
|
+
// that into a success receipt — refuse at this boundary.
|
|
2119
|
+
if (!result.success) {
|
|
2120
|
+
return {
|
|
2121
|
+
success: false,
|
|
2122
|
+
patternId: '',
|
|
2123
|
+
controller: 'bridge-fallback',
|
|
2124
|
+
error: result.error ?? 'pattern write was not persisted',
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2114
2127
|
// Add to HNSW index for fast semantic search (bridgeStoreEntry stores SQL only)
|
|
2115
2128
|
if (result.rawEmbedding) {
|
|
2116
2129
|
try {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module v3/cli/memory-initializer
|
|
10
10
|
*/
|
|
11
|
+
import { loadBetterSqlite3 } from './shared-sqlite.js';
|
|
11
12
|
import { liveMemoryRowSql } from './live-memory-row.js';
|
|
12
13
|
import { encodeEmbeddingQ8, MAX_LIST_EMBEDDINGS } from './embedding-q8.js';
|
|
13
14
|
import * as fs from 'fs';
|
|
@@ -1408,8 +1409,8 @@ export async function recoverMemoryDatabase(dbPath, opts = {}) {
|
|
|
1408
1409
|
try {
|
|
1409
1410
|
// Module name behind a variable so TS does not statically resolve the
|
|
1410
1411
|
// optional native dep's types at build time (CI may not install them).
|
|
1411
|
-
|
|
1412
|
-
Database =
|
|
1412
|
+
// #3693: same better-sqlite3 identity as AgentDB (see shared-sqlite.ts).
|
|
1413
|
+
Database = await loadBetterSqlite3();
|
|
1413
1414
|
}
|
|
1414
1415
|
catch {
|
|
1415
1416
|
return await restoreFromBackup('no-native');
|
|
@@ -1557,8 +1558,8 @@ export async function repairVectorIndexes(dbPath, opts = {}) {
|
|
|
1557
1558
|
try {
|
|
1558
1559
|
// Module name behind a variable so TS does not statically resolve the
|
|
1559
1560
|
// optional native dep's types at build time (CI may not install them).
|
|
1560
|
-
|
|
1561
|
-
Database =
|
|
1561
|
+
// #3693: same better-sqlite3 identity as AgentDB (see shared-sqlite.ts).
|
|
1562
|
+
Database = await loadBetterSqlite3();
|
|
1562
1563
|
}
|
|
1563
1564
|
catch {
|
|
1564
1565
|
// Native module absent (e.g. WASM-only host). Statusline fix still covers
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type DbCtor = new (path: string, opts?: Record<string, unknown>) => any;
|
|
2
|
+
/** Test seam: pass `from` to resolve relative to another entry file. */
|
|
3
|
+
export declare function resolveAgentdbBetterSqlite3(from?: string): DbCtor | null;
|
|
4
|
+
/** Constructor shared with AgentDB when possible, else the CLI's own copy. */
|
|
5
|
+
export declare function loadBetterSqlite3(): Promise<DbCtor>;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=shared-sqlite.d.ts.map
|