@astralyn/pi 0.81.1-2 → 0.82.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/CHANGELOG.md +53 -0
- package/README.md +14 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +1 -1
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +1 -1
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/core/agent-session.d.ts +11 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +7 -8
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +22 -4
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +11 -3
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +4 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +9 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-config.d.ts +30 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +6 -0
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +2 -2
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +7 -0
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/model-runtime.d.ts +0 -1
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +3 -5
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/remote-catalog-provider.d.ts +1 -1
- package/dist/core/remote-catalog-provider.d.ts.map +1 -1
- package/dist/core/remote-catalog-provider.js +5 -10
- package/dist/core/remote-catalog-provider.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +2 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +49 -48
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +1 -0
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts +13 -3
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +36 -6
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +4 -3
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
- package/dist/core/tools/tool-definition-wrapper.js +5 -1
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +2 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +1 -2
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/extensions/subagent/activity.d.ts +16 -0
- package/dist/extensions/subagent/activity.d.ts.map +1 -0
- package/dist/extensions/subagent/activity.js +150 -0
- package/dist/extensions/subagent/activity.js.map +1 -0
- package/dist/extensions/subagent/agents.d.ts +7 -0
- package/dist/extensions/subagent/agents.d.ts.map +1 -0
- package/dist/extensions/subagent/agents.js +218 -0
- package/dist/extensions/subagent/agents.js.map +1 -0
- package/dist/extensions/subagent/constants.d.ts +22 -0
- package/dist/extensions/subagent/constants.d.ts.map +1 -0
- package/dist/extensions/subagent/constants.js +32 -0
- package/dist/extensions/subagent/constants.js.map +1 -0
- package/dist/extensions/subagent/index.d.ts +3 -0
- package/dist/extensions/subagent/index.d.ts.map +1 -0
- package/dist/extensions/subagent/index.js +249 -0
- package/dist/extensions/subagent/index.js.map +1 -0
- package/dist/extensions/subagent/picker.d.ts +31 -0
- package/dist/extensions/subagent/picker.d.ts.map +1 -0
- package/dist/extensions/subagent/picker.js +116 -0
- package/dist/extensions/subagent/picker.js.map +1 -0
- package/dist/extensions/subagent/render.d.ts +8 -0
- package/dist/extensions/subagent/render.d.ts.map +1 -0
- package/dist/extensions/subagent/render.js +322 -0
- package/dist/extensions/subagent/render.js.map +1 -0
- package/dist/extensions/subagent/resolve.d.ts +16 -0
- package/dist/extensions/subagent/resolve.d.ts.map +1 -0
- package/dist/extensions/subagent/resolve.js +103 -0
- package/dist/extensions/subagent/resolve.js.map +1 -0
- package/dist/extensions/subagent/runner.d.ts +30 -0
- package/dist/extensions/subagent/runner.d.ts.map +1 -0
- package/dist/extensions/subagent/runner.js +355 -0
- package/dist/extensions/subagent/runner.js.map +1 -0
- package/dist/extensions/subagent/schema.d.ts +28 -0
- package/dist/extensions/subagent/schema.d.ts.map +1 -0
- package/dist/extensions/subagent/schema.js +33 -0
- package/dist/extensions/subagent/schema.js.map +1 -0
- package/dist/extensions/subagent/sdk-runner.d.ts +14 -0
- package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -0
- package/dist/extensions/subagent/sdk-runner.js +210 -0
- package/dist/extensions/subagent/sdk-runner.js.map +1 -0
- package/dist/extensions/subagent/settings.d.ts +9 -0
- package/dist/extensions/subagent/settings.d.ts.map +1 -0
- package/dist/extensions/subagent/settings.js +131 -0
- package/dist/extensions/subagent/settings.js.map +1 -0
- package/dist/extensions/subagent/types.d.ts +99 -0
- package/dist/extensions/subagent/types.d.ts.map +1 -0
- package/dist/extensions/subagent/types.js +2 -0
- package/dist/extensions/subagent/types.js.map +1 -0
- package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +16 -46
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +2 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +25 -4
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tool-group.d.ts +16 -0
- package/dist/modes/interactive/components/tool-group.d.ts.map +1 -0
- package/dist/modes/interactive/components/tool-group.js +64 -0
- package/dist/modes/interactive/components/tool-group.js.map +1 -0
- package/dist/modes/interactive/external-editor.d.ts +12 -0
- package/dist/modes/interactive/external-editor.d.ts.map +1 -0
- package/dist/modes/interactive/external-editor.js +37 -0
- package/dist/modes/interactive/external-editor.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +3 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +61 -62
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +19 -8
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/output-decoder.d.ts +52 -0
- package/dist/utils/output-decoder.d.ts.map +1 -0
- package/dist/utils/output-decoder.js +134 -0
- package/dist/utils/output-decoder.js.map +1 -0
- package/dist/utils/shell.d.ts +1 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +21 -0
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +3 -1
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +1 -1
- package/docs/custom-provider.md +5 -2
- package/docs/environment-variables.md +88 -0
- package/docs/extensions.md +11 -3
- package/docs/index.md +4 -1
- package/docs/models.md +6 -2
- package/docs/providers.md +8 -1
- package/docs/rpc.md +23 -6
- package/docs/settings.md +1 -1
- package/docs/usage.md +0 -13
- package/examples/extensions/custom-compaction.ts +3 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/handoff.ts +9 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/summarize.ts +3 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +16 -15
- package/package.json +5 -4
- package/dist/core/tools/bash-command-summary.d.ts +0 -7
- package/dist/core/tools/bash-command-summary.d.ts.map +0 -1
- package/dist/core/tools/bash-command-summary.js +0 -261
- package/dist/core/tools/bash-command-summary.js.map +0 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { isUtf8 } from "node:buffer";
|
|
2
|
+
import { spawnSync } from "child_process";
|
|
3
|
+
/**
|
|
4
|
+
* Windows OEM code pages mapped to WHATWG TextDecoder labels. Console
|
|
5
|
+
* programs emit piped output in the OEM code page, so only code pages that
|
|
6
|
+
* TextDecoder can represent are listed; others keep UTF-8 decoding.
|
|
7
|
+
*/
|
|
8
|
+
const OEM_CODE_PAGE_ENCODINGS = {
|
|
9
|
+
"866": "ibm866",
|
|
10
|
+
"874": "windows-874",
|
|
11
|
+
"932": "shift_jis",
|
|
12
|
+
"936": "gbk",
|
|
13
|
+
"949": "euc-kr",
|
|
14
|
+
"950": "big5",
|
|
15
|
+
"1251": "windows-1251",
|
|
16
|
+
"1252": "windows-1252",
|
|
17
|
+
};
|
|
18
|
+
let cachedConsoleFallbackEncoding;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the fallback encoding for console output on this system.
|
|
21
|
+
*
|
|
22
|
+
* On Windows, cmd.exe builtins, Windows PowerShell, and classic console tools
|
|
23
|
+
* such as ipconfig emit piped output in the legacy OEM code page (GBK on
|
|
24
|
+
* Simplified Chinese systems, Shift_JIS on Japanese systems, and so on)
|
|
25
|
+
* rather than UTF-8. Returns the TextDecoder label for that code page, or
|
|
26
|
+
* null when output should stay UTF-8: non-Windows platforms, UTF-8 systems
|
|
27
|
+
* (code page 65001), and code pages TextDecoder cannot represent.
|
|
28
|
+
*/
|
|
29
|
+
export function getConsoleFallbackEncoding() {
|
|
30
|
+
if (cachedConsoleFallbackEncoding === undefined) {
|
|
31
|
+
cachedConsoleFallbackEncoding = detectConsoleFallbackEncoding();
|
|
32
|
+
}
|
|
33
|
+
return cachedConsoleFallbackEncoding;
|
|
34
|
+
}
|
|
35
|
+
function detectConsoleFallbackEncoding() {
|
|
36
|
+
if (process.platform !== "win32") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const result = spawnSync("reg", ["query", "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Nls\\CodePage", "/v", "OEMCP"], { encoding: "utf-8", timeout: 5000, windowsHide: true });
|
|
41
|
+
if (result.status !== 0 || !result.stdout) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const match = result.stdout.match(/OEMCP\s+REG_SZ\s+(\d+)/);
|
|
45
|
+
const encoding = match ? OEM_CODE_PAGE_ENCODINGS[match[1]] : undefined;
|
|
46
|
+
if (!encoding) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// Node builds with small-icu lack these decoders; verify availability.
|
|
50
|
+
new TextDecoder(encoding);
|
|
51
|
+
return encoding;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const DEFAULT_DETECTION_LIMIT_BYTES = 256 * 1024;
|
|
58
|
+
/**
|
|
59
|
+
* Streaming decoder that assumes UTF-8 and falls back to the system console
|
|
60
|
+
* encoding when the stream turns out not to be UTF-8.
|
|
61
|
+
*
|
|
62
|
+
* Decoding starts as plain streaming UTF-8 — the fast path for virtually all
|
|
63
|
+
* output. When a replacement character appears, the bytes seen so far are
|
|
64
|
+
* checked with isUtf8: if they are genuinely invalid UTF-8, everything is
|
|
65
|
+
* re-decoded with the fallback encoding and the result is reported with
|
|
66
|
+
* `rewound: true` so the caller can rebuild its derived state. If the bytes
|
|
67
|
+
* are valid UTF-8 (the source itself contained replacement characters),
|
|
68
|
+
* detection stops and UTF-8 decoding continues.
|
|
69
|
+
*
|
|
70
|
+
* Raw bytes are retained only while detection is active and only up to
|
|
71
|
+
* `detectionLimitBytes`; larger streams lock to UTF-8 and memory stays flat.
|
|
72
|
+
*/
|
|
73
|
+
export class OutputDecoder {
|
|
74
|
+
decoder = new TextDecoder();
|
|
75
|
+
fallbackEncoding;
|
|
76
|
+
detectionLimitBytes;
|
|
77
|
+
pending = [];
|
|
78
|
+
pendingBytes = 0;
|
|
79
|
+
detecting;
|
|
80
|
+
constructor(options = {}) {
|
|
81
|
+
this.fallbackEncoding =
|
|
82
|
+
options.fallbackEncoding === undefined ? getConsoleFallbackEncoding() : options.fallbackEncoding;
|
|
83
|
+
this.detectionLimitBytes = options.detectionLimitBytes ?? DEFAULT_DETECTION_LIMIT_BYTES;
|
|
84
|
+
this.detecting = this.fallbackEncoding !== null;
|
|
85
|
+
}
|
|
86
|
+
push(data) {
|
|
87
|
+
const text = this.decoder.decode(data, { stream: true });
|
|
88
|
+
if (!this.detecting) {
|
|
89
|
+
return { text, rewound: false };
|
|
90
|
+
}
|
|
91
|
+
this.pending.push(data);
|
|
92
|
+
this.pendingBytes += data.length;
|
|
93
|
+
if (text.includes("�")) {
|
|
94
|
+
return this.tryRewind(text);
|
|
95
|
+
}
|
|
96
|
+
if (this.pendingBytes > this.detectionLimitBytes) {
|
|
97
|
+
this.stopDetecting();
|
|
98
|
+
}
|
|
99
|
+
return { text, rewound: false };
|
|
100
|
+
}
|
|
101
|
+
flush() {
|
|
102
|
+
const text = this.decoder.decode();
|
|
103
|
+
if (this.detecting && text.includes("�")) {
|
|
104
|
+
return this.tryRewind(text);
|
|
105
|
+
}
|
|
106
|
+
this.stopDetecting();
|
|
107
|
+
return { text, rewound: false };
|
|
108
|
+
}
|
|
109
|
+
tryRewind(utf8Text) {
|
|
110
|
+
const raw = Buffer.concat(this.pending);
|
|
111
|
+
if (isUtf8(raw)) {
|
|
112
|
+
// The replacement characters exist in the source bytes themselves;
|
|
113
|
+
// the stream is valid UTF-8, so stop watching and keep it.
|
|
114
|
+
this.stopDetecting();
|
|
115
|
+
return { text: utf8Text, rewound: false };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
this.decoder = new TextDecoder(this.fallbackEncoding);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
this.stopDetecting();
|
|
122
|
+
return { text: utf8Text, rewound: false };
|
|
123
|
+
}
|
|
124
|
+
const text = this.decoder.decode(raw, { stream: true });
|
|
125
|
+
this.stopDetecting();
|
|
126
|
+
return { text, rewound: true };
|
|
127
|
+
}
|
|
128
|
+
stopDetecting() {
|
|
129
|
+
this.detecting = false;
|
|
130
|
+
this.pending = [];
|
|
131
|
+
this.pendingBytes = 0;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=output-decoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-decoder.js","sourceRoot":"","sources":["../../src/utils/output-decoder.ts"],"names":[],"mappings":"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","sourcesContent":["import { isUtf8 } from \"node:buffer\";\nimport { spawnSync } from \"child_process\";\n\n/**\n * Windows OEM code pages mapped to WHATWG TextDecoder labels. Console\n * programs emit piped output in the OEM code page, so only code pages that\n * TextDecoder can represent are listed; others keep UTF-8 decoding.\n */\nconst OEM_CODE_PAGE_ENCODINGS: Record<string, string> = {\n\t\"866\": \"ibm866\",\n\t\"874\": \"windows-874\",\n\t\"932\": \"shift_jis\",\n\t\"936\": \"gbk\",\n\t\"949\": \"euc-kr\",\n\t\"950\": \"big5\",\n\t\"1251\": \"windows-1251\",\n\t\"1252\": \"windows-1252\",\n};\n\nlet cachedConsoleFallbackEncoding: string | null | undefined;\n\n/**\n * Resolve the fallback encoding for console output on this system.\n *\n * On Windows, cmd.exe builtins, Windows PowerShell, and classic console tools\n * such as ipconfig emit piped output in the legacy OEM code page (GBK on\n * Simplified Chinese systems, Shift_JIS on Japanese systems, and so on)\n * rather than UTF-8. Returns the TextDecoder label for that code page, or\n * null when output should stay UTF-8: non-Windows platforms, UTF-8 systems\n * (code page 65001), and code pages TextDecoder cannot represent.\n */\nexport function getConsoleFallbackEncoding(): string | null {\n\tif (cachedConsoleFallbackEncoding === undefined) {\n\t\tcachedConsoleFallbackEncoding = detectConsoleFallbackEncoding();\n\t}\n\treturn cachedConsoleFallbackEncoding;\n}\n\nfunction detectConsoleFallbackEncoding(): string | null {\n\tif (process.platform !== \"win32\") {\n\t\treturn null;\n\t}\n\ttry {\n\t\tconst result = spawnSync(\n\t\t\t\"reg\",\n\t\t\t[\"query\", \"HKLM\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\Nls\\\\CodePage\", \"/v\", \"OEMCP\"],\n\t\t\t{ encoding: \"utf-8\", timeout: 5000, windowsHide: true },\n\t\t);\n\t\tif (result.status !== 0 || !result.stdout) {\n\t\t\treturn null;\n\t\t}\n\t\tconst match = result.stdout.match(/OEMCP\\s+REG_SZ\\s+(\\d+)/);\n\t\tconst encoding = match ? OEM_CODE_PAGE_ENCODINGS[match[1]] : undefined;\n\t\tif (!encoding) {\n\t\t\treturn null;\n\t\t}\n\t\t// Node builds with small-icu lack these decoders; verify availability.\n\t\tnew TextDecoder(encoding);\n\t\treturn encoding;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport interface DecodedChunk {\n\t/** Decoded text for this push, or the full re-decoded transcript when rewound. */\n\ttext: string;\n\t/**\n\t * True when the decoder switched to the fallback encoding. All previously\n\t * returned text is invalid; `text` replaces the entire transcript.\n\t */\n\trewound: boolean;\n}\n\nconst DEFAULT_DETECTION_LIMIT_BYTES = 256 * 1024;\n\n/**\n * Streaming decoder that assumes UTF-8 and falls back to the system console\n * encoding when the stream turns out not to be UTF-8.\n *\n * Decoding starts as plain streaming UTF-8 — the fast path for virtually all\n * output. When a replacement character appears, the bytes seen so far are\n * checked with isUtf8: if they are genuinely invalid UTF-8, everything is\n * re-decoded with the fallback encoding and the result is reported with\n * `rewound: true` so the caller can rebuild its derived state. If the bytes\n * are valid UTF-8 (the source itself contained replacement characters),\n * detection stops and UTF-8 decoding continues.\n *\n * Raw bytes are retained only while detection is active and only up to\n * `detectionLimitBytes`; larger streams lock to UTF-8 and memory stays flat.\n */\nexport class OutputDecoder {\n\tprivate decoder = new TextDecoder();\n\tprivate readonly fallbackEncoding: string | null;\n\tprivate readonly detectionLimitBytes: number;\n\tprivate pending: Buffer[] = [];\n\tprivate pendingBytes = 0;\n\tprivate detecting: boolean;\n\n\tconstructor(options: { fallbackEncoding?: string | null; detectionLimitBytes?: number } = {}) {\n\t\tthis.fallbackEncoding =\n\t\t\toptions.fallbackEncoding === undefined ? getConsoleFallbackEncoding() : options.fallbackEncoding;\n\t\tthis.detectionLimitBytes = options.detectionLimitBytes ?? DEFAULT_DETECTION_LIMIT_BYTES;\n\t\tthis.detecting = this.fallbackEncoding !== null;\n\t}\n\n\tpush(data: Buffer): DecodedChunk {\n\t\tconst text = this.decoder.decode(data, { stream: true });\n\t\tif (!this.detecting) {\n\t\t\treturn { text, rewound: false };\n\t\t}\n\t\tthis.pending.push(data);\n\t\tthis.pendingBytes += data.length;\n\t\tif (text.includes(\"�\")) {\n\t\t\treturn this.tryRewind(text);\n\t\t}\n\t\tif (this.pendingBytes > this.detectionLimitBytes) {\n\t\t\tthis.stopDetecting();\n\t\t}\n\t\treturn { text, rewound: false };\n\t}\n\n\tflush(): DecodedChunk {\n\t\tconst text = this.decoder.decode();\n\t\tif (this.detecting && text.includes(\"�\")) {\n\t\t\treturn this.tryRewind(text);\n\t\t}\n\t\tthis.stopDetecting();\n\t\treturn { text, rewound: false };\n\t}\n\n\tprivate tryRewind(utf8Text: string): DecodedChunk {\n\t\tconst raw = Buffer.concat(this.pending);\n\t\tif (isUtf8(raw)) {\n\t\t\t// The replacement characters exist in the source bytes themselves;\n\t\t\t// the stream is valid UTF-8, so stop watching and keep it.\n\t\t\tthis.stopDetecting();\n\t\t\treturn { text: utf8Text, rewound: false };\n\t\t}\n\t\ttry {\n\t\t\tthis.decoder = new TextDecoder(this.fallbackEncoding as string);\n\t\t} catch {\n\t\t\tthis.stopDetecting();\n\t\t\treturn { text: utf8Text, rewound: false };\n\t\t}\n\t\tconst text = this.decoder.decode(raw, { stream: true });\n\t\tthis.stopDetecting();\n\t\treturn { text, rewound: true };\n\t}\n\n\tprivate stopDetecting(): void {\n\t\tthis.detecting = false;\n\t\tthis.pending = [];\n\t\tthis.pendingBytes = 0;\n\t}\n}\n"]}
|
package/dist/utils/shell.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface ShellConfig {
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function getShellConfig(customShellPath?: string): ShellConfig;
|
|
14
14
|
export declare function getShellEnv(): NodeJS.ProcessEnv;
|
|
15
|
+
export declare function rewriteCmdNulRedirects(command: string): string;
|
|
15
16
|
/**
|
|
16
17
|
* Sanitize binary output for display/storage.
|
|
17
18
|
* Removes characters that crash string-width or cause display issues:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACpC;AAkDD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,WAAW,CAqDpE;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAY/C;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAQD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAyBjD","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { delimiter } from \"node:path\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getBinDir } from \"../config.ts\";\n\nexport interface ShellConfig {\n\tshell: string;\n\targs: string[];\n\tcommandTransport?: \"argv\" | \"stdin\";\n}\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction isLegacyWslBashPath(path: string): boolean {\n\tconst normalized = path.replace(/\\//g, \"\\\\\").toLowerCase();\n\treturn /^[a-z]:\\\\windows\\\\(?:system32|sysnative)\\\\bash\\.exe$/.test(normalized);\n}\n\nfunction getBashShellConfig(shell: string): ShellConfig {\n\treturn isLegacyWslBashPath(shell) ? { shell, args: [\"-s\"], commandTransport: \"stdin\" } : { shell, args: [\"-c\"] };\n}\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\"], {\n\t\t\t\tencoding: \"utf-8\",\n\t\t\t\ttimeout: 5000,\n\t\t\t\twindowsHide: true,\n\t\t\t});\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 * Resolve shell configuration based on platform and an optional explicit shell path.\n * Resolution order:\n * 1. User-specified shellPath\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(customShellPath?: string): ShellConfig {\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\treturn getBashShellConfig(customShellPath);\n\t\t}\n\t\tthrow new Error(`Custom shell path not found: ${customShellPath}`);\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\treturn getBashShellConfig(path);\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\treturn getBashShellConfig(bashOnPath);\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 settings.json\\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\treturn getBashShellConfig(\"/bin/bash\");\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\treturn getBashShellConfig(bashOnPath);\n\t}\n\n\treturn { shell: \"sh\", args: [\"-c\"] };\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 * Detached child processes must be tracked so they can be killed on parent\n * shutdown signals (SIGHUP/SIGTERM).\n */\nconst trackedDetachedChildPids = new Set<number>();\n\nexport function trackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.add(pid);\n}\n\nexport function untrackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.delete(pid);\n}\n\nexport function killTrackedDetachedChildren(): void {\n\tfor (const pid of trackedDetachedChildPids) {\n\t\tkillProcessTree(pid);\n\t}\n\ttrackedDetachedChildPids.clear();\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\twindowsHide: 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.d.ts","sourceRoot":"","sources":["../../src/utils/shell.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACpC;AAkDD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,WAAW,CAqDpE;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAY/C;AAqBD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAQD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAyBjD","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { delimiter } from \"node:path\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getBinDir } from \"../config.ts\";\n\nexport interface ShellConfig {\n\tshell: string;\n\targs: string[];\n\tcommandTransport?: \"argv\" | \"stdin\";\n}\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction isLegacyWslBashPath(path: string): boolean {\n\tconst normalized = path.replace(/\\//g, \"\\\\\").toLowerCase();\n\treturn /^[a-z]:\\\\windows\\\\(?:system32|sysnative)\\\\bash\\.exe$/.test(normalized);\n}\n\nfunction getBashShellConfig(shell: string): ShellConfig {\n\treturn isLegacyWslBashPath(shell) ? { shell, args: [\"-s\"], commandTransport: \"stdin\" } : { shell, args: [\"-c\"] };\n}\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\"], {\n\t\t\t\tencoding: \"utf-8\",\n\t\t\t\ttimeout: 5000,\n\t\t\t\twindowsHide: true,\n\t\t\t});\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 * Resolve shell configuration based on platform and an optional explicit shell path.\n * Resolution order:\n * 1. User-specified shellPath\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(customShellPath?: string): ShellConfig {\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\treturn getBashShellConfig(customShellPath);\n\t\t}\n\t\tthrow new Error(`Custom shell path not found: ${customShellPath}`);\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\treturn getBashShellConfig(path);\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\treturn getBashShellConfig(bashOnPath);\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 settings.json\\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\treturn getBashShellConfig(\"/bin/bash\");\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\treturn getBashShellConfig(bashOnPath);\n\t}\n\n\treturn { shell: \"sh\", args: [\"-c\"] };\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 * Rewrite Windows CMD-style `nul` redirects to POSIX `/dev/null`.\n *\n * Models occasionally emit CMD syntax such as `command 2>nul` even though the\n * shell resolved by `getShellConfig` is always POSIX. A POSIX shell treats\n * `nul` as an ordinary file name, so the redirect creates a literal `nul`\n * file. On Windows that name is a reserved device name: Explorer and most\n * tools cannot delete the file, and it breaks `git add .` and `git clone`\n * for the affected directory.\n *\n * Matches `>nul`, `> NUL`, `2>nul`, `&>nul`, and `>>nul` case-insensitively\n * while leaving longer tokens such as `>null` or `>nul.txt` untouched. The\n * regex does not parse shell quoting, so a quoted literal such as\n * `echo \"2>nul crashes\"` is also rewritten when `nul` is followed by\n * whitespace; that is accepted because the pattern is rare and the rewritten\n * string stays harmless.\n */\nconst CMD_NUL_REDIRECT_REGEX = /(\\d?&?>+\\s*)[Nn][Uu][Ll](?=\\s|$|[|&;)\\n])/g;\n\nexport function rewriteCmdNulRedirects(command: string): string {\n\treturn command.replace(CMD_NUL_REDIRECT_REGEX, \"$1/dev/null\");\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 * Detached child processes must be tracked so they can be killed on parent\n * shutdown signals (SIGHUP/SIGTERM).\n */\nconst trackedDetachedChildPids = new Set<number>();\n\nexport function trackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.add(pid);\n}\n\nexport function untrackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.delete(pid);\n}\n\nexport function killTrackedDetachedChildren(): void {\n\tfor (const pid of trackedDetachedChildPids) {\n\t\tkillProcessTree(pid);\n\t}\n\ttrackedDetachedChildPids.clear();\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\twindowsHide: 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"]}
|
package/dist/utils/shell.js
CHANGED
|
@@ -112,6 +112,27 @@ export function getShellEnv() {
|
|
|
112
112
|
[pathKey]: updatedPath,
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Rewrite Windows CMD-style `nul` redirects to POSIX `/dev/null`.
|
|
117
|
+
*
|
|
118
|
+
* Models occasionally emit CMD syntax such as `command 2>nul` even though the
|
|
119
|
+
* shell resolved by `getShellConfig` is always POSIX. A POSIX shell treats
|
|
120
|
+
* `nul` as an ordinary file name, so the redirect creates a literal `nul`
|
|
121
|
+
* file. On Windows that name is a reserved device name: Explorer and most
|
|
122
|
+
* tools cannot delete the file, and it breaks `git add .` and `git clone`
|
|
123
|
+
* for the affected directory.
|
|
124
|
+
*
|
|
125
|
+
* Matches `>nul`, `> NUL`, `2>nul`, `&>nul`, and `>>nul` case-insensitively
|
|
126
|
+
* while leaving longer tokens such as `>null` or `>nul.txt` untouched. The
|
|
127
|
+
* regex does not parse shell quoting, so a quoted literal such as
|
|
128
|
+
* `echo "2>nul crashes"` is also rewritten when `nul` is followed by
|
|
129
|
+
* whitespace; that is accepted because the pattern is rare and the rewritten
|
|
130
|
+
* string stays harmless.
|
|
131
|
+
*/
|
|
132
|
+
const CMD_NUL_REDIRECT_REGEX = /(\d?&?>+\s*)[Nn][Uu][Ll](?=\s|$|[|&;)\n])/g;
|
|
133
|
+
export function rewriteCmdNulRedirects(command) {
|
|
134
|
+
return command.replace(CMD_NUL_REDIRECT_REGEX, "$1/dev/null");
|
|
135
|
+
}
|
|
115
136
|
/**
|
|
116
137
|
* Sanitize binary output for display/storage.
|
|
117
138
|
* Removes characters that crash string-width or cause display issues:
|
package/dist/utils/shell.js.map
CHANGED
|
@@ -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 } from \"../config.ts\";\n\nexport interface ShellConfig {\n\tshell: string;\n\targs: string[];\n\tcommandTransport?: \"argv\" | \"stdin\";\n}\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction isLegacyWslBashPath(path: string): boolean {\n\tconst normalized = path.replace(/\\//g, \"\\\\\").toLowerCase();\n\treturn /^[a-z]:\\\\windows\\\\(?:system32|sysnative)\\\\bash\\.exe$/.test(normalized);\n}\n\nfunction getBashShellConfig(shell: string): ShellConfig {\n\treturn isLegacyWslBashPath(shell) ? { shell, args: [\"-s\"], commandTransport: \"stdin\" } : { shell, args: [\"-c\"] };\n}\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\"], {\n\t\t\t\tencoding: \"utf-8\",\n\t\t\t\ttimeout: 5000,\n\t\t\t\twindowsHide: true,\n\t\t\t});\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 * Resolve shell configuration based on platform and an optional explicit shell path.\n * Resolution order:\n * 1. User-specified shellPath\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(customShellPath?: string): ShellConfig {\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\treturn getBashShellConfig(customShellPath);\n\t\t}\n\t\tthrow new Error(`Custom shell path not found: ${customShellPath}`);\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\treturn getBashShellConfig(path);\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\treturn getBashShellConfig(bashOnPath);\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 settings.json\\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\treturn getBashShellConfig(\"/bin/bash\");\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\treturn getBashShellConfig(bashOnPath);\n\t}\n\n\treturn { shell: \"sh\", args: [\"-c\"] };\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 * Detached child processes must be tracked so they can be killed on parent\n * shutdown signals (SIGHUP/SIGTERM).\n */\nconst trackedDetachedChildPids = new Set<number>();\n\nexport function trackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.add(pid);\n}\n\nexport function untrackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.delete(pid);\n}\n\nexport function killTrackedDetachedChildren(): void {\n\tfor (const pid of trackedDetachedChildPids) {\n\t\tkillProcessTree(pid);\n\t}\n\ttrackedDetachedChildPids.clear();\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\twindowsHide: 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 } from \"../config.ts\";\n\nexport interface ShellConfig {\n\tshell: string;\n\targs: string[];\n\tcommandTransport?: \"argv\" | \"stdin\";\n}\n\n/**\n * Find bash executable on PATH (cross-platform)\n */\nfunction isLegacyWslBashPath(path: string): boolean {\n\tconst normalized = path.replace(/\\//g, \"\\\\\").toLowerCase();\n\treturn /^[a-z]:\\\\windows\\\\(?:system32|sysnative)\\\\bash\\.exe$/.test(normalized);\n}\n\nfunction getBashShellConfig(shell: string): ShellConfig {\n\treturn isLegacyWslBashPath(shell) ? { shell, args: [\"-s\"], commandTransport: \"stdin\" } : { shell, args: [\"-c\"] };\n}\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\"], {\n\t\t\t\tencoding: \"utf-8\",\n\t\t\t\ttimeout: 5000,\n\t\t\t\twindowsHide: true,\n\t\t\t});\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 * Resolve shell configuration based on platform and an optional explicit shell path.\n * Resolution order:\n * 1. User-specified shellPath\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(customShellPath?: string): ShellConfig {\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\treturn getBashShellConfig(customShellPath);\n\t\t}\n\t\tthrow new Error(`Custom shell path not found: ${customShellPath}`);\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\treturn getBashShellConfig(path);\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\treturn getBashShellConfig(bashOnPath);\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 settings.json\\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\treturn getBashShellConfig(\"/bin/bash\");\n\t}\n\n\tconst bashOnPath = findBashOnPath();\n\tif (bashOnPath) {\n\t\treturn getBashShellConfig(bashOnPath);\n\t}\n\n\treturn { shell: \"sh\", args: [\"-c\"] };\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 * Rewrite Windows CMD-style `nul` redirects to POSIX `/dev/null`.\n *\n * Models occasionally emit CMD syntax such as `command 2>nul` even though the\n * shell resolved by `getShellConfig` is always POSIX. A POSIX shell treats\n * `nul` as an ordinary file name, so the redirect creates a literal `nul`\n * file. On Windows that name is a reserved device name: Explorer and most\n * tools cannot delete the file, and it breaks `git add .` and `git clone`\n * for the affected directory.\n *\n * Matches `>nul`, `> NUL`, `2>nul`, `&>nul`, and `>>nul` case-insensitively\n * while leaving longer tokens such as `>null` or `>nul.txt` untouched. The\n * regex does not parse shell quoting, so a quoted literal such as\n * `echo \"2>nul crashes\"` is also rewritten when `nul` is followed by\n * whitespace; that is accepted because the pattern is rare and the rewritten\n * string stays harmless.\n */\nconst CMD_NUL_REDIRECT_REGEX = /(\\d?&?>+\\s*)[Nn][Uu][Ll](?=\\s|$|[|&;)\\n])/g;\n\nexport function rewriteCmdNulRedirects(command: string): string {\n\treturn command.replace(CMD_NUL_REDIRECT_REGEX, \"$1/dev/null\");\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 * Detached child processes must be tracked so they can be killed on parent\n * shutdown signals (SIGHUP/SIGTERM).\n */\nconst trackedDetachedChildPids = new Set<number>();\n\nexport function trackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.add(pid);\n}\n\nexport function untrackDetachedChildPid(pid: number): void {\n\ttrackedDetachedChildPids.delete(pid);\n}\n\nexport function killTrackedDetachedChildren(): void {\n\tfor (const pid of trackedDetachedChildPids) {\n\t\tkillProcessTree(pid);\n\t}\n\ttrackedDetachedChildPids.clear();\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\twindowsHide: 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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-check.d.ts","sourceRoot":"","sources":["../../src/utils/version-check.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOpG;AAED,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAM/F;AAED,wBAAsB,kBAAkB,CACvC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAiBtC;AAED,wBAAsB,kBAAkB,CACvC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE7B;AAED,wBAAsB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"version-check.d.ts","sourceRoot":"","sources":["../../src/utils/version-check.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOpG;AAED,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAM/F;AAED,wBAAsB,kBAAkB,CACvC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAiBtC;AAED,wBAAsB,kBAAkB,CACvC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE7B;AAED,wBAAsB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAYvG","sourcesContent":["import { compare, valid } from \"semver\";\nimport { PACKAGE_NAME } from \"../config.ts\";\nimport { getPiUserAgent } from \"./pi-user-agent.ts\";\n\nconst LATEST_VERSION_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;\nconst DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;\n\nexport interface LatestPiRelease {\n\tversion: string;\n\tpackageName?: string;\n\tnote?: string;\n}\n\nexport function comparePackageVersions(leftVersion: string, rightVersion: string): number | undefined {\n\tconst left = valid(leftVersion.trim());\n\tconst right = valid(rightVersion.trim());\n\tif (!left || !right) {\n\t\treturn undefined;\n\t}\n\treturn compare(left, right);\n}\n\nexport function isNewerPackageVersion(candidateVersion: string, currentVersion: string): boolean {\n\tconst comparison = comparePackageVersions(candidateVersion, currentVersion);\n\tif (comparison !== undefined) {\n\t\treturn comparison > 0;\n\t}\n\treturn candidateVersion.trim() !== currentVersion.trim();\n}\n\nexport async function getLatestPiRelease(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<LatestPiRelease | undefined> {\n\tif (process.env.PI_OFFLINE) return undefined;\n\n\tconst response = await fetch(LATEST_VERSION_URL, {\n\t\theaders: {\n\t\t\t\"User-Agent\": getPiUserAgent(currentVersion),\n\t\t\taccept: \"application/json\",\n\t\t},\n\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_VERSION_CHECK_TIMEOUT_MS),\n\t});\n\tif (!response.ok) return undefined;\n\n\tconst data = (await response.json()) as { version?: unknown };\n\tif (typeof data.version !== \"string\" || !data.version.trim()) {\n\t\treturn undefined;\n\t}\n\treturn { version: data.version.trim(), packageName: PACKAGE_NAME };\n}\n\nexport async function getLatestPiVersion(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<string | undefined> {\n\treturn (await getLatestPiRelease(currentVersion, options))?.version;\n}\n\nexport async function checkForNewPiVersion(currentVersion: string): Promise<LatestPiRelease | undefined> {\n\tif (process.env.PI_SKIP_VERSION_CHECK) return undefined;\n\n\ttry {\n\t\tconst latestRelease = await getLatestPiRelease(currentVersion);\n\t\tif (latestRelease && isNewerPackageVersion(latestRelease.version, currentVersion)) {\n\t\t\treturn latestRelease;\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n"]}
|
|
@@ -19,7 +19,7 @@ export function isNewerPackageVersion(candidateVersion, currentVersion) {
|
|
|
19
19
|
return candidateVersion.trim() !== currentVersion.trim();
|
|
20
20
|
}
|
|
21
21
|
export async function getLatestPiRelease(currentVersion, options = {}) {
|
|
22
|
-
if (process.env.
|
|
22
|
+
if (process.env.PI_OFFLINE)
|
|
23
23
|
return undefined;
|
|
24
24
|
const response = await fetch(LATEST_VERSION_URL, {
|
|
25
25
|
headers: {
|
|
@@ -40,6 +40,8 @@ export async function getLatestPiVersion(currentVersion, options = {}) {
|
|
|
40
40
|
return (await getLatestPiRelease(currentVersion, options))?.version;
|
|
41
41
|
}
|
|
42
42
|
export async function checkForNewPiVersion(currentVersion) {
|
|
43
|
+
if (process.env.PI_SKIP_VERSION_CHECK)
|
|
44
|
+
return undefined;
|
|
43
45
|
try {
|
|
44
46
|
const latestRelease = await getLatestPiRelease(currentVersion);
|
|
45
47
|
if (latestRelease && isNewerPackageVersion(latestRelease.version, currentVersion)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-check.js","sourceRoot":"","sources":["../../src/utils/version-check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,kBAAkB,GAAG,8BAA8B,kBAAkB,CAAC,YAAY,CAAC,SAAS,CAAC;AACnG,MAAM,gCAAgC,GAAG,KAAK,CAAC;AAQ/C,MAAM,UAAU,sBAAsB,CAAC,WAAmB,EAAE,YAAoB,EAAsB;IACrG,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;IACzC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAAA,CAC5B;AAED,MAAM,UAAU,qBAAqB,CAAC,gBAAwB,EAAE,cAAsB,EAAW;IAChG,MAAM,UAAU,GAAG,sBAAsB,CAAC,gBAAgB,EAAE,cAAc,CAAC,CAAC;IAC5E,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,UAAU,GAAG,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,gBAAgB,CAAC,IAAI,EAAE,KAAK,cAAc,CAAC,IAAI,EAAE,CAAC;AAAA,CACzD;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,cAAsB,EACtB,OAAO,GAA2B,EAAE,EACG;IACvC,IAAI,OAAO,CAAC,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"version-check.js","sourceRoot":"","sources":["../../src/utils/version-check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,kBAAkB,GAAG,8BAA8B,kBAAkB,CAAC,YAAY,CAAC,SAAS,CAAC;AACnG,MAAM,gCAAgC,GAAG,KAAK,CAAC;AAQ/C,MAAM,UAAU,sBAAsB,CAAC,WAAmB,EAAE,YAAoB,EAAsB;IACrG,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;IACzC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAAA,CAC5B;AAED,MAAM,UAAU,qBAAqB,CAAC,gBAAwB,EAAE,cAAsB,EAAW;IAChG,MAAM,UAAU,GAAG,sBAAsB,CAAC,gBAAgB,EAAE,cAAc,CAAC,CAAC;IAC5E,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,UAAU,GAAG,CAAC,CAAC;IACvB,CAAC;IACD,OAAO,gBAAgB,CAAC,IAAI,EAAE,KAAK,cAAc,CAAC,IAAI,EAAE,CAAC;AAAA,CACzD;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,cAAsB,EACtB,OAAO,GAA2B,EAAE,EACG;IACvC,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU;QAAE,OAAO,SAAS,CAAC;IAE7C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,kBAAkB,EAAE;QAChD,OAAO,EAAE;YACR,YAAY,EAAE,cAAc,CAAC,cAAc,CAAC;YAC5C,MAAM,EAAE,kBAAkB;SAC1B;QACD,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,gCAAgC,CAAC;KAClF,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,SAAS,CAAC;IAEnC,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA0B,CAAC;IAC9D,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QAC9D,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;AAAA,CACnE;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,cAAsB,EACtB,OAAO,GAA2B,EAAE,EACN;IAC9B,OAAO,CAAC,MAAM,kBAAkB,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC;AAAA,CACpE;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,cAAsB,EAAwC;IACxG,IAAI,OAAO,CAAC,GAAG,CAAC,qBAAqB;QAAE,OAAO,SAAS,CAAC;IAExD,IAAI,CAAC;QACJ,MAAM,aAAa,GAAG,MAAM,kBAAkB,CAAC,cAAc,CAAC,CAAC;QAC/D,IAAI,aAAa,IAAI,qBAAqB,CAAC,aAAa,CAAC,OAAO,EAAE,cAAc,CAAC,EAAE,CAAC;YACnF,OAAO,aAAa,CAAC;QACtB,CAAC;QACD,OAAO,SAAS,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AAAA,CACD","sourcesContent":["import { compare, valid } from \"semver\";\nimport { PACKAGE_NAME } from \"../config.ts\";\nimport { getPiUserAgent } from \"./pi-user-agent.ts\";\n\nconst LATEST_VERSION_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;\nconst DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;\n\nexport interface LatestPiRelease {\n\tversion: string;\n\tpackageName?: string;\n\tnote?: string;\n}\n\nexport function comparePackageVersions(leftVersion: string, rightVersion: string): number | undefined {\n\tconst left = valid(leftVersion.trim());\n\tconst right = valid(rightVersion.trim());\n\tif (!left || !right) {\n\t\treturn undefined;\n\t}\n\treturn compare(left, right);\n}\n\nexport function isNewerPackageVersion(candidateVersion: string, currentVersion: string): boolean {\n\tconst comparison = comparePackageVersions(candidateVersion, currentVersion);\n\tif (comparison !== undefined) {\n\t\treturn comparison > 0;\n\t}\n\treturn candidateVersion.trim() !== currentVersion.trim();\n}\n\nexport async function getLatestPiRelease(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<LatestPiRelease | undefined> {\n\tif (process.env.PI_OFFLINE) return undefined;\n\n\tconst response = await fetch(LATEST_VERSION_URL, {\n\t\theaders: {\n\t\t\t\"User-Agent\": getPiUserAgent(currentVersion),\n\t\t\taccept: \"application/json\",\n\t\t},\n\t\tsignal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_VERSION_CHECK_TIMEOUT_MS),\n\t});\n\tif (!response.ok) return undefined;\n\n\tconst data = (await response.json()) as { version?: unknown };\n\tif (typeof data.version !== \"string\" || !data.version.trim()) {\n\t\treturn undefined;\n\t}\n\treturn { version: data.version.trim(), packageName: PACKAGE_NAME };\n}\n\nexport async function getLatestPiVersion(\n\tcurrentVersion: string,\n\toptions: { timeoutMs?: number } = {},\n): Promise<string | undefined> {\n\treturn (await getLatestPiRelease(currentVersion, options))?.version;\n}\n\nexport async function checkForNewPiVersion(currentVersion: string): Promise<LatestPiRelease | undefined> {\n\tif (process.env.PI_SKIP_VERSION_CHECK) return undefined;\n\n\ttry {\n\t\tconst latestRelease = await getLatestPiRelease(currentVersion);\n\t\tif (latestRelease && isNewerPackageVersion(latestRelease.version, currentVersion)) {\n\t\t\treturn latestRelease;\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n"]}
|
package/docs/compaction.md
CHANGED
|
@@ -20,7 +20,7 @@ Pi has two summarization mechanisms:
|
|
|
20
20
|
| Compaction | Context exceeds threshold, or `/compact` | Summarize old messages to free up context |
|
|
21
21
|
| Branch summarization | `/tree` navigation | Preserve context when switching branches |
|
|
22
22
|
|
|
23
|
-
Both use the same structured summary format and track file operations cumulatively.
|
|
23
|
+
Both use the same structured summary format and track file operations cumulatively. Compaction and branch-summary requests use fresh routing session IDs and, where supported by the provider, disable prompt-cache writes because these one-off prompts are unlikely to be reused.
|
|
24
24
|
|
|
25
25
|
## Compaction
|
|
26
26
|
|
package/docs/custom-provider.md
CHANGED
|
@@ -259,7 +259,7 @@ models: [{
|
|
|
259
259
|
```
|
|
260
260
|
|
|
261
261
|
Use `openrouter` for OpenRouter-style `reasoning: { effort }` controls. Use `together` for Together-style `reasoning: { enabled }` controls; with `supportsReasoningEffort`, it also sends `reasoning_effort`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`.
|
|
262
|
-
Use `cacheControlFormat: "anthropic"` for OpenAI-compatible providers that expose Anthropic-style prompt caching via `cache_control` on the system prompt, last tool definition, and last user
|
|
262
|
+
Use `cacheControlFormat: "anthropic"` for OpenAI-compatible providers that expose Anthropic-style prompt caching via `cache_control` on the system prompt, last tool definition, and last user, assistant, or tool-result text content.
|
|
263
263
|
|
|
264
264
|
For Anthropic-compatible providers using `api: "anthropic-messages"`, set `compat.forceAdaptiveThinking: true` on models or providers whose upstream model requires adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`). Built-in adaptive Claude models set this automatically. Set `compat.allowEmptySignature: true` only for providers that emit empty thinking signatures and expect `signature: ""` on replay.
|
|
265
265
|
|
|
@@ -737,6 +737,8 @@ interface ProviderModelConfig {
|
|
|
737
737
|
supportsDeveloperRole?: boolean;
|
|
738
738
|
supportsReasoningEffort?: boolean;
|
|
739
739
|
supportsUsageInStreaming?: boolean;
|
|
740
|
+
supportsStrictMode?: boolean;
|
|
741
|
+
supportsOpenAIGrammarTools?: boolean; // openai-completions/openai-responses; false falls back to normal function tools
|
|
740
742
|
maxTokensField?: "max_completion_tokens" | "max_tokens";
|
|
741
743
|
requiresToolResultName?: boolean;
|
|
742
744
|
requiresAssistantAfterToolResult?: boolean;
|
|
@@ -755,9 +757,10 @@ interface ProviderModelConfig {
|
|
|
755
757
|
supportsCacheControlOnTools?: boolean;
|
|
756
758
|
forceAdaptiveThinking?: boolean;
|
|
757
759
|
allowEmptySignature?: boolean;
|
|
760
|
+
supportsStrictTools?: boolean;
|
|
758
761
|
};
|
|
759
762
|
}
|
|
760
763
|
```
|
|
761
764
|
|
|
762
765
|
`openrouter` sends `reasoning: { effort }`. `deepseek` sends `thinking: { type: "enabled" | "disabled" }` and `reasoning_effort` when enabled. `together` sends `reasoning: { enabled }` and also `reasoning_effort` when `supportsReasoningEffort` is enabled. `qwen` is for DashScope-style top-level `enable_thinking`. Use `qwen-chat-template` for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking` and need `preserve_thinking`. Use `chat-template` for configurable `chat_template_kwargs`, for example DeepSeek V3.x behind vLLM with `chatTemplateKwargs: { "thinking": { "$var": "thinking.enabled" } }`.
|
|
763
|
-
`cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user
|
|
766
|
+
`cacheControlFormat: "anthropic"` applies Anthropic-style `cache_control` markers to the system prompt, last tool definition, and last user, assistant, or tool-result text content.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Environment Variables
|
|
2
|
+
|
|
3
|
+
Pi uses environment variables in three ways:
|
|
4
|
+
|
|
5
|
+
- Variables such as `PI_OFFLINE` configure the Pi process.
|
|
6
|
+
- Pi sets `PI_CODING_AGENT` so child processes can detect that they run inside Pi.
|
|
7
|
+
- Commands run by the LLM-callable bash tool receive `PI_*` variables describing the current session.
|
|
8
|
+
|
|
9
|
+
Provider API-key variables are documented separately in [Providers](providers.md#environment-variables-or-auth-file).
|
|
10
|
+
|
|
11
|
+
## Process Marker
|
|
12
|
+
|
|
13
|
+
The CLI and RPC entry points set `PI_CODING_AGENT=true`. Child processes inherit it and can use it to detect that they run inside Pi. It is not session-specific and is not set automatically when Pi is embedded through the SDK.
|
|
14
|
+
|
|
15
|
+
## Bash Tool Session Environment
|
|
16
|
+
|
|
17
|
+
Commands run by the bash tool receive the current Pi session state:
|
|
18
|
+
|
|
19
|
+
| Variable | Description |
|
|
20
|
+
|----------|-------------|
|
|
21
|
+
| `PI_SESSION_ID` | Current session ID |
|
|
22
|
+
| `PI_SESSION_FILE` | Absolute path to the current session JSONL file; unset for ephemeral sessions |
|
|
23
|
+
| `PI_PROVIDER` | Currently selected model provider |
|
|
24
|
+
| `PI_MODEL` | Currently selected model ID |
|
|
25
|
+
| `PI_REASONING_LEVEL` | Current effective reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |
|
|
26
|
+
|
|
27
|
+
The values are resolved when each command starts. Switching models or changing the reasoning level therefore affects the next bash command without restarting Pi. `PI_PROVIDER` and `PI_MODEL` identify the selected Pi model, not a different upstream model that a router may choose internally.
|
|
28
|
+
|
|
29
|
+
When asked which model or provider is running, inspect these variables instead of inferring the answer from the system prompt:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
printf '%s/%s\n' "$PI_PROVIDER" "$PI_MODEL"
|
|
33
|
+
printf 'reasoning=%s session=%s\n' "$PI_REASONING_LEVEL" "$PI_SESSION_ID"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The session file can be inspected directly when the session is persistent:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
if [ -n "$PI_SESSION_FILE" ]; then
|
|
40
|
+
tail -n 1 "$PI_SESSION_FILE"
|
|
41
|
+
fi
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
These variables are injected into the LLM-callable bash tool. They are not injected into user-entered `!` or `!!` commands.
|
|
45
|
+
|
|
46
|
+
### Custom Bash Tools
|
|
47
|
+
|
|
48
|
+
Bash tools created with `createBashTool()` expose the session environment by default when registered with Pi. Injection happens before `spawnHook`, so a hook receives the variables in `ctx.env`:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const bashTool = createBashTool(cwd, {
|
|
52
|
+
spawnHook: (ctx) => ({
|
|
53
|
+
...ctx,
|
|
54
|
+
env: { ...ctx.env, CI: "1" },
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Disable session metadata independently of the spawn hook:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const bashTool = createBashTool(cwd, {
|
|
63
|
+
exposeSessionEnvironment: false,
|
|
64
|
+
spawnHook: (ctx) => ctx,
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
When disabled, Pi removes inherited values for these variables so nested Pi processes do not expose stale parent-session metadata.
|
|
69
|
+
|
|
70
|
+
## Pi Process Configuration
|
|
71
|
+
|
|
72
|
+
These variables are read by Pi itself:
|
|
73
|
+
|
|
74
|
+
| Variable | Description |
|
|
75
|
+
|----------|-------------|
|
|
76
|
+
| `PI_CODING_AGENT_DIR` | Override the config directory; default is `~/.pi/agent` |
|
|
77
|
+
| `PI_CODING_AGENT_SESSION_DIR` | Override session storage; overridden by `--session-dir` |
|
|
78
|
+
| `PI_PACKAGE_DIR` | Override the package directory, useful for Nix/Guix store paths |
|
|
79
|
+
| `PI_OFFLINE` | Disable startup network operations, including npm version checks, package update checks, and model refresh |
|
|
80
|
+
| `PI_SKIP_VERSION_CHECK` | Skip the Pi version update check at startup. This prevents the npm registry latest-version request |
|
|
81
|
+
| `PI_TELEMETRY` | Override optional provider attribution headers: `1`/`true`/`yes` or `0`/`false`/`no`. This does not disable version checks |
|
|
82
|
+
| `PI_CACHE_RETENTION` | Set to `long` for extended provider prompt caching where supported |
|
|
83
|
+
| `PI_SHARE_VIEWER_URL` | Override the base URL used by `/share` |
|
|
84
|
+
| `PI_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor; see [Terminal setup](terminal-setup.md) |
|
|
85
|
+
| `VISUAL`, `EDITOR` | Fallback external editor for Ctrl+G when `externalEditor` is unset; defaults to Notepad on Windows and `nano` elsewhere |
|
|
86
|
+
| `HTTP_PROXY`, `HTTPS_PROXY` | Proxy outbound HTTP requests |
|
|
87
|
+
|
|
88
|
+
Provider credentials such as `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and cloud-provider configuration are listed in [Providers](providers.md#environment-variables-or-auth-file).
|
package/docs/extensions.md
CHANGED
|
@@ -982,9 +982,9 @@ ctx.sessionManager.buildContextEntries() // Active branch entries with compac
|
|
|
982
982
|
ctx.sessionManager.getLeafId() // Current leaf entry ID
|
|
983
983
|
```
|
|
984
984
|
|
|
985
|
-
### ctx.modelRegistry / ctx.model
|
|
985
|
+
### ctx.modelRegistry / ctx.model / ctx.thinkingLevel
|
|
986
986
|
|
|
987
|
-
Access to models, providers, and resolved authentication. `ctx.modelRegistry.getProvider(id)` returns the effective pi-ai provider, while `getProviderAuth(id)` resolves its current API key, headers, base URL, and provider-scoped environment without requiring a loaded model. `ctx.model` is the active model.
|
|
987
|
+
Access to models, providers, and resolved authentication. `ctx.modelRegistry.getProvider(id)` returns the effective pi-ai provider, while `getProviderAuth(id)` resolves its current API key, headers, base URL, and provider-scoped environment without requiring a loaded model. `ctx.model` is the active model, and `ctx.thinkingLevel` is its current effective thinking level.
|
|
988
988
|
|
|
989
989
|
### ctx.signal
|
|
990
990
|
|
|
@@ -2096,7 +2096,15 @@ const bashTool = createBashTool(cwd, {
|
|
|
2096
2096
|
});
|
|
2097
2097
|
```
|
|
2098
2098
|
|
|
2099
|
-
|
|
2099
|
+
`createBashTool()` exposes the current session to commands through `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, and `PI_REASONING_LEVEL`. Injection happens before `spawnHook`, so hooks receive these values in `env` and preserve them when they spread the existing environment as above. Set `exposeSessionEnvironment: false` to disable them:
|
|
2100
|
+
|
|
2101
|
+
```typescript
|
|
2102
|
+
const bashTool = createBashTool(cwd, {
|
|
2103
|
+
exposeSessionEnvironment: false,
|
|
2104
|
+
});
|
|
2105
|
+
```
|
|
2106
|
+
|
|
2107
|
+
See [Bash tool session environment](environment-variables.md#bash-tool-session-environment) for variable semantics. See [examples/extensions/ssh.ts](../examples/extensions/ssh.ts) for a complete SSH example with `--ssh` flag.
|
|
2100
2108
|
|
|
2101
2109
|
### Output Truncation
|
|
2102
2110
|
|
package/docs/index.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
# Pi
|
|
1
|
+
# Pi user and API documentation
|
|
2
2
|
|
|
3
3
|
Pi is a minimal terminal coding harness. It is designed to stay small at the core while being extended through TypeScript extensions, skills, prompt templates, themes, and pi packages.
|
|
4
4
|
|
|
5
|
+
This documentation covers product usage and public APIs. Fork architecture, bundled behavior, upstream synchronization, and publishing are documented in the repository root [Fork documentation](../../../docs/README.md).
|
|
6
|
+
|
|
5
7
|
## Quick start
|
|
6
8
|
|
|
7
9
|
Install Pi with npm:
|
|
@@ -62,6 +64,7 @@ For the full first-run flow, see [Quickstart](quickstart.md).
|
|
|
62
64
|
|
|
63
65
|
## Reference
|
|
64
66
|
|
|
67
|
+
- [Environment variables](environment-variables.md) - Pi process configuration and session metadata available to bash tools.
|
|
65
68
|
- [Session format](session-format.md) - JSONL session file format, entry types, and SessionManager API.
|
|
66
69
|
|
|
67
70
|
## Platform setup
|
package/docs/models.md
CHANGED
|
@@ -375,6 +375,8 @@ Some Anthropic models require adaptive thinking (`thinking.type: "adaptive"` plu
|
|
|
375
375
|
|
|
376
376
|
Some Anthropic-compatible providers emit thinking blocks with empty signatures and still expect them on replay. Set `allowEmptySignature` to `true` only for those providers; real Anthropic rejects empty thinking signatures.
|
|
377
377
|
|
|
378
|
+
Built-in Anthropic models enable `supportsStrictTools` in their model metadata. Custom Anthropic-compatible models must set it to `true` when their endpoint accepts strict JSON-schema tool definitions.
|
|
379
|
+
|
|
378
380
|
```json
|
|
379
381
|
{
|
|
380
382
|
"providers": {
|
|
@@ -408,6 +410,7 @@ Some Anthropic-compatible providers emit thinking blocks with empty signatures a
|
|
|
408
410
|
| `supportsCacheControlOnTools` | Whether the provider accepts Anthropic-style `cache_control` markers on tool definitions. Default: `true`. |
|
|
409
411
|
| `forceAdaptiveThinking` | Whether to send adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`) for this model. Built-in adaptive models set this automatically. Default: `false`. |
|
|
410
412
|
| `allowEmptySignature` | Whether to replay empty thinking signatures as `signature: ""` instead of converting thinking to text. Default: `false`. |
|
|
413
|
+
| `supportsStrictTools` | Whether the provider accepts strict JSON-schema tool definitions. Default: `false`; built-in Anthropic models enable it in generated metadata. |
|
|
411
414
|
|
|
412
415
|
## OpenAI Compatibility
|
|
413
416
|
|
|
@@ -445,10 +448,11 @@ For providers with partial OpenAI compatibility, use the `compat` field.
|
|
|
445
448
|
| `requiresReasoningContentOnAssistantMessages` | Include empty `reasoning_content` on all replayed assistant messages when reasoning is enabled |
|
|
446
449
|
| `thinkingFormat` | Use `reasoning_effort`, `openrouter`, `deepseek`, `together`, `zai`, `qwen`, `chat-template`, or `qwen-chat-template` thinking parameters |
|
|
447
450
|
| `chatTemplateKwargs` | `chat_template_kwargs` values for `thinkingFormat: "chat-template"`; use `{ "$var": "thinking.enabled" }` or `{ "$var": "thinking.effort" }` for pi-controlled thinking values |
|
|
448
|
-
| `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user
|
|
451
|
+
| `cacheControlFormat` | Use Anthropic-style `cache_control` markers on the system prompt, last tool definition, and last user, assistant, or tool-result text content. Currently only `anthropic` is supported. |
|
|
449
452
|
| `sendSessionAffinityHeaders` | For `openai-completions`, send session-affinity headers from the session id when caching is enabled. Default: `false`. |
|
|
450
453
|
| `sessionAffinityFormat` | For `openai-completions` and `openai-responses`, the session-affinity header format: `openai` sends `session_id`/`x-client-request-id` (completions also `x-session-affinity`), `openai-nosession` omits the underscore-containing `session_id` header, `openrouter` sends `x-session-id`. Does not affect the `prompt_cache_key` body param. Default: auto-detected. |
|
|
451
|
-
| `supportsStrictMode` |
|
|
454
|
+
| `supportsStrictMode` | Whether the provider accepts strict JSON-schema function tool definitions. Defaults depend on the API; built-in OpenAI models carry explicit capability metadata. |
|
|
455
|
+
| `supportsOpenAIGrammarTools` | Whether OpenAI-compatible APIs emit custom Lark/regex grammar tools. When `false`, grammar-constrained tools fall back to normal function tools. Default: `false`; the built-in model catalog enables it for GPT-5+ models on OpenAI, OpenAI Codex, Azure OpenAI, GitHub Copilot, opencode, and Cloudflare AI Gateway. |
|
|
452
456
|
| `deferredToolsMode` | Use provider-specific deferred tool serialization. Currently only `"kimi"` is supported for Kimi's OpenAI-compatible Chat Completions format. |
|
|
453
457
|
| `supportsLongCacheRetention` | Whether the provider accepts long cache retention when cache retention is `long`: `prompt_cache_retention: "24h"` for OpenAI prompt caching, or `cache_control.ttl: "1h"` when `cacheControlFormat` is `anthropic`. Default: `true`. |
|
|
454
458
|
| `openRouterRouting` | OpenRouter provider routing preferences. This object is sent as-is in the `provider` field of the [OpenRouter API request](https://openrouter.ai/docs/guides/routing/provider-selection). |
|
package/docs/providers.md
CHANGED
|
@@ -20,9 +20,10 @@ Use `/login` in interactive mode, then select a provider:
|
|
|
20
20
|
- Claude Pro/Max
|
|
21
21
|
- GitHub Copilot
|
|
22
22
|
- xAI (Grok/X subscription)
|
|
23
|
+
- OpenRouter (OAuth-minted API key billed from OpenRouter credits)
|
|
23
24
|
- Radius
|
|
24
25
|
|
|
25
|
-
Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json` and auto-refresh when expired.
|
|
26
|
+
Use `/logout` to clear credentials. Tokens are stored in `~/.pi/agent/auth.json` and auto-refresh when expired. OpenRouter instead mints a user-controlled API key that does not expire automatically.
|
|
26
27
|
|
|
27
28
|
### OpenAI Codex
|
|
28
29
|
|
|
@@ -43,6 +44,12 @@ Anthropic subscription auth is active for Claude Pro/Max accounts. Third-party h
|
|
|
43
44
|
- Run `/login xai`, then select **Use a subscription**
|
|
44
45
|
- `XAI_API_KEY` remains available through **Use an API key**
|
|
45
46
|
|
|
47
|
+
### OpenRouter
|
|
48
|
+
|
|
49
|
+
- Run `/login openrouter`, then select **Sign in with OpenRouter** to open the OpenRouter PKCE authorization flow
|
|
50
|
+
- The authorization creates a user-controlled OpenRouter API key billed from your OpenRouter credits
|
|
51
|
+
- `OPENROUTER_API_KEY` remains available through **Use an API key**
|
|
52
|
+
|
|
46
53
|
### Radius
|
|
47
54
|
|
|
48
55
|
Radius is a dynamic `pi-messages` gateway. `/login radius` stores OAuth tokens in `auth.json`; the gateway catalog is refreshed independently and cached in `models-store.json`. Custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` and a gateway `baseUrl`.
|