gsd-pi 2.8.0 → 2.8.2
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/dist/loader.js +5 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/config.js +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/config.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js +117 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js +97 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js +112 -3
- package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +32 -22
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +3 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js +4 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts +7 -0
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js +11 -0
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/config.ts +5 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/artifact-manager.ts +125 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/bash-executor.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/core/blob-store.ts +106 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/session-manager.ts +119 -3
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +35 -22
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
- package/node_modules/@gsd/pi-coding-agent/src/index.ts +4 -1
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
- package/node_modules/@gsd/pi-coding-agent/src/utils/shell.ts +11 -0
- package/package.json +6 -1
- package/packages/pi-coding-agent/dist/config.d.ts +2 -0
- package/packages/pi-coding-agent/dist/config.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/config.js +4 -0
- package/packages/pi-coding-agent/dist/config.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.js +117 -0
- package/packages/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/bash-executor.js +2 -2
- package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
- package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/blob-store.js +97 -0
- package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +112 -3
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
- package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/bash.js +32 -22
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +3 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.d.ts +7 -0
- package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.js +11 -0
- package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
- package/packages/pi-coding-agent/src/config.ts +5 -0
- package/packages/pi-coding-agent/src/core/artifact-manager.ts +125 -0
- package/packages/pi-coding-agent/src/core/bash-executor.ts +2 -2
- package/packages/pi-coding-agent/src/core/blob-store.ts +106 -0
- package/packages/pi-coding-agent/src/core/session-manager.ts +119 -3
- package/packages/pi-coding-agent/src/core/tools/bash.ts +35 -22
- package/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
- package/packages/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
- package/packages/pi-coding-agent/src/index.ts +4 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
- package/packages/pi-coding-agent/src/utils/shell.ts +11 -0
- package/src/resources/extensions/bg-shell/index.ts +2 -1
- package/src/resources/extensions/browser-tools/lifecycle.ts +6 -1
- package/src/resources/extensions/gsd/auto.ts +92 -49
- package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +76 -0
- package/src/resources/extensions/gsd/exit-command.ts +18 -0
- package/src/resources/extensions/gsd/files.ts +9 -40
- package/src/resources/extensions/gsd/git-service.ts +62 -17
- package/src/resources/extensions/gsd/gitignore.ts +28 -0
- package/src/resources/extensions/gsd/guided-flow.ts +49 -11
- package/src/resources/extensions/gsd/index.ts +111 -16
- package/src/resources/extensions/gsd/preferences.ts +8 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +27 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -3
- package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
- package/src/resources/extensions/gsd/roadmap-slices.ts +50 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +116 -39
- package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +2 -4
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
- package/src/resources/extensions/ttsr/index.ts +163 -0
- package/src/resources/extensions/ttsr/rule-loader.ts +121 -0
- package/src/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
- package/src/resources/extensions/ttsr/ttsr-manager.ts +344 -0
|
@@ -9,6 +9,13 @@ export declare function getShellConfig(): {
|
|
|
9
9
|
shell: string;
|
|
10
10
|
args: string[];
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* On Windows + Git Bash, rewrite Windows-style NUL redirects to /dev/null.
|
|
14
|
+
* Git Bash doesn't recognize NUL as a device name and creates a literal file
|
|
15
|
+
* that is undeletable due to NUL being a reserved Windows device name.
|
|
16
|
+
* No-op on non-Windows platforms.
|
|
17
|
+
*/
|
|
18
|
+
export declare function sanitizeCommand(command: string): string;
|
|
12
19
|
export declare function getShellEnv(): NodeJS.ProcessEnv;
|
|
13
20
|
/**
|
|
14
21
|
* Sanitize binary output for display/storage.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AA2CA;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAoElE;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAY/C;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAwBjD"}
|
|
1
|
+
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AA2CA;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAoElE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGvD;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAY/C;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAwBjD"}
|
|
@@ -102,6 +102,17 @@ export function getShellConfig() {
|
|
|
102
102
|
cachedShellConfig = { shell: "sh", args: ["-c"] };
|
|
103
103
|
return cachedShellConfig;
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* On Windows + Git Bash, rewrite Windows-style NUL redirects to /dev/null.
|
|
107
|
+
* Git Bash doesn't recognize NUL as a device name and creates a literal file
|
|
108
|
+
* that is undeletable due to NUL being a reserved Windows device name.
|
|
109
|
+
* No-op on non-Windows platforms.
|
|
110
|
+
*/
|
|
111
|
+
export function sanitizeCommand(command) {
|
|
112
|
+
if (process.platform !== "win32")
|
|
113
|
+
return command;
|
|
114
|
+
return command.replace(/(\d*>>?) *\bNUL\b(?=\s|;|\||&|\)|$)/gi, "$1 /dev/null");
|
|
115
|
+
}
|
|
105
116
|
export function getShellEnv() {
|
|
106
117
|
const binDir = getBinDir();
|
|
107
118
|
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { delimiter } from \"node:path\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getBinDir, getSettingsPath } from \"../config.js\";\nimport { SettingsManager } from \"../core/settings-manager.js\";\n\nlet cachedShellConfig: { shell: string; args: string[] } | null = null;\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction findBashOnPath(): string | null {\n\tif (process.platform === \"win32\") {\n\t\t// Windows: Use 'where' and verify file exists (where can return non-existent paths)\n\t\ttry {\n\t\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\t\tif (result.status === 0 && result.stdout) {\n\t\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\t\treturn firstMatch;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Unix: Use 'which' and trust its output (handles Termux and special filesystems)\n\ttry {\n\t\tconst result = spawnSync(\"which\", [\"bash\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Get shell configuration based on platform.\n * Resolution order:\n * 1. User-specified shellPath in settings.json\n * 2. On Windows: Git Bash in known locations, then bash on PATH\n * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh\n */\nexport function getShellConfig(): { shell: string; args: string[] } {\n\tif (cachedShellConfig) {\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst settings = SettingsManager.create();\n\tconst customShellPath = settings.getShellPath();\n\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\tcachedShellConfig = { shell: customShellPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\t\tthrow new Error(\n\t\t\t`Custom shell path not found: ${customShellPath}\\nPlease update shellPath in ${getSettingsPath()}`,\n\t\t);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\tcachedShellConfig = { shell: path, args: [\"-c\"] };\n\t\t\t\treturn cachedShellConfig;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t` 3. Set shellPath in ${getSettingsPath()}\\n\\n` +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\t// Unix: try /bin/bash, then bash on PATH, then fallback to sh\n\tif (existsSync(\"/bin/bash\")) {\n\t\tcachedShellConfig = { shell: \"/bin/bash\", args: [\"-c\"] };\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\treturn cachedShellConfig;\n\t}\n\n\tcachedShellConfig = { shell: \"sh\", args: [\"-c\"] };\n\treturn cachedShellConfig;\n}\n\nexport function getShellEnv(): NodeJS.ProcessEnv {\n\tconst binDir = getBinDir();\n\tconst pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === \"path\") ?? \"PATH\";\n\tconst currentPath = process.env[pathKey] ?? \"\";\n\tconst pathEntries = currentPath.split(delimiter).filter(Boolean);\n\tconst hasBinDir = pathEntries.includes(binDir);\n\tconst updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);\n\n\treturn {\n\t\t...process.env,\n\t\t[pathKey]: updatedPath,\n\t};\n}\n\n/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\t// Use Array.from to properly iterate over code points (not code units)\n\t// This handles surrogate pairs correctly and catches edge cases where\n\t// codePointAt() might return undefined\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates (already filtered by Array.from)\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)\n\t\t\tif (code <= 0x1f) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n\n/**\n * Kill a process and all its children (cross-platform)\n */\nexport function killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { delimiter } from \"node:path\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getBinDir, getSettingsPath } from \"../config.js\";\nimport { SettingsManager } from \"../core/settings-manager.js\";\n\nlet cachedShellConfig: { shell: string; args: string[] } | null = null;\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction findBashOnPath(): string | null {\n\tif (process.platform === \"win32\") {\n\t\t// Windows: Use 'where' and verify file exists (where can return non-existent paths)\n\t\ttry {\n\t\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\t\tif (result.status === 0 && result.stdout) {\n\t\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\t\treturn firstMatch;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore errors\n\t\t}\n\t\treturn null;\n\t}\n\n\t// Unix: Use 'which' and trust its output (handles Termux and special filesystems)\n\ttry {\n\t\tconst result = spawnSync(\"which\", [\"bash\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Get shell configuration based on platform.\n * Resolution order:\n * 1. User-specified shellPath in settings.json\n * 2. On Windows: Git Bash in known locations, then bash on PATH\n * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh\n */\nexport function getShellConfig(): { shell: string; args: string[] } {\n\tif (cachedShellConfig) {\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst settings = SettingsManager.create();\n\tconst customShellPath = settings.getShellPath();\n\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\tcachedShellConfig = { shell: customShellPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\t\tthrow new Error(\n\t\t\t`Custom shell path not found: ${customShellPath}\\nPlease update shellPath in ${getSettingsPath()}`,\n\t\t);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\tcachedShellConfig = { shell: path, args: [\"-c\"] };\n\t\t\t\treturn cachedShellConfig;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t` 3. Set shellPath in ${getSettingsPath()}\\n\\n` +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\t// Unix: try /bin/bash, then bash on PATH, then fallback to sh\n\tif (existsSync(\"/bin/bash\")) {\n\t\tcachedShellConfig = { shell: \"/bin/bash\", args: [\"-c\"] };\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\treturn cachedShellConfig;\n\t}\n\n\tcachedShellConfig = { shell: \"sh\", args: [\"-c\"] };\n\treturn cachedShellConfig;\n}\n\n/**\n * On Windows + Git Bash, rewrite Windows-style NUL redirects to /dev/null.\n * Git Bash doesn't recognize NUL as a device name and creates a literal file\n * that is undeletable due to NUL being a reserved Windows device name.\n * No-op on non-Windows platforms.\n */\nexport function sanitizeCommand(command: string): string {\n\tif (process.platform !== \"win32\") return command;\n\treturn command.replace(/(\\d*>>?) *\\bNUL\\b(?=\\s|;|\\||&|\\)|$)/gi, \"$1 /dev/null\");\n}\n\nexport function getShellEnv(): NodeJS.ProcessEnv {\n\tconst binDir = getBinDir();\n\tconst pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === \"path\") ?? \"PATH\";\n\tconst currentPath = process.env[pathKey] ?? \"\";\n\tconst pathEntries = currentPath.split(delimiter).filter(Boolean);\n\tconst hasBinDir = pathEntries.includes(binDir);\n\tconst updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);\n\n\treturn {\n\t\t...process.env,\n\t\t[pathKey]: updatedPath,\n\t};\n}\n\n/**\n * Sanitize binary output for display/storage.\n * Removes characters that crash string-width or cause display issues:\n * - Control characters (except tab, newline, carriage return)\n * - Lone surrogates\n * - Unicode Format characters (crash string-width due to a bug)\n * - Characters with undefined code points\n */\nexport function sanitizeBinaryOutput(str: string): string {\n\t// Use Array.from to properly iterate over code points (not code units)\n\t// This handles surrogate pairs correctly and catches edge cases where\n\t// codePointAt() might return undefined\n\treturn Array.from(str)\n\t\t.filter((char) => {\n\t\t\t// Filter out characters that cause string-width to crash\n\t\t\t// This includes:\n\t\t\t// - Unicode format characters\n\t\t\t// - Lone surrogates (already filtered by Array.from)\n\t\t\t// - Control chars except \\t \\n \\r\n\t\t\t// - Characters with undefined code points\n\n\t\t\tconst code = char.codePointAt(0);\n\n\t\t\t// Skip if code point is undefined (edge case with invalid strings)\n\t\t\tif (code === undefined) return false;\n\n\t\t\t// Allow tab, newline, carriage return\n\t\t\tif (code === 0x09 || code === 0x0a || code === 0x0d) return true;\n\n\t\t\t// Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)\n\t\t\tif (code <= 0x1f) return false;\n\n\t\t\t// Filter out Unicode format characters\n\t\t\tif (code >= 0xfff9 && code <= 0xfffb) return false;\n\n\t\t\treturn true;\n\t\t})\n\t\t.join(\"\");\n}\n\n/**\n * Kill a process and all its children (cross-platform)\n */\nexport function killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n"]}
|
|
@@ -235,6 +235,11 @@ export function getSessionsDir(): string {
|
|
|
235
235
|
return join(getAgentDir(), "sessions");
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/** Get path to content-addressed blob store directory */
|
|
239
|
+
export function getBlobsDir(): string {
|
|
240
|
+
return join(getAgentDir(), "blobs");
|
|
241
|
+
}
|
|
242
|
+
|
|
238
243
|
/** Get path to debug log file */
|
|
239
244
|
export function getDebugLogPath(): string {
|
|
240
245
|
return join(getAgentDir(), `${APP_NAME}-debug.log`);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped artifact storage for truncated tool outputs.
|
|
3
|
+
*
|
|
4
|
+
* Artifacts are stored in a directory alongside the session file,
|
|
5
|
+
* accessible via artifact:// URLs.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdirSync, readdirSync, writeFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Manages artifact storage for a session.
|
|
12
|
+
*
|
|
13
|
+
* Artifacts are stored with sequential IDs in the session's artifact directory.
|
|
14
|
+
* The directory is created lazily on first write.
|
|
15
|
+
*/
|
|
16
|
+
export class ArtifactManager {
|
|
17
|
+
#nextId = 0;
|
|
18
|
+
readonly #dir: string;
|
|
19
|
+
#dirCreated = false;
|
|
20
|
+
#initialized = false;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param sessionFile Path to the session .jsonl file
|
|
24
|
+
*/
|
|
25
|
+
constructor(sessionFile: string) {
|
|
26
|
+
// Artifact directory is session file path without .jsonl extension
|
|
27
|
+
this.#dir = sessionFile.slice(0, -6);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Artifact directory path.
|
|
32
|
+
* Directory may not exist until first artifact is saved.
|
|
33
|
+
*/
|
|
34
|
+
get dir(): string {
|
|
35
|
+
return this.#dir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#ensureDir(): void {
|
|
39
|
+
if (!this.#dirCreated) {
|
|
40
|
+
mkdirSync(this.#dir, { recursive: true });
|
|
41
|
+
this.#dirCreated = true;
|
|
42
|
+
}
|
|
43
|
+
if (!this.#initialized) {
|
|
44
|
+
this.#scanExistingIds();
|
|
45
|
+
this.#initialized = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Scan existing artifact files to find the next available ID.
|
|
51
|
+
* Ensures we don't overwrite artifacts when resuming a session.
|
|
52
|
+
*/
|
|
53
|
+
#scanExistingIds(): void {
|
|
54
|
+
const files = this.listFiles();
|
|
55
|
+
let maxId = -1;
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const match = file.match(/^(\d+)\..*\.log$/);
|
|
58
|
+
if (match) {
|
|
59
|
+
const id = parseInt(match[1], 10);
|
|
60
|
+
if (id > maxId) maxId = id;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
this.#nextId = maxId + 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Atomically allocate next artifact ID. */
|
|
67
|
+
allocateId(): number {
|
|
68
|
+
return this.#nextId++;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Allocate a new artifact path and ID without writing content.
|
|
73
|
+
* @param toolType Tool name for file extension (e.g., "bash", "fetch")
|
|
74
|
+
*/
|
|
75
|
+
allocatePath(toolType: string): { id: string; path: string } {
|
|
76
|
+
this.#ensureDir();
|
|
77
|
+
const id = String(this.allocateId());
|
|
78
|
+
const filename = `${id}.${toolType}.log`;
|
|
79
|
+
return { id, path: join(this.#dir, filename) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Save content as an artifact and return the artifact ID.
|
|
84
|
+
* @param content Full content to save
|
|
85
|
+
* @param toolType Tool name for file extension (e.g., "bash", "fetch")
|
|
86
|
+
* @returns Artifact ID (numeric string)
|
|
87
|
+
*/
|
|
88
|
+
save(content: string, toolType: string): string {
|
|
89
|
+
const { id, path } = this.allocatePath(toolType);
|
|
90
|
+
writeFileSync(path, content);
|
|
91
|
+
return id;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if an artifact exists.
|
|
96
|
+
* @param id Artifact ID (numeric string)
|
|
97
|
+
*/
|
|
98
|
+
exists(id: string): boolean {
|
|
99
|
+
const files = this.listFiles();
|
|
100
|
+
return files.some((f) => f.startsWith(`${id}.`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* List all artifact files in the directory.
|
|
105
|
+
* Returns empty array if directory doesn't exist.
|
|
106
|
+
*/
|
|
107
|
+
listFiles(): string[] {
|
|
108
|
+
try {
|
|
109
|
+
return readdirSync(this.#dir);
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the full path to an artifact file.
|
|
117
|
+
* Returns null if artifact doesn't exist.
|
|
118
|
+
* @param id Artifact ID (numeric string)
|
|
119
|
+
*/
|
|
120
|
+
getPath(id: string): string | null {
|
|
121
|
+
const files = this.listFiles();
|
|
122
|
+
const match = files.find((f) => f.startsWith(`${id}.`));
|
|
123
|
+
return match ? join(this.#dir, match) : null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -12,7 +12,7 @@ import { tmpdir } from "node:os";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { type ChildProcess, spawn } from "child_process";
|
|
14
14
|
import stripAnsi from "strip-ansi";
|
|
15
|
-
import { getShellConfig, getShellEnv, killProcessTree, sanitizeBinaryOutput } from "../utils/shell.js";
|
|
15
|
+
import { getShellConfig, getShellEnv, killProcessTree, sanitizeBinaryOutput, sanitizeCommand } from "../utils/shell.js";
|
|
16
16
|
import type { BashOperations } from "./tools/bash.js";
|
|
17
17
|
import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js";
|
|
18
18
|
|
|
@@ -61,7 +61,7 @@ export interface BashResult {
|
|
|
61
61
|
export function executeBash(command: string, options?: BashExecutorOptions): Promise<BashResult> {
|
|
62
62
|
return new Promise((resolve, reject) => {
|
|
63
63
|
const { shell, args } = getShellConfig();
|
|
64
|
-
const child: ChildProcess = spawn(shell, [...args, command], {
|
|
64
|
+
const child: ChildProcess = spawn(shell, [...args, sanitizeCommand(command)], {
|
|
65
65
|
detached: true,
|
|
66
66
|
env: getShellEnv(),
|
|
67
67
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-addressed blob store for externalizing large binary data (images) from session JSONL files.
|
|
3
|
+
*
|
|
4
|
+
* Files are stored at `<dir>/<sha256-hex>` with no extension. The SHA-256 hash is computed
|
|
5
|
+
* over the raw binary data (not base64). Content-addressing makes writes idempotent and
|
|
6
|
+
* provides automatic deduplication across sessions.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, accessSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
const BLOB_PREFIX = "blob:sha256:";
|
|
13
|
+
const SHA256_HEX_RE = /^[a-f0-9]{64}$/;
|
|
14
|
+
|
|
15
|
+
export interface BlobPutResult {
|
|
16
|
+
hash: string;
|
|
17
|
+
path: string;
|
|
18
|
+
get ref(): string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class BlobStore {
|
|
22
|
+
readonly dir: string;
|
|
23
|
+
constructor(dir: string) {
|
|
24
|
+
this.dir = dir;
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Write binary data to the blob store. Idempotent — same content → same hash. */
|
|
29
|
+
put(data: Buffer): BlobPutResult {
|
|
30
|
+
const hash = createHash("sha256").update(data).digest("hex");
|
|
31
|
+
const blobPath = join(this.dir, hash);
|
|
32
|
+
const result: BlobPutResult = {
|
|
33
|
+
hash,
|
|
34
|
+
path: blobPath,
|
|
35
|
+
get ref() {
|
|
36
|
+
return `${BLOB_PREFIX}${hash}`;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (!existsSync(blobPath)) {
|
|
41
|
+
writeFileSync(blobPath, data);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Read blob by hash, returns Buffer or null if not found. */
|
|
47
|
+
get(hash: string): Buffer | null {
|
|
48
|
+
if (!SHA256_HEX_RE.test(hash)) return null;
|
|
49
|
+
const blobPath = join(this.dir, hash);
|
|
50
|
+
try {
|
|
51
|
+
return readFileSync(blobPath);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Check if a blob exists. */
|
|
58
|
+
has(hash: string): boolean {
|
|
59
|
+
if (!SHA256_HEX_RE.test(hash)) return false;
|
|
60
|
+
try {
|
|
61
|
+
accessSync(join(this.dir, hash));
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Check if a data string is a blob reference. */
|
|
70
|
+
export function isBlobRef(data: string): boolean {
|
|
71
|
+
return data.startsWith(BLOB_PREFIX);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Extract the SHA-256 hash from a blob reference string. Returns null if format is invalid. */
|
|
75
|
+
export function parseBlobRef(data: string): string | null {
|
|
76
|
+
if (!data.startsWith(BLOB_PREFIX)) return null;
|
|
77
|
+
const hash = data.slice(BLOB_PREFIX.length);
|
|
78
|
+
if (!SHA256_HEX_RE.test(hash)) return null;
|
|
79
|
+
return hash;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Externalize an image's base64 data to the blob store, returning a blob reference.
|
|
84
|
+
* If the data is already a blob reference, returns it unchanged.
|
|
85
|
+
*/
|
|
86
|
+
export function externalizeImageData(blobStore: BlobStore, base64Data: string): string {
|
|
87
|
+
if (isBlobRef(base64Data)) return base64Data;
|
|
88
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
89
|
+
const { ref } = blobStore.put(buffer);
|
|
90
|
+
return ref;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a blob reference back to base64 data.
|
|
95
|
+
* If the data is not a blob reference, returns it unchanged.
|
|
96
|
+
* If the blob is missing, returns the ref unchanged.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveImageData(blobStore: BlobStore, data: string): string {
|
|
99
|
+
const hash = parseBlobRef(data);
|
|
100
|
+
if (!hash) return data;
|
|
101
|
+
|
|
102
|
+
const buffer = blobStore.get(hash);
|
|
103
|
+
if (!buffer) return data; // Missing blob — return ref as-is
|
|
104
|
+
|
|
105
|
+
return buffer.toString("base64");
|
|
106
|
+
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from "fs";
|
|
16
16
|
import { readdir, readFile, stat } from "fs/promises";
|
|
17
17
|
import { join, resolve } from "path";
|
|
18
|
-
import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
|
|
18
|
+
import { getAgentDir as getDefaultAgentDir, getBlobsDir, getSessionsDir } from "../config.js";
|
|
19
19
|
import {
|
|
20
20
|
type BashExecutionMessage,
|
|
21
21
|
type CustomMessage,
|
|
@@ -23,6 +23,11 @@ import {
|
|
|
23
23
|
createCompactionSummaryMessage,
|
|
24
24
|
createCustomMessage,
|
|
25
25
|
} from "./messages.js";
|
|
26
|
+
import { BlobStore, externalizeImageData, isBlobRef, resolveImageData } from "./blob-store.js";
|
|
27
|
+
|
|
28
|
+
const BLOB_EXTERNALIZE_THRESHOLD = 1024; // 1KB minimum to externalize
|
|
29
|
+
const MAX_PERSIST_CHARS = 500_000;
|
|
30
|
+
const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]";
|
|
26
31
|
|
|
27
32
|
export const CURRENT_SESSION_VERSION = 3;
|
|
28
33
|
|
|
@@ -426,6 +431,112 @@ function getDefaultSessionDir(cwd: string): string {
|
|
|
426
431
|
return sessionDir;
|
|
427
432
|
}
|
|
428
433
|
|
|
434
|
+
function isImageBlock(value: unknown): value is { type: "image"; data: string; mimeType?: string } {
|
|
435
|
+
return (
|
|
436
|
+
typeof value === "object" &&
|
|
437
|
+
value !== null &&
|
|
438
|
+
"type" in value &&
|
|
439
|
+
(value as { type?: string }).type === "image" &&
|
|
440
|
+
"data" in value &&
|
|
441
|
+
typeof (value as { data?: string }).data === "string"
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function truncateString(s: string, maxLength: number): string {
|
|
446
|
+
if (s.length <= maxLength) return s;
|
|
447
|
+
// Avoid splitting surrogate pairs
|
|
448
|
+
if (maxLength > 0 && s.charCodeAt(maxLength - 1) >= 0xd800 && s.charCodeAt(maxLength - 1) <= 0xdbff) {
|
|
449
|
+
return s.slice(0, maxLength - 1);
|
|
450
|
+
}
|
|
451
|
+
return s.slice(0, maxLength);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Prepare an entry for JSONL persistence: externalize large images to blob store,
|
|
456
|
+
* truncate oversized strings, strip transient fields.
|
|
457
|
+
*/
|
|
458
|
+
function prepareForPersistence(obj: unknown, blobStore: BlobStore, key?: string): unknown {
|
|
459
|
+
if (obj === null || obj === undefined) return obj;
|
|
460
|
+
|
|
461
|
+
if (typeof obj === "string") {
|
|
462
|
+
if (obj.length > MAX_PERSIST_CHARS) {
|
|
463
|
+
// Cryptographic signatures must be preserved exactly or cleared entirely
|
|
464
|
+
if (key === "thinkingSignature" || key === "thoughtSignature" || key === "textSignature") {
|
|
465
|
+
return "";
|
|
466
|
+
}
|
|
467
|
+
const limit = Math.max(0, MAX_PERSIST_CHARS - TRUNCATION_NOTICE.length);
|
|
468
|
+
return `${truncateString(obj, limit)}${TRUNCATION_NOTICE}`;
|
|
469
|
+
}
|
|
470
|
+
return obj;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (Array.isArray(obj)) {
|
|
474
|
+
let changed = false;
|
|
475
|
+
const result = obj.map((item) => {
|
|
476
|
+
// Externalize oversized images to blob store
|
|
477
|
+
if (key === "content" && isImageBlock(item)) {
|
|
478
|
+
if (!isBlobRef(item.data) && item.data.length >= BLOB_EXTERNALIZE_THRESHOLD) {
|
|
479
|
+
changed = true;
|
|
480
|
+
const blobRef = externalizeImageData(blobStore, item.data);
|
|
481
|
+
return { ...item, data: blobRef };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const newItem = prepareForPersistence(item, blobStore, key);
|
|
485
|
+
if (newItem !== item) changed = true;
|
|
486
|
+
return newItem;
|
|
487
|
+
});
|
|
488
|
+
return changed ? result : obj;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (typeof obj === "object") {
|
|
492
|
+
let changed = false;
|
|
493
|
+
const result: Record<string, unknown> = {};
|
|
494
|
+
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
495
|
+
// Strip transient properties
|
|
496
|
+
if (k === "partialJson" || k === "jsonlEvents") {
|
|
497
|
+
changed = true;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const newV = prepareForPersistence(v, blobStore, k);
|
|
501
|
+
result[k] = newV;
|
|
502
|
+
if (newV !== v) changed = true;
|
|
503
|
+
}
|
|
504
|
+
// Update lineCount if content was truncated (for FileMentionFile)
|
|
505
|
+
if (changed && "lineCount" in result && "content" in result && typeof result.content === "string") {
|
|
506
|
+
result.lineCount = (result.content as string).split("\n").length;
|
|
507
|
+
}
|
|
508
|
+
return changed ? result : obj;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return obj;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Resolve blob references in loaded entries, replacing `blob:sha256:<hash>` data
|
|
516
|
+
* fields with actual base64 content. Mutates entries in place.
|
|
517
|
+
*/
|
|
518
|
+
function resolveBlobRefsInEntries(entries: FileEntry[], blobStore: BlobStore): void {
|
|
519
|
+
for (const entry of entries) {
|
|
520
|
+
if (entry.type === "session") continue;
|
|
521
|
+
|
|
522
|
+
let contentArray: unknown[] | undefined;
|
|
523
|
+
if (entry.type === "message") {
|
|
524
|
+
const content = ((entry as SessionMessageEntry).message as { content?: unknown }).content;
|
|
525
|
+
if (Array.isArray(content)) contentArray = content;
|
|
526
|
+
} else if (entry.type === "custom_message" && Array.isArray((entry as any).content)) {
|
|
527
|
+
contentArray = (entry as any).content;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!contentArray) continue;
|
|
531
|
+
|
|
532
|
+
for (const block of contentArray) {
|
|
533
|
+
if (isImageBlock(block) && isBlobRef(block.data)) {
|
|
534
|
+
(block as { data: string }).data = resolveImageData(blobStore, block.data);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
429
540
|
/** Exported for testing */
|
|
430
541
|
export function loadEntriesFromFile(filePath: string): FileEntry[] {
|
|
431
542
|
if (!existsSync(filePath)) return [];
|
|
@@ -669,6 +780,7 @@ export class SessionManager {
|
|
|
669
780
|
private flushed: boolean = false;
|
|
670
781
|
private fileEntries: FileEntry[] = [];
|
|
671
782
|
private byId: Map<string, SessionEntry> = new Map();
|
|
783
|
+
private blobStore: BlobStore;
|
|
672
784
|
private labelsById: Map<string, string> = new Map();
|
|
673
785
|
private leafId: string | null = null;
|
|
674
786
|
|
|
@@ -676,6 +788,7 @@ export class SessionManager {
|
|
|
676
788
|
this.cwd = cwd;
|
|
677
789
|
this.sessionDir = sessionDir;
|
|
678
790
|
this.persist = persist;
|
|
791
|
+
this.blobStore = new BlobStore(getBlobsDir());
|
|
679
792
|
if (persist && sessionDir && !existsSync(sessionDir)) {
|
|
680
793
|
mkdirSync(sessionDir, { recursive: true });
|
|
681
794
|
}
|
|
@@ -712,6 +825,7 @@ export class SessionManager {
|
|
|
712
825
|
}
|
|
713
826
|
|
|
714
827
|
this._buildIndex();
|
|
828
|
+
resolveBlobRefsInEntries(this.fileEntries, this.blobStore);
|
|
715
829
|
this.flushed = true;
|
|
716
830
|
} else {
|
|
717
831
|
const explicitPath = this.sessionFile;
|
|
@@ -800,11 +914,13 @@ export class SessionManager {
|
|
|
800
914
|
|
|
801
915
|
if (!this.flushed) {
|
|
802
916
|
for (const e of this.fileEntries) {
|
|
803
|
-
|
|
917
|
+
const prepared = prepareForPersistence(e, this.blobStore) as FileEntry;
|
|
918
|
+
appendFileSync(this.sessionFile, `${JSON.stringify(prepared)}\n`);
|
|
804
919
|
}
|
|
805
920
|
this.flushed = true;
|
|
806
921
|
} else {
|
|
807
|
-
|
|
922
|
+
const prepared = prepareForPersistence(entry, this.blobStore) as FileEntry;
|
|
923
|
+
appendFileSync(this.sessionFile, `${JSON.stringify(prepared)}\n`);
|
|
808
924
|
}
|
|
809
925
|
}
|
|
810
926
|
|