@earendil-works/pi-coding-agent 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/bundle/chunks/{anthropic-messages-Y22YDKSW.js → anthropic-messages-KT2ID7DZ.js} +1 -1
- package/dist/bundle/chunks/anthropic.js +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-YJDJIIDQ.js → azure-openai-responses-6A4PNZTH.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +2 -2
- package/dist/bundle/chunks/chunk-33CRHCAK.js +2 -0
- package/dist/bundle/chunks/{chunk-F4TCZWQO.js → chunk-BK6FEHYX.js} +3 -3
- package/dist/bundle/chunks/chunk-CH4ZBH45.js +11 -0
- package/dist/bundle/chunks/{chunk-XXKX4I4Z.js → chunk-GXLAGZ3Z.js} +1 -1
- package/dist/bundle/chunks/{chunk-KIPSWGXN.js → chunk-HKYL362W.js} +1 -1
- package/dist/bundle/chunks/chunk-LPXNMA2W.js +2 -0
- package/dist/bundle/chunks/{chunk-TAOL3JFA.js → chunk-LTSARG5Q.js} +1 -1
- package/dist/bundle/chunks/{chunk-VLMAON6X.js → chunk-MXTZIDYW.js} +3 -3
- package/dist/bundle/chunks/chunk-O7YIWAPD.js +2 -0
- package/dist/bundle/chunks/{chunk-H33F2TZD.js → chunk-OIM2DMFI.js} +66 -59
- package/dist/bundle/chunks/{chunk-64GOPQSC.js → chunk-OTL6MGLJ.js} +3 -3
- package/dist/bundle/chunks/{chunk-LD2P6SFZ.js → chunk-PMRZO46Y.js} +1 -1
- package/dist/bundle/chunks/{chunk-FKGBRPJC.js → chunk-SYCGMWFH.js} +1 -1
- package/dist/bundle/chunks/{chunk-YKYP4C4A.js → chunk-UH5J7I7X.js} +3 -3
- package/dist/bundle/chunks/{chunk-WE4MXUQS.js → chunk-VLRCF7MR.js} +1 -1
- package/dist/bundle/chunks/chunk-W5MPVSHA.js +82 -0
- package/dist/bundle/chunks/chunk-Y4NYF362.js +2 -0
- package/dist/bundle/chunks/{cli-FRW732V6.js → cli-OQYLWLA3.js} +2 -2
- package/dist/bundle/chunks/cloudflare-workers-ai-system-one-U3AH4ODQ.js +2 -0
- package/dist/bundle/chunks/codemode-worker.js +2 -2
- package/dist/bundle/chunks/{easter-egg-3d-GBYHZI2E.js → easter-egg-3d-TQFQS342.js} +1 -1
- package/dist/bundle/chunks/{execute-MNHU53Y5.js → execute-7IRB5TUX.js} +11 -5
- package/dist/bundle/chunks/github-copilot.js +1 -1
- package/dist/bundle/chunks/{google-generative-ai-XMFHGQC5.js → google-generative-ai-X2VFFS4I.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-4XRRNP74.js → google-vertex-KMGHFTSP.js} +1 -1
- package/dist/bundle/chunks/image-resize-worker.js +1 -1
- package/dist/bundle/chunks/{llama-cpp-classify-XT44PI3C.js → llama-cpp-classify-FYN623RH.js} +2 -2
- package/dist/bundle/chunks/{mistral-conversations-Q22M3IBG.js → mistral-conversations-PRZLM26U.js} +2 -2
- package/dist/bundle/chunks/openai-chatgpt.js +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-4MYQSO5E.js → openai-codex-responses-ZSLJZYOE.js} +2 -2
- package/dist/bundle/chunks/openai-codex.js +1 -1
- package/dist/bundle/chunks/{openai-completions-OSRCWAAA.js → openai-completions-WWQW7S3V.js} +1 -1
- package/dist/bundle/chunks/openai-decisions-QLZKZLAK.js +5 -0
- package/dist/bundle/chunks/{openai-responses-UHF3DHNG.js → openai-responses-KV7QHJY6.js} +1 -1
- package/dist/bundle/chunks/{openrouter-images-ZVXWCSIM.js → openrouter-images-DUPG5DZO.js} +1 -1
- package/dist/bundle/chunks/{pi-messages-TM5SOOIW.js → pi-messages-YHKPQIHC.js} +1 -1
- package/dist/bundle/chunks/{runtime-FW2HXAJ6.js → runtime-ROGZ2EXI.js} +1 -1
- package/dist/bundle/chunks/typesafe-system-one-B4NNEG67.js +2 -0
- package/dist/bundle/chunks/{virtual-modules-LQIMBBGM.js → virtual-modules-4YN2GNVW.js} +1 -1
- package/dist/bundle/cli-runtime.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +13 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +7 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +12 -7
- 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 +40 -45
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js +2 -0
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/types.d.ts +11 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +2 -1
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/nested-tool-calls.d.ts +1 -0
- package/dist/core/nested-tool-calls.d.ts.map +1 -1
- package/dist/core/nested-tool-calls.js +1 -0
- package/dist/core/nested-tool-calls.js.map +1 -1
- package/dist/core/sdk.d.ts +4 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +17 -5
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +13 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +37 -13
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/renderers/bash.d.ts.map +1 -1
- package/dist/core/tools/renderers/bash.js +8 -3
- package/dist/core/tools/renderers/bash.js.map +1 -1
- package/dist/core/tools/renderers/edit.d.ts.map +1 -1
- package/dist/core/tools/renderers/edit.js +5 -4
- package/dist/core/tools/renderers/edit.js.map +1 -1
- package/dist/extensions/codemode/execute.d.ts.map +1 -1
- package/dist/extensions/codemode/execute.js +63 -7
- package/dist/extensions/codemode/execute.js.map +1 -1
- package/dist/extensions/codemode/tool.js +2 -2
- package/dist/extensions/codemode/tool.js.map +1 -1
- package/dist/extensions/llama/provider.d.ts.map +1 -1
- package/dist/extensions/llama/provider.js +34 -11
- package/dist/extensions/llama/provider.js.map +1 -1
- package/dist/extensions/mcp/cli.js +17 -23
- package/dist/extensions/mcp/cli.js.map +1 -1
- package/dist/extensions/mcp/index.d.ts.map +1 -1
- package/dist/extensions/mcp/index.js +165 -64
- package/dist/extensions/mcp/index.js.map +1 -1
- package/dist/extensions/mcp/oauth.d.ts +5 -3
- package/dist/extensions/mcp/oauth.d.ts.map +1 -1
- package/dist/extensions/mcp/oauth.js +36 -10
- package/dist/extensions/mcp/oauth.js.map +1 -1
- package/dist/extensions/mcp/ui.d.ts +3 -3
- package/dist/extensions/mcp/ui.d.ts.map +1 -1
- package/dist/extensions/mcp/ui.js +10 -2
- package/dist/extensions/mcp/ui.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +5 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +17 -14
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts +2 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +5 -2
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +2 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +5 -2
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-entry.d.ts +3 -1
- package/dist/modes/interactive/components/custom-entry.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-entry.js +8 -2
- package/dist/modes/interactive/components/custom-entry.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js +1 -0
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts +2 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +5 -2
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +5 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +10 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +50 -29
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/program-status-reporter.d.ts +34 -0
- package/dist/modes/interactive/program-status-reporter.d.ts.map +1 -0
- package/dist/modes/interactive/program-status-reporter.js +110 -0
- package/dist/modes/interactive/program-status-reporter.js.map +1 -0
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +24 -0
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/ansi.d.ts +8 -0
- package/dist/utils/ansi.d.ts.map +1 -1
- package/dist/utils/ansi.js +30 -11
- package/dist/utils/ansi.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +12 -11
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/image-resize-core.d.ts +12 -0
- package/dist/utils/image-resize-core.d.ts.map +1 -1
- package/dist/utils/image-resize-core.js +5 -0
- package/dist/utils/image-resize-core.js.map +1 -1
- package/dist/utils/image-resize-worker.js +3 -2
- package/dist/utils/image-resize-worker.js.map +1 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +9 -10
- package/dist/utils/image-resize.js.map +1 -1
- package/docs/cli.md +4 -4
- package/docs/codemode.md +23 -3
- package/docs/environment-variables.md +1 -0
- package/docs/json.md +3 -3
- package/docs/llama-cpp.md +8 -1
- package/docs/message-types.md +22 -4
- package/docs/models.md +4 -1
- package/docs/providers.md +27 -0
- package/docs/settings.md +3 -3
- package/docs/terminal-setup.md +14 -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/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +12 -12
- package/dist/bundle/chunks/chunk-2QSWSXFK.js +0 -82
- package/dist/bundle/chunks/chunk-A3JYRWB6.js +0 -2
- package/dist/bundle/chunks/chunk-C47CLQZK.js +0 -2
- package/dist/bundle/chunks/chunk-HQHD6PZI.js +0 -2
- package/dist/bundle/chunks/chunk-KJUDFE6Y.js +0 -11
- package/dist/bundle/chunks/cloudflare-workers-ai-system-one-NM3JRRWQ.js +0 -2
- package/dist/bundle/chunks/typesafe-system-one-PRR5K6CI.js +0 -2
package/dist/utils/ansi.js
CHANGED
|
@@ -25,17 +25,36 @@
|
|
|
25
25
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
26
|
* SOFTWARE.
|
|
27
27
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
// Valid string terminator sequences are BEL, ESC\, and 0x9c
|
|
29
|
+
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
|
|
30
|
+
// OSC sequences: ESC ] ... ST
|
|
31
|
+
const OSC_START = "\\u001B\\]";
|
|
32
|
+
// CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :), then final byte
|
|
33
|
+
const CSI_START = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?";
|
|
34
|
+
const CSI_FINAL = "[\\dA-PR-TZcf-nq-uy=><~]";
|
|
35
|
+
// Complete sequences. OSC is non-greedy until the first ST.
|
|
36
|
+
const ansiRegex = new RegExp(`(?:${OSC_START}[\\s\\S]*?${ST})|${CSI_START}${CSI_FINAL}`, "g");
|
|
37
|
+
// Unfinished sequence at the end of the text: OSC without its ST (a trailing ESC may start ESC\),
|
|
38
|
+
// or CSI without its final byte.
|
|
39
|
+
const unfinishedAnsiAtEndRegex = new RegExp(`(?:${OSC_START}(?:[^\\u0007\\u009C\\u001B]|\\u001B(?!\\\\))*|${CSI_START})$`);
|
|
40
|
+
// Longest unfinished sequence held back while streaming. Longer ones are processed as-is.
|
|
41
|
+
const MAX_PENDING_ANSI_LENGTH = 256;
|
|
42
|
+
/**
|
|
43
|
+
* Split streamed text into a part that is safe to pass to stripAnsi now and a trailing
|
|
44
|
+
* unfinished escape sequence that should be prepended to the next chunk.
|
|
45
|
+
*/
|
|
46
|
+
export function splitIncompleteAnsiSuffix(value) {
|
|
47
|
+
if (!value.includes("\u001B") && !value.includes("\u009B")) {
|
|
48
|
+
return { complete: value, pending: "" };
|
|
49
|
+
}
|
|
50
|
+
const windowStart = Math.max(0, value.length - MAX_PENDING_ANSI_LENGTH);
|
|
51
|
+
const match = unfinishedAnsiAtEndRegex.exec(value.slice(windowStart));
|
|
52
|
+
if (!match) {
|
|
53
|
+
return { complete: value, pending: "" };
|
|
54
|
+
}
|
|
55
|
+
const splitAt = windowStart + match.index;
|
|
56
|
+
return { complete: value.slice(0, splitAt), pending: value.slice(splitAt) };
|
|
37
57
|
}
|
|
38
|
-
const regex = ansiRegex();
|
|
39
58
|
export function stripAnsi(value) {
|
|
40
59
|
if (typeof value !== "string") {
|
|
41
60
|
throw new TypeError(`Expected a \`string\`, got \`${typeof value}\``);
|
|
@@ -47,6 +66,6 @@ export function stripAnsi(value) {
|
|
|
47
66
|
// Even though the regex is global, we don't need to reset the `.lastIndex`
|
|
48
67
|
// because unlike `.exec()` and `.test()`, `.replace()` does it automatically
|
|
49
68
|
// and doing it manually has a performance penalty.
|
|
50
|
-
return value.replace(
|
|
69
|
+
return value.replace(ansiRegex, "");
|
|
51
70
|
}
|
|
52
71
|
//# sourceMappingURL=ansi.js.map
|
package/dist/utils/ansi.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ansi.js","sourceRoot":"","sources":["../../src/utils/ansi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,
|
|
1
|
+
{"version":3,"file":"ansi.js","sourceRoot":"","sources":["../../src/utils/ansi.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,4DAA4D;AAC5D,MAAM,EAAE,GAAG,oCAAoC,CAAC;AAEhD,8BAA8B;AAC9B,MAAM,SAAS,GAAG,YAAY,CAAC;AAE/B,uGAAuG;AACvG,MAAM,SAAS,GAAG,4DAA4D,CAAC;AAC/E,MAAM,SAAS,GAAG,0BAA0B,CAAC;AAE7C,4DAA4D;AAC5D,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,MAAM,SAAS,aAAa,EAAE,KAAK,SAAS,GAAG,SAAS,EAAE,EAAE,GAAG,CAAC,CAAC;AAE9F,kGAAkG;AAClG,iCAAiC;AACjC,MAAM,wBAAwB,GAAG,IAAI,MAAM,CAC1C,MAAM,SAAS,iDAAiD,SAAS,IAAI,CAC7E,CAAC;AAEF,0FAA0F;AAC1F,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAEpC;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAa;IACtD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACzC,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,uBAAuB,CAAC,CAAC;IACxE,MAAM,KAAK,GAAG,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IACtE,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACzC,CAAC;IACD,MAAM,OAAO,GAAG,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC;IAC1C,OAAO,EAAE,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa;IACtC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,SAAS,CAAC,gCAAgC,OAAO,KAAK,IAAI,CAAC,CAAC;IACvE,CAAC;IAED,sEAAsE;IACtE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5D,OAAO,KAAK,CAAC;IACd,CAAC;IAED,2EAA2E;IAC3E,6EAA6E;IAC7E,mDAAmD;IACnD,OAAO,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;AACrC,CAAC","sourcesContent":["/*\n * Portions of this file are derived from:\n * - ansi-regex (https://github.com/chalk/ansi-regex)\n * - strip-ansi (https://github.com/chalk/strip-ansi)\n *\n * MIT License\n *\n * Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n// Valid string terminator sequences are BEL, ESC\\, and 0x9c\nconst ST = \"(?:\\\\u0007|\\\\u001B\\\\u005C|\\\\u009C)\";\n\n// OSC sequences: ESC ] ... ST\nconst OSC_START = \"\\\\u001B\\\\]\";\n\n// CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :), then final byte\nconst CSI_START = \"[\\\\u001B\\\\u009B][[\\\\]()#;?]*(?:\\\\d{1,4}(?:[;:]\\\\d{0,4})*)?\";\nconst CSI_FINAL = \"[\\\\dA-PR-TZcf-nq-uy=><~]\";\n\n// Complete sequences. OSC is non-greedy until the first ST.\nconst ansiRegex = new RegExp(`(?:${OSC_START}[\\\\s\\\\S]*?${ST})|${CSI_START}${CSI_FINAL}`, \"g\");\n\n// Unfinished sequence at the end of the text: OSC without its ST (a trailing ESC may start ESC\\),\n// or CSI without its final byte.\nconst unfinishedAnsiAtEndRegex = new RegExp(\n\t`(?:${OSC_START}(?:[^\\\\u0007\\\\u009C\\\\u001B]|\\\\u001B(?!\\\\\\\\))*|${CSI_START})$`,\n);\n\n// Longest unfinished sequence held back while streaming. Longer ones are processed as-is.\nconst MAX_PENDING_ANSI_LENGTH = 256;\n\n/**\n * Split streamed text into a part that is safe to pass to stripAnsi now and a trailing\n * unfinished escape sequence that should be prepended to the next chunk.\n */\nexport function splitIncompleteAnsiSuffix(value: string): { complete: string; pending: string } {\n\tif (!value.includes(\"\\u001B\") && !value.includes(\"\\u009B\")) {\n\t\treturn { complete: value, pending: \"\" };\n\t}\n\tconst windowStart = Math.max(0, value.length - MAX_PENDING_ANSI_LENGTH);\n\tconst match = unfinishedAnsiAtEndRegex.exec(value.slice(windowStart));\n\tif (!match) {\n\t\treturn { complete: value, pending: \"\" };\n\t}\n\tconst splitAt = windowStart + match.index;\n\treturn { complete: value.slice(0, splitAt), pending: value.slice(splitAt) };\n}\n\nexport function stripAnsi(value: string): string {\n\tif (typeof value !== \"string\") {\n\t\tthrow new TypeError(`Expected a \\`string\\`, got \\`${typeof value}\\``);\n\t}\n\n\t// Fast path: ANSI codes require ESC (7-bit) or CSI (8-bit) introducer\n\tif (!value.includes(\"\\u001B\") && !value.includes(\"\\u009B\")) {\n\t\treturn value;\n\t}\n\n\t// Even though the regex is global, we don't need to reset the `.lastIndex`\n\t// because unlike `.exec()` and `.test()`, `.replace()` does it automatically\n\t// and doing it manually has a performance penalty.\n\treturn value.replace(ansiRegex, \"\");\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"AAoDA,iDAAiD;AACjD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"clipboard.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"AAoDA,iDAAiD;AACjD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmBhE;AAED,8EAA8E;AAC9E,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAGvE;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgEjE"}
|
package/dist/utils/clipboard.js
CHANGED
|
@@ -51,20 +51,21 @@ async function copyViaWindowsClipboard(text) {
|
|
|
51
51
|
}
|
|
52
52
|
/** Read plain text from the system clipboard. */
|
|
53
53
|
export async function readClipboardText() {
|
|
54
|
+
const commands = [];
|
|
55
|
+
// Termux reports platform "android", not "linux" (#10391).
|
|
56
|
+
if (process.env.TERMUX_VERSION)
|
|
57
|
+
commands.push(["termux-clipboard-get", []]);
|
|
54
58
|
if (platform() === "linux") {
|
|
55
|
-
const commands = [];
|
|
56
|
-
if (process.env.TERMUX_VERSION)
|
|
57
|
-
commands.push(["termux-clipboard-get", []]);
|
|
58
59
|
if (process.env.WAYLAND_DISPLAY)
|
|
59
60
|
commands.push(["wl-paste", ["--no-newline", "--type", "text"]]);
|
|
60
61
|
if (process.env.DISPLAY) {
|
|
61
62
|
commands.push(["xclip", ["-selection", "clipboard", "-out"]], ["xsel", ["--clipboard", "--output"]]);
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
}
|
|
65
|
+
for (const [command, args] of commands) {
|
|
66
|
+
const bytes = await runClipboardCommand(command, args, { timeoutMs: 5000 });
|
|
67
|
+
if (bytes !== undefined)
|
|
68
|
+
return bytes.toString("utf8") || null;
|
|
68
69
|
}
|
|
69
70
|
try {
|
|
70
71
|
return (await getNativeClipboard()?.getText()) || null;
|
|
@@ -140,10 +141,10 @@ export async function copyToClipboard(text) {
|
|
|
140
141
|
return;
|
|
141
142
|
if (oversized)
|
|
142
143
|
throw new Error("Clipboard unavailable: text exceeds the OSC 52 size limit");
|
|
144
|
+
if (env.TERMUX_VERSION) {
|
|
145
|
+
throw new Error("Clipboard unavailable: install the Termux:API app and `termux-api` package");
|
|
146
|
+
}
|
|
143
147
|
if (p === "linux") {
|
|
144
|
-
if (env.TERMUX_VERSION) {
|
|
145
|
-
throw new Error("Clipboard unavailable: install the Termux:API app and `termux-api` package");
|
|
146
|
-
}
|
|
147
148
|
if (env.WAYLAND_DISPLAY) {
|
|
148
149
|
throw new Error("Clipboard unavailable: install `wl-clipboard` (`wl-copy`) or check Wayland access");
|
|
149
150
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { unlinkSync, writeFileSync } from \"node:fs\";\nimport { platform, tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { getNativeClipboard } from \"@earendil-works/pi-tui\";\nimport { runClipboardCommand } from \"./clipboard-command.ts\";\nimport { isWSL } from \"./wsl.ts\";\n\nconst MAX_OSC52_ENCODED_LENGTH = 100_000;\n\nfunction isRemoteSession(env: NodeJS.ProcessEnv): boolean {\n\treturn Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION);\n}\n\nfunction emitOsc52(text: string): boolean {\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tif (encoded.length > MAX_OSC52_ENCODED_LENGTH) {\n\t\treturn false;\n\t}\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\treturn true;\n}\n\n/**\n * WSL without WSLg has no Linux display, so the Windows clipboard is written through\n * interop. PowerShell reads the text from a file because `clip.exe` and PowerShell stdin\n * decode piped bytes with the console code page, which mangles non-ASCII UTF-8.\n */\nasync function copyViaWindowsClipboard(text: string): Promise<boolean> {\n\tconst tmpFile = join(tmpdir(), `pi-wsl-clip-${randomUUID()}.txt`);\n\ttry {\n\t\twriteFileSync(tmpFile, text, { encoding: \"utf8\", mode: 0o600 });\n\t\tconst winPath = (await runClipboardCommand(\"wslpath\", [\"-w\", tmpFile], { timeoutMs: 1000 }))\n\t\t\t?.toString(\"utf8\")\n\t\t\t.trim();\n\t\tif (!winPath) return false;\n\t\tconst script = `Set-Clipboard -Value ([System.IO.File]::ReadAllText('${winPath.replaceAll(\"'\", \"''\")}', [System.Text.Encoding]::UTF8))`;\n\t\tconst result = await runClipboardCommand(\"powershell.exe\", [\"-NoProfile\", \"-Command\", script], {\n\t\t\ttimeoutMs: 5000,\n\t\t});\n\t\treturn result !== undefined;\n\t} catch {\n\t\treturn false;\n\t} finally {\n\t\ttry {\n\t\t\tunlinkSync(tmpFile);\n\t\t} catch {\n\t\t\t// The file may not have been created.\n\t\t}\n\t}\n}\n\n/** Read plain text from the system clipboard. */\nexport async function readClipboardText(): Promise<string | null> {\n\tif (platform() === \"linux\") {\n\t\tconst commands: [string, string[]][] = [];\n\t\tif (process.env.TERMUX_VERSION) commands.push([\"termux-clipboard-get\", []]);\n\t\tif (process.env.WAYLAND_DISPLAY) commands.push([\"wl-paste\", [\"--no-newline\", \"--type\", \"text\"]]);\n\t\tif (process.env.DISPLAY) {\n\t\t\tcommands.push([\"xclip\", [\"-selection\", \"clipboard\", \"-out\"]], [\"xsel\", [\"--clipboard\", \"--output\"]]);\n\t\t}\n\t\tfor (const [command, args] of commands) {\n\t\t\tconst bytes = await runClipboardCommand(command, args, { timeoutMs: 5000 });\n\t\t\tif (bytes !== undefined) return bytes.toString(\"utf8\") || null;\n\t\t}\n\t}\n\ttry {\n\t\treturn (await getNativeClipboard()?.getText()) || null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/** Read file paths, such as Finder file copies, from the native clipboard. */\nexport async function readClipboardFilePaths(): Promise<string[] | null> {\n\tconst paths = await getNativeClipboard()?.getFilePaths?.();\n\treturn paths?.length ? paths : null;\n}\n\nexport async function copyToClipboard(text: string): Promise<void> {\n\tconst p = platform();\n\tconst env = process.env;\n\tlet copied = false;\n\t// Direct writes precede OSC 52 so the terminal cannot race the native writer.\n\t// Linux tools retain clipboard selection ownership after this call returns.\n\tif (p !== \"linux\") {\n\t\ttry {\n\t\t\tconst clipboard = getNativeClipboard();\n\t\t\tif (clipboard?.setText) {\n\t\t\t\tawait clipboard.setText(text);\n\t\t\t\tcopied = true;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Try platform commands next.\n\t\t}\n\t}\n\tif (!copied) {\n\t\tconst commands: [string, string[]][] = [];\n\t\tif (p === \"darwin\") commands.push([\"pbcopy\", []]);\n\t\telse if (p === \"win32\") commands.push([\"clip\", []]);\n\t\telse {\n\t\t\tif (env.TERMUX_VERSION) commands.push([\"termux-clipboard-set\", []]);\n\t\t\tif (env.WAYLAND_DISPLAY) commands.push([\"wl-copy\", []]);\n\t\t\tif (env.DISPLAY) {\n\t\t\t\tcommands.push([\"xclip\", [\"-selection\", \"clipboard\"]], [\"xsel\", [\"--clipboard\", \"--input\"]]);\n\t\t\t}\n\t\t}\n\t\tfor (const [command, args] of commands) {\n\t\t\tif ((await runClipboardCommand(command, args, { input: text, timeoutMs: 5000 })) !== undefined) {\n\t\t\t\tcopied = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tlet osc52Emitted = false;\n\tif (!copied && p === \"linux\" && isWSL(env)) {\n\t\t// Windows Terminal supports OSC 52; prefer it over the slower PowerShell round trip.\n\t\tif (env.WT_SESSION) osc52Emitted = emitOsc52(text);\n\t\tcopied = osc52Emitted || (await copyViaWindowsClipboard(text));\n\t}\n\t// OSC 52 cannot be verified, so a desktop session with a display reports the failure\n\t// instead (#9618). Without a display the terminal is the only clipboard route (containers,\n\t// WSL without WSLg), and remote sessions always emit it to reach the client clipboard.\n\tconst headless = p === \"linux\" && !env.DISPLAY && !env.WAYLAND_DISPLAY && !env.TERMUX_VERSION;\n\tlet oversized = false;\n\tif (!osc52Emitted && (isRemoteSession(env) || (!copied && headless))) {\n\t\tif (emitOsc52(text)) copied = true;\n\t\telse oversized = true;\n\t}\n\tif (copied) return;\n\tif (oversized) throw new Error(\"Clipboard unavailable: text exceeds the OSC 52 size limit\");\n\tif (p === \"linux\") {\n\t\tif (env.TERMUX_VERSION) {\n\t\t\tthrow new Error(\"Clipboard unavailable: install the Termux:API app and `termux-api` package\");\n\t\t}\n\t\tif (env.WAYLAND_DISPLAY) {\n\t\t\tthrow new Error(\"Clipboard unavailable: install `wl-clipboard` (`wl-copy`) or check Wayland access\");\n\t\t}\n\t\tif (env.DISPLAY) {\n\t\t\tthrow new Error(\"Clipboard unavailable: install `xclip` or `xsel`, or check X11 access\");\n\t\t}\n\t}\n\tthrow new Error(\"Clipboard unavailable\");\n}\n"]}
|
|
1
|
+
{"version":3,"file":"clipboard.js","sourceRoot":"","sources":["../../src/utils/clipboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC,MAAM,wBAAwB,GAAG,OAAO,CAAC;AAEzC,SAAS,eAAe,CAAC,GAAsB;IAC9C,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC;AAC7E,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACrD,IAAI,OAAO,CAAC,MAAM,GAAG,wBAAwB,EAAE,CAAC;QAC/C,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,OAAO,MAAM,CAAC,CAAC;IACjD,OAAO,IAAI,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,uBAAuB,CAAC,IAAY;IAClD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,eAAe,UAAU,EAAE,MAAM,CAAC,CAAC;IAClE,IAAI,CAAC;QACJ,aAAa,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAChE,MAAM,OAAO,GAAG,CAAC,MAAM,mBAAmB,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3F,EAAE,QAAQ,CAAC,MAAM,CAAC;aACjB,IAAI,EAAE,CAAC;QACT,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAC3B,MAAM,MAAM,GAAG,wDAAwD,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,mCAAmC,CAAC;QACxI,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,gBAAgB,EAAE,CAAC,YAAY,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE;YAC9F,SAAS,EAAE,IAAI;SACf,CAAC,CAAC;QACH,OAAO,MAAM,KAAK,SAAS,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACV,IAAI,CAAC;YACJ,UAAU,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACR,sCAAsC;QACvC,CAAC;IACF,CAAC;AACF,CAAC;AAED,iDAAiD;AACjD,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACtC,MAAM,QAAQ,GAAyB,EAAE,CAAC;IAC1C,2DAA2D;IAC3D,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc;QAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC,CAAC;IAC5E,IAAI,QAAQ,EAAE,KAAK,OAAO,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe;YAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QACjG,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;YACzB,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;QACtG,CAAC;IACF,CAAC;IACD,KAAK,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,mBAAmB,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5E,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC;IAChE,CAAC;IACD,IAAI,CAAC;QACJ,OAAO,CAAC,MAAM,kBAAkB,EAAE,EAAE,OAAO,EAAE,CAAC,IAAI,IAAI,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,sBAAsB;IAC3C,MAAM,KAAK,GAAG,MAAM,kBAAkB,EAAE,EAAE,YAAY,EAAE,EAAE,CAAC;IAC3D,OAAO,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACrC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAY;IACjD,MAAM,CAAC,GAAG,QAAQ,EAAE,CAAC;IACrB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IACxB,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,8EAA8E;IAC9E,4EAA4E;IAC5E,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;QACnB,IAAI,CAAC;YACJ,MAAM,SAAS,GAAG,kBAAkB,EAAE,CAAC;YACvC,IAAI,SAAS,EAAE,OAAO,EAAE,CAAC;gBACxB,MAAM,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAC9B,MAAM,GAAG,IAAI,CAAC;YACf,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,8BAA8B;QAC/B,CAAC;IACF,CAAC;IACD,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,QAAQ,GAAyB,EAAE,CAAC;QAC1C,IAAI,CAAC,KAAK,QAAQ;YAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC;aAC7C,IAAI,CAAC,KAAK,OAAO;YAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;aAC/C,CAAC;YACL,IAAI,GAAG,CAAC,cAAc;gBAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC,CAAC;YACpE,IAAI,GAAG,CAAC,eAAe;gBAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;YACxD,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;gBACjB,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;YAC7F,CAAC;QACF,CAAC;QACD,KAAK,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,QAAQ,EAAE,CAAC;YACxC,IAAI,CAAC,MAAM,mBAAmB,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;gBAChG,MAAM,GAAG,IAAI,CAAC;gBACd,MAAM;YACP,CAAC;QACF,CAAC;IACF,CAAC;IACD,IAAI,YAAY,GAAG,KAAK,CAAC;IACzB,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5C,qFAAqF;QACrF,IAAI,GAAG,CAAC,UAAU;YAAE,YAAY,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QACnD,MAAM,GAAG,YAAY,IAAI,CAAC,MAAM,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC;IAChE,CAAC;IACD,qFAAqF;IACrF,2FAA2F;IAC3F,uFAAuF;IACvF,MAAM,QAAQ,GAAG,CAAC,KAAK,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC;IAC9F,IAAI,SAAS,GAAG,KAAK,CAAC;IACtB,IAAI,CAAC,YAAY,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,QAAQ,CAAC,CAAC,EAAE,CAAC;QACtE,IAAI,SAAS,CAAC,IAAI,CAAC;YAAE,MAAM,GAAG,IAAI,CAAC;;YAC9B,SAAS,GAAG,IAAI,CAAC;IACvB,CAAC;IACD,IAAI,MAAM;QAAE,OAAO;IACnB,IAAI,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC5F,IAAI,GAAG,CAAC,cAAc,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,4EAA4E,CAAC,CAAC;IAC/F,CAAC;IACD,IAAI,CAAC,KAAK,OAAO,EAAE,CAAC;QACnB,IAAI,GAAG,CAAC,eAAe,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,mFAAmF,CAAC,CAAC;QACtG,CAAC;QACD,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;QAC1F,CAAC;IACF,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;AAC1C,CAAC","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { unlinkSync, writeFileSync } from \"node:fs\";\nimport { platform, tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { getNativeClipboard } from \"@earendil-works/pi-tui\";\nimport { runClipboardCommand } from \"./clipboard-command.ts\";\nimport { isWSL } from \"./wsl.ts\";\n\nconst MAX_OSC52_ENCODED_LENGTH = 100_000;\n\nfunction isRemoteSession(env: NodeJS.ProcessEnv): boolean {\n\treturn Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION);\n}\n\nfunction emitOsc52(text: string): boolean {\n\tconst encoded = Buffer.from(text).toString(\"base64\");\n\tif (encoded.length > MAX_OSC52_ENCODED_LENGTH) {\n\t\treturn false;\n\t}\n\tprocess.stdout.write(`\\x1b]52;c;${encoded}\\x07`);\n\treturn true;\n}\n\n/**\n * WSL without WSLg has no Linux display, so the Windows clipboard is written through\n * interop. PowerShell reads the text from a file because `clip.exe` and PowerShell stdin\n * decode piped bytes with the console code page, which mangles non-ASCII UTF-8.\n */\nasync function copyViaWindowsClipboard(text: string): Promise<boolean> {\n\tconst tmpFile = join(tmpdir(), `pi-wsl-clip-${randomUUID()}.txt`);\n\ttry {\n\t\twriteFileSync(tmpFile, text, { encoding: \"utf8\", mode: 0o600 });\n\t\tconst winPath = (await runClipboardCommand(\"wslpath\", [\"-w\", tmpFile], { timeoutMs: 1000 }))\n\t\t\t?.toString(\"utf8\")\n\t\t\t.trim();\n\t\tif (!winPath) return false;\n\t\tconst script = `Set-Clipboard -Value ([System.IO.File]::ReadAllText('${winPath.replaceAll(\"'\", \"''\")}', [System.Text.Encoding]::UTF8))`;\n\t\tconst result = await runClipboardCommand(\"powershell.exe\", [\"-NoProfile\", \"-Command\", script], {\n\t\t\ttimeoutMs: 5000,\n\t\t});\n\t\treturn result !== undefined;\n\t} catch {\n\t\treturn false;\n\t} finally {\n\t\ttry {\n\t\t\tunlinkSync(tmpFile);\n\t\t} catch {\n\t\t\t// The file may not have been created.\n\t\t}\n\t}\n}\n\n/** Read plain text from the system clipboard. */\nexport async function readClipboardText(): Promise<string | null> {\n\tconst commands: [string, string[]][] = [];\n\t// Termux reports platform \"android\", not \"linux\" (#10391).\n\tif (process.env.TERMUX_VERSION) commands.push([\"termux-clipboard-get\", []]);\n\tif (platform() === \"linux\") {\n\t\tif (process.env.WAYLAND_DISPLAY) commands.push([\"wl-paste\", [\"--no-newline\", \"--type\", \"text\"]]);\n\t\tif (process.env.DISPLAY) {\n\t\t\tcommands.push([\"xclip\", [\"-selection\", \"clipboard\", \"-out\"]], [\"xsel\", [\"--clipboard\", \"--output\"]]);\n\t\t}\n\t}\n\tfor (const [command, args] of commands) {\n\t\tconst bytes = await runClipboardCommand(command, args, { timeoutMs: 5000 });\n\t\tif (bytes !== undefined) return bytes.toString(\"utf8\") || null;\n\t}\n\ttry {\n\t\treturn (await getNativeClipboard()?.getText()) || null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/** Read file paths, such as Finder file copies, from the native clipboard. */\nexport async function readClipboardFilePaths(): Promise<string[] | null> {\n\tconst paths = await getNativeClipboard()?.getFilePaths?.();\n\treturn paths?.length ? paths : null;\n}\n\nexport async function copyToClipboard(text: string): Promise<void> {\n\tconst p = platform();\n\tconst env = process.env;\n\tlet copied = false;\n\t// Direct writes precede OSC 52 so the terminal cannot race the native writer.\n\t// Linux tools retain clipboard selection ownership after this call returns.\n\tif (p !== \"linux\") {\n\t\ttry {\n\t\t\tconst clipboard = getNativeClipboard();\n\t\t\tif (clipboard?.setText) {\n\t\t\t\tawait clipboard.setText(text);\n\t\t\t\tcopied = true;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Try platform commands next.\n\t\t}\n\t}\n\tif (!copied) {\n\t\tconst commands: [string, string[]][] = [];\n\t\tif (p === \"darwin\") commands.push([\"pbcopy\", []]);\n\t\telse if (p === \"win32\") commands.push([\"clip\", []]);\n\t\telse {\n\t\t\tif (env.TERMUX_VERSION) commands.push([\"termux-clipboard-set\", []]);\n\t\t\tif (env.WAYLAND_DISPLAY) commands.push([\"wl-copy\", []]);\n\t\t\tif (env.DISPLAY) {\n\t\t\t\tcommands.push([\"xclip\", [\"-selection\", \"clipboard\"]], [\"xsel\", [\"--clipboard\", \"--input\"]]);\n\t\t\t}\n\t\t}\n\t\tfor (const [command, args] of commands) {\n\t\t\tif ((await runClipboardCommand(command, args, { input: text, timeoutMs: 5000 })) !== undefined) {\n\t\t\t\tcopied = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\tlet osc52Emitted = false;\n\tif (!copied && p === \"linux\" && isWSL(env)) {\n\t\t// Windows Terminal supports OSC 52; prefer it over the slower PowerShell round trip.\n\t\tif (env.WT_SESSION) osc52Emitted = emitOsc52(text);\n\t\tcopied = osc52Emitted || (await copyViaWindowsClipboard(text));\n\t}\n\t// OSC 52 cannot be verified, so a desktop session with a display reports the failure\n\t// instead (#9618). Without a display the terminal is the only clipboard route (containers,\n\t// WSL without WSLg), and remote sessions always emit it to reach the client clipboard.\n\tconst headless = p === \"linux\" && !env.DISPLAY && !env.WAYLAND_DISPLAY && !env.TERMUX_VERSION;\n\tlet oversized = false;\n\tif (!osc52Emitted && (isRemoteSession(env) || (!copied && headless))) {\n\t\tif (emitOsc52(text)) copied = true;\n\t\telse oversized = true;\n\t}\n\tif (copied) return;\n\tif (oversized) throw new Error(\"Clipboard unavailable: text exceeds the OSC 52 size limit\");\n\tif (env.TERMUX_VERSION) {\n\t\tthrow new Error(\"Clipboard unavailable: install the Termux:API app and `termux-api` package\");\n\t}\n\tif (p === \"linux\") {\n\t\tif (env.WAYLAND_DISPLAY) {\n\t\t\tthrow new Error(\"Clipboard unavailable: install `wl-clipboard` (`wl-copy`) or check Wayland access\");\n\t\t}\n\t\tif (env.DISPLAY) {\n\t\t\tthrow new Error(\"Clipboard unavailable: install `xclip` or `xsel`, or check X11 access\");\n\t\t}\n\t}\n\tthrow new Error(\"Clipboard unavailable\");\n}\n"]}
|
|
@@ -13,6 +13,18 @@ export interface ResizedImage {
|
|
|
13
13
|
height: number;
|
|
14
14
|
wasResized: boolean;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Tag on image resize worker replies. Node can post its own messages on the worker channel
|
|
18
|
+
* (for example `{ "watch:require": [...] }` under `node --watch`), so replies must be identifiable.
|
|
19
|
+
*/
|
|
20
|
+
export declare const IMAGE_RESIZE_WORKER_RESPONSE_TYPE = "pi:image-resize-response";
|
|
21
|
+
export type ResizeImageWorkerResponse = {
|
|
22
|
+
type: typeof IMAGE_RESIZE_WORKER_RESPONSE_TYPE;
|
|
23
|
+
result: ResizedImage | null;
|
|
24
|
+
} | {
|
|
25
|
+
type: typeof IMAGE_RESIZE_WORKER_RESPONSE_TYPE;
|
|
26
|
+
error: string;
|
|
27
|
+
};
|
|
16
28
|
/**
|
|
17
29
|
* Resize an image to fit within the specified max dimensions and encoded file size.
|
|
18
30
|
* Returns null if the image cannot be resized below maxBytes.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize-core.d.ts","sourceRoot":"","sources":["../../src/utils/image-resize-core.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"image-resize-core.d.ts","sourceRoot":"","sources":["../../src/utils/image-resize-core.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,iCAAiC,6BAA6B,CAAC;AAE5E,MAAM,MAAM,yBAAyB,GAClC;IAAE,IAAI,EAAE,OAAO,iCAAiC,CAAC;IAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAA;CAAE,GAC/E;IAAE,IAAI,EAAE,OAAO,iCAAiC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA2BrE;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CACzC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAqG9B"}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { applyExifOrientation } from "./exif-orientation.js";
|
|
2
2
|
import { loadPhoton } from "./photon.js";
|
|
3
|
+
/**
|
|
4
|
+
* Tag on image resize worker replies. Node can post its own messages on the worker channel
|
|
5
|
+
* (for example `{ "watch:require": [...] }` under `node --watch`), so replies must be identifiable.
|
|
6
|
+
*/
|
|
7
|
+
export const IMAGE_RESIZE_WORKER_RESPONSE_TYPE = "pi:image-resize-response";
|
|
3
8
|
// 4.5MB of base64 payload. Provides headroom below Anthropic's 5MB limit.
|
|
4
9
|
const DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;
|
|
5
10
|
const DEFAULT_OPTIONS = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize-core.js","sourceRoot":"","sources":["../../src/utils/image-resize-core.ts"],"names":[],"mappings":"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","sourcesContent":["import { applyExifOrientation } from \"./exif-orientation.ts\";\nimport { loadPhoton } from \"./photon.ts\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tmaxBytes?: number; // Default: 4.5MB of base64 payload (below Anthropic's 5MB limit)\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\n// 4.5MB of base64 payload. Provides headroom below Anthropic's 5MB limit.\nconst DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tmaxBytes: DEFAULT_MAX_BYTES,\n\tjpegQuality: 80,\n};\n\ninterface EncodedCandidate {\n\tdata: string;\n\tencodedSize: number;\n\tmimeType: string;\n}\n\nfunction encodeCandidate(buffer: Uint8Array, mimeType: string): EncodedCandidate {\n\tconst data = Buffer.from(buffer).toString(\"base64\");\n\treturn {\n\t\tdata,\n\t\tencodedSize: Buffer.byteLength(data, \"utf-8\"),\n\t\tmimeType,\n\t};\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and encoded file size.\n * Returns null if the image cannot be resized below maxBytes.\n *\n * Uses Photon (Rust/WASM) for image processing. If Photon is not available,\n * returns null.\n *\n * Strategy for staying under maxBytes:\n * 1. First resize to maxWidth/maxHeight\n * 2. Try both PNG and JPEG formats, pick the smaller one\n * 3. If still too large, try JPEG with decreasing quality\n * 4. If still too large, progressively reduce dimensions until 1x1\n */\nexport async function resizeImageInProcess(\n\tinputBytes: Uint8Array,\n\tmimeType: string,\n\toptions?: ImageResizeOptions,\n): Promise<ResizedImage | null> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst inputBase64Size = Math.ceil(inputBytes.byteLength / 3) * 4;\n\n\tconst photon = await loadPhoton();\n\tif (!photon) {\n\t\treturn null;\n\t}\n\n\tlet image: ReturnType<typeof photon.PhotonImage.new_from_byteslice> | undefined;\n\ttry {\n\t\tconst rawImage = photon.PhotonImage.new_from_byteslice(inputBytes);\n\t\timage = applyExifOrientation(photon, rawImage, inputBytes);\n\t\tif (image !== rawImage) rawImage.free();\n\n\t\tconst originalWidth = image.get_width();\n\t\tconst originalHeight = image.get_height();\n\t\tconst format = mimeType.split(\"/\")[1] ?? \"png\";\n\n\t\t// Check if already within all limits (dimensions AND encoded size)\n\t\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && inputBase64Size < opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: Buffer.from(inputBytes).toString(\"base64\"),\n\t\t\t\tmimeType: mimeType || `image/${format}`,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: originalWidth,\n\t\t\t\theight: originalHeight,\n\t\t\t\twasResized: false,\n\t\t\t};\n\t\t}\n\n\t\t// Calculate initial dimensions respecting max limits\n\t\tlet targetWidth = originalWidth;\n\t\tlet targetHeight = originalHeight;\n\n\t\tif (targetWidth > opts.maxWidth) {\n\t\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\t\ttargetWidth = opts.maxWidth;\n\t\t}\n\t\tif (targetHeight > opts.maxHeight) {\n\t\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\t\ttargetHeight = opts.maxHeight;\n\t\t}\n\n\t\tfunction tryEncodings(width: number, height: number, jpegQualities: number[]): EncodedCandidate[] {\n\t\t\tconst resized = photon!.resize(image!, width, height, photon!.SamplingFilter.Lanczos3);\n\n\t\t\ttry {\n\t\t\t\tconst candidates: EncodedCandidate[] = [encodeCandidate(resized.get_bytes(), \"image/png\")];\n\t\t\t\tfor (const quality of jpegQualities) {\n\t\t\t\t\tcandidates.push(encodeCandidate(resized.get_bytes_jpeg(quality), \"image/jpeg\"));\n\t\t\t\t}\n\t\t\t\treturn candidates;\n\t\t\t} finally {\n\t\t\t\tresized.free();\n\t\t\t}\n\t\t}\n\n\t\tconst qualitySteps = Array.from(new Set([opts.jpegQuality, 85, 70, 55, 40]));\n\t\tlet currentWidth = targetWidth;\n\t\tlet currentHeight = targetHeight;\n\n\t\twhile (true) {\n\t\t\tconst candidates = tryEncodings(currentWidth, currentHeight, qualitySteps);\n\t\t\tfor (const candidate of candidates) {\n\t\t\t\tif (candidate.encodedSize < opts.maxBytes) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: candidate.data,\n\t\t\t\t\t\tmimeType: candidate.mimeType,\n\t\t\t\t\t\toriginalWidth,\n\t\t\t\t\t\toriginalHeight,\n\t\t\t\t\t\twidth: currentWidth,\n\t\t\t\t\t\theight: currentHeight,\n\t\t\t\t\t\twasResized: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (currentWidth === 1 && currentHeight === 1) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tconst nextWidth = currentWidth === 1 ? 1 : Math.max(1, Math.floor(currentWidth * 0.75));\n\t\t\tconst nextHeight = currentHeight === 1 ? 1 : Math.max(1, Math.floor(currentHeight * 0.75));\n\t\t\tif (nextWidth === currentWidth && nextHeight === currentHeight) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcurrentWidth = nextWidth;\n\t\t\tcurrentHeight = nextHeight;\n\t\t}\n\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t} finally {\n\t\tif (image) {\n\t\t\timage.free();\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"image-resize-core.js","sourceRoot":"","sources":["../../src/utils/image-resize-core.ts"],"names":[],"mappings":"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","sourcesContent":["import { applyExifOrientation } from \"./exif-orientation.ts\";\nimport { loadPhoton } from \"./photon.ts\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tmaxBytes?: number; // Default: 4.5MB of base64 payload (below Anthropic's 5MB limit)\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\n/**\n * Tag on image resize worker replies. Node can post its own messages on the worker channel\n * (for example `{ \"watch:require\": [...] }` under `node --watch`), so replies must be identifiable.\n */\nexport const IMAGE_RESIZE_WORKER_RESPONSE_TYPE = \"pi:image-resize-response\";\n\nexport type ResizeImageWorkerResponse =\n\t| { type: typeof IMAGE_RESIZE_WORKER_RESPONSE_TYPE; result: ResizedImage | null }\n\t| { type: typeof IMAGE_RESIZE_WORKER_RESPONSE_TYPE; error: string };\n\n// 4.5MB of base64 payload. Provides headroom below Anthropic's 5MB limit.\nconst DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tmaxBytes: DEFAULT_MAX_BYTES,\n\tjpegQuality: 80,\n};\n\ninterface EncodedCandidate {\n\tdata: string;\n\tencodedSize: number;\n\tmimeType: string;\n}\n\nfunction encodeCandidate(buffer: Uint8Array, mimeType: string): EncodedCandidate {\n\tconst data = Buffer.from(buffer).toString(\"base64\");\n\treturn {\n\t\tdata,\n\t\tencodedSize: Buffer.byteLength(data, \"utf-8\"),\n\t\tmimeType,\n\t};\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and encoded file size.\n * Returns null if the image cannot be resized below maxBytes.\n *\n * Uses Photon (Rust/WASM) for image processing. If Photon is not available,\n * returns null.\n *\n * Strategy for staying under maxBytes:\n * 1. First resize to maxWidth/maxHeight\n * 2. Try both PNG and JPEG formats, pick the smaller one\n * 3. If still too large, try JPEG with decreasing quality\n * 4. If still too large, progressively reduce dimensions until 1x1\n */\nexport async function resizeImageInProcess(\n\tinputBytes: Uint8Array,\n\tmimeType: string,\n\toptions?: ImageResizeOptions,\n): Promise<ResizedImage | null> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst inputBase64Size = Math.ceil(inputBytes.byteLength / 3) * 4;\n\n\tconst photon = await loadPhoton();\n\tif (!photon) {\n\t\treturn null;\n\t}\n\n\tlet image: ReturnType<typeof photon.PhotonImage.new_from_byteslice> | undefined;\n\ttry {\n\t\tconst rawImage = photon.PhotonImage.new_from_byteslice(inputBytes);\n\t\timage = applyExifOrientation(photon, rawImage, inputBytes);\n\t\tif (image !== rawImage) rawImage.free();\n\n\t\tconst originalWidth = image.get_width();\n\t\tconst originalHeight = image.get_height();\n\t\tconst format = mimeType.split(\"/\")[1] ?? \"png\";\n\n\t\t// Check if already within all limits (dimensions AND encoded size)\n\t\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && inputBase64Size < opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: Buffer.from(inputBytes).toString(\"base64\"),\n\t\t\t\tmimeType: mimeType || `image/${format}`,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: originalWidth,\n\t\t\t\theight: originalHeight,\n\t\t\t\twasResized: false,\n\t\t\t};\n\t\t}\n\n\t\t// Calculate initial dimensions respecting max limits\n\t\tlet targetWidth = originalWidth;\n\t\tlet targetHeight = originalHeight;\n\n\t\tif (targetWidth > opts.maxWidth) {\n\t\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\t\ttargetWidth = opts.maxWidth;\n\t\t}\n\t\tif (targetHeight > opts.maxHeight) {\n\t\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\t\ttargetHeight = opts.maxHeight;\n\t\t}\n\n\t\tfunction tryEncodings(width: number, height: number, jpegQualities: number[]): EncodedCandidate[] {\n\t\t\tconst resized = photon!.resize(image!, width, height, photon!.SamplingFilter.Lanczos3);\n\n\t\t\ttry {\n\t\t\t\tconst candidates: EncodedCandidate[] = [encodeCandidate(resized.get_bytes(), \"image/png\")];\n\t\t\t\tfor (const quality of jpegQualities) {\n\t\t\t\t\tcandidates.push(encodeCandidate(resized.get_bytes_jpeg(quality), \"image/jpeg\"));\n\t\t\t\t}\n\t\t\t\treturn candidates;\n\t\t\t} finally {\n\t\t\t\tresized.free();\n\t\t\t}\n\t\t}\n\n\t\tconst qualitySteps = Array.from(new Set([opts.jpegQuality, 85, 70, 55, 40]));\n\t\tlet currentWidth = targetWidth;\n\t\tlet currentHeight = targetHeight;\n\n\t\twhile (true) {\n\t\t\tconst candidates = tryEncodings(currentWidth, currentHeight, qualitySteps);\n\t\t\tfor (const candidate of candidates) {\n\t\t\t\tif (candidate.encodedSize < opts.maxBytes) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: candidate.data,\n\t\t\t\t\t\tmimeType: candidate.mimeType,\n\t\t\t\t\t\toriginalWidth,\n\t\t\t\t\t\toriginalHeight,\n\t\t\t\t\t\twidth: currentWidth,\n\t\t\t\t\t\theight: currentHeight,\n\t\t\t\t\t\twasResized: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (currentWidth === 1 && currentHeight === 1) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tconst nextWidth = currentWidth === 1 ? 1 : Math.max(1, Math.floor(currentWidth * 0.75));\n\t\t\tconst nextHeight = currentHeight === 1 ? 1 : Math.max(1, Math.floor(currentHeight * 0.75));\n\t\t\tif (nextWidth === currentWidth && nextHeight === currentHeight) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcurrentWidth = nextWidth;\n\t\t\tcurrentHeight = nextHeight;\n\t\t}\n\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t} finally {\n\t\tif (image) {\n\t\t\timage.free();\n\t\t}\n\t}\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
|
-
import { resizeImageInProcess } from "./image-resize-core.js";
|
|
2
|
+
import { IMAGE_RESIZE_WORKER_RESPONSE_TYPE, resizeImageInProcess, } from "./image-resize-core.js";
|
|
3
3
|
function isResizeImageWorkerRequest(value) {
|
|
4
4
|
if (!value || typeof value !== "object")
|
|
5
5
|
return false;
|
|
@@ -17,11 +17,12 @@ port.once("message", (message) => {
|
|
|
17
17
|
throw new Error("Invalid image resize worker request");
|
|
18
18
|
}
|
|
19
19
|
const result = await resizeImageInProcess(message.inputBytes, message.mimeType, message.options);
|
|
20
|
-
const response = { result };
|
|
20
|
+
const response = { type: IMAGE_RESIZE_WORKER_RESPONSE_TYPE, result };
|
|
21
21
|
port.postMessage(response);
|
|
22
22
|
}
|
|
23
23
|
catch (error) {
|
|
24
24
|
const response = {
|
|
25
|
+
type: IMAGE_RESIZE_WORKER_RESPONSE_TYPE,
|
|
25
26
|
error: error instanceof Error ? error.message : String(error),
|
|
26
27
|
};
|
|
27
28
|
port.postMessage(response);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize-worker.js","sourceRoot":"","sources":["../../src/utils/image-resize-worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,
|
|
1
|
+
{"version":3,"file":"image-resize-worker.js","sourceRoot":"","sources":["../../src/utils/image-resize-worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EACN,iCAAiC,EAGjC,oBAAoB,GACpB,MAAM,wBAAwB,CAAC;AAQhC,SAAS,0BAA0B,CAAC,KAAc;IACjD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,OAAO,MAAM,CAAC,UAAU,YAAY,UAAU,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC;AACvF,CAAC;AAED,MAAM,IAAI,GAAG,UAAU,CAAC;AACxB,IAAI,CAAC,IAAI,EAAE,CAAC;IACX,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;AAC5D,CAAC;AAED,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,OAAgB,EAAE,EAAE;IACzC,KAAK,CAAC,KAAK,IAAI,EAAE;QAChB,IAAI,CAAC;YACJ,IAAI,CAAC,0BAA0B,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC1C,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;YACxD,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;YACjG,MAAM,QAAQ,GAA8B,EAAE,IAAI,EAAE,iCAAiC,EAAE,MAAM,EAAE,CAAC;YAChG,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,QAAQ,GAA8B;gBAC3C,IAAI,EAAE,iCAAiC;gBACvC,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;aAC7D,CAAC;YACF,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC;IACF,CAAC,CAAC,EAAE,CAAC;AACN,CAAC,CAAC,CAAC","sourcesContent":["import { parentPort } from \"node:worker_threads\";\nimport {\n\tIMAGE_RESIZE_WORKER_RESPONSE_TYPE,\n\ttype ImageResizeOptions,\n\ttype ResizeImageWorkerResponse,\n\tresizeImageInProcess,\n} from \"./image-resize-core.ts\";\n\ninterface ResizeImageWorkerRequest {\n\tinputBytes: Uint8Array;\n\tmimeType: string;\n\toptions?: ImageResizeOptions;\n}\n\nfunction isResizeImageWorkerRequest(value: unknown): value is ResizeImageWorkerRequest {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\treturn record.inputBytes instanceof Uint8Array && typeof record.mimeType === \"string\";\n}\n\nconst port = parentPort;\nif (!port) {\n\tthrow new Error(\"image resize worker requires parentPort\");\n}\n\nport.once(\"message\", (message: unknown) => {\n\tvoid (async () => {\n\t\ttry {\n\t\t\tif (!isResizeImageWorkerRequest(message)) {\n\t\t\t\tthrow new Error(\"Invalid image resize worker request\");\n\t\t\t}\n\t\t\tconst result = await resizeImageInProcess(message.inputBytes, message.mimeType, message.options);\n\t\t\tconst response: ResizeImageWorkerResponse = { type: IMAGE_RESIZE_WORKER_RESPONSE_TYPE, result };\n\t\t\tport.postMessage(response);\n\t\t} catch (error) {\n\t\t\tconst response: ResizeImageWorkerResponse = {\n\t\t\t\ttype: IMAGE_RESIZE_WORKER_RESPONSE_TYPE,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t};\n\t\t\tport.postMessage(response);\n\t\t}\n\t})();\n});\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize.d.ts","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"image-resize.d.ts","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,kBAAkB,EACvB,KAAK,YAAY,EAGjB,MAAM,wBAAwB,CAAC;AAEhC,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAmE/E;;;;;;GAMG;AACH,wBAAsB,WAAW,CAChC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAqB9B;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAO5E"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Worker } from "node:worker_threads";
|
|
2
|
-
import { resizeImageInProcess } from "./image-resize-core.js";
|
|
2
|
+
import { IMAGE_RESIZE_WORKER_RESPONSE_TYPE, resizeImageInProcess, } from "./image-resize-core.js";
|
|
3
3
|
function toTransferableBytes(input) {
|
|
4
4
|
// Transfer detaches the buffer, so transfer a worker-owned copy and leave the
|
|
5
5
|
// caller's bytes intact.
|
|
6
6
|
return new Uint8Array(input);
|
|
7
7
|
}
|
|
8
8
|
function isResizeImageWorkerResponse(value) {
|
|
9
|
-
return value !== null && typeof value === "object";
|
|
9
|
+
return (value !== null && typeof value === "object" && "type" in value && value.type === IMAGE_RESIZE_WORKER_RESPONSE_TYPE);
|
|
10
10
|
}
|
|
11
11
|
function createResizeWorker(workerSpecifier) {
|
|
12
12
|
return new Worker(workerSpecifier);
|
|
@@ -29,16 +29,15 @@ async function resizeImageInWorker(workerSpecifier, inputBytes, mimeType, option
|
|
|
29
29
|
settled = true;
|
|
30
30
|
reject(error);
|
|
31
31
|
};
|
|
32
|
-
worker.
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
worker.on("message", (message) => {
|
|
33
|
+
// Ignore messages Node posts on the worker channel itself, such as
|
|
34
|
+
// `{ "watch:require": [...] }` under `node --watch` (nodejs/node#65044).
|
|
35
|
+
if (!isResizeImageWorkerResponse(message))
|
|
35
36
|
return;
|
|
36
|
-
|
|
37
|
-
if (message.error) {
|
|
37
|
+
if ("error" in message)
|
|
38
38
|
fail(new Error(message.error));
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
settle(message.result ?? null);
|
|
39
|
+
else
|
|
40
|
+
settle(message.result);
|
|
42
41
|
});
|
|
43
42
|
worker.once("error", fail);
|
|
44
43
|
worker.once("exit", (code) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,
|
|
1
|
+
{"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"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","sourcesContent":["import { Worker } from \"node:worker_threads\";\nimport {\n\tIMAGE_RESIZE_WORKER_RESPONSE_TYPE,\n\ttype ImageResizeOptions,\n\ttype ResizedImage,\n\ttype ResizeImageWorkerResponse,\n\tresizeImageInProcess,\n} from \"./image-resize-core.ts\";\n\nexport type { ImageResizeOptions, ResizedImage } from \"./image-resize-core.ts\";\n\nfunction toTransferableBytes(input: Uint8Array): Uint8Array<ArrayBuffer> {\n\t// Transfer detaches the buffer, so transfer a worker-owned copy and leave the\n\t// caller's bytes intact.\n\treturn new Uint8Array(input);\n}\n\nfunction isResizeImageWorkerResponse(value: unknown): value is ResizeImageWorkerResponse {\n\treturn (\n\t\tvalue !== null && typeof value === \"object\" && \"type\" in value && value.type === IMAGE_RESIZE_WORKER_RESPONSE_TYPE\n\t);\n}\n\nfunction createResizeWorker(workerSpecifier: string | URL): Worker {\n\treturn new Worker(workerSpecifier);\n}\n\nasync function resizeImageInWorker(\n\tworkerSpecifier: string | URL,\n\tinputBytes: Uint8Array,\n\tmimeType: string,\n\toptions?: ImageResizeOptions,\n): Promise<ResizedImage | null> {\n\tconst worker = createResizeWorker(workerSpecifier);\n\ttry {\n\t\tconst inputBytesForWorker = toTransferableBytes(inputBytes);\n\t\treturn await new Promise<ResizedImage | null>((resolve, reject) => {\n\t\t\tlet settled = false;\n\t\t\tconst settle = (result: ResizedImage | null): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tresolve(result);\n\t\t\t};\n\t\t\tconst fail = (error: Error): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\treject(error);\n\t\t\t};\n\n\t\t\tworker.on(\"message\", (message: unknown) => {\n\t\t\t\t// Ignore messages Node posts on the worker channel itself, such as\n\t\t\t\t// `{ \"watch:require\": [...] }` under `node --watch` (nodejs/node#65044).\n\t\t\t\tif (!isResizeImageWorkerResponse(message)) return;\n\t\t\t\tif (\"error\" in message) fail(new Error(message.error));\n\t\t\t\telse settle(message.result);\n\t\t\t});\n\t\t\tworker.once(\"error\", fail);\n\t\t\tworker.once(\"exit\", (code) => {\n\t\t\t\tif (!settled) {\n\t\t\t\t\tfail(new Error(`Image resize worker exited with code ${code}`));\n\t\t\t\t}\n\t\t\t});\n\t\t\tworker.postMessage(\n\t\t\t\t{\n\t\t\t\t\tinputBytes: inputBytesForWorker,\n\t\t\t\t\tmimeType,\n\t\t\t\t\toptions,\n\t\t\t\t},\n\t\t\t\t[inputBytesForWorker.buffer],\n\t\t\t);\n\t\t});\n\t} finally {\n\t\tvoid worker.terminate().catch(() => undefined);\n\t}\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and encoded file size.\n * Runs Photon in a worker thread so WASM decoding, resizing, and encoding do not\n * block the TUI event loop. If the worker cannot be loaded (for example in some\n * Bun compiled executable layouts), fall back to in-process resizing so image\n * reads still work.\n */\nexport async function resizeImage(\n\tinputBytes: Uint8Array,\n\tmimeType: string,\n\toptions?: ImageResizeOptions,\n): Promise<ResizedImage | null> {\n\tconst isTypeScriptRuntime = import.meta.url.endsWith(\".ts\");\n\tconst workerUrl = new URL(\n\t\tisTypeScriptRuntime ? \"./image-resize-worker.ts\" : \"./image-resize-worker.js\",\n\t\timport.meta.url,\n\t);\n\n\t// Bun compiled executables resolve worker entrypoints by string path, not via\n\t// new URL(..., import.meta.url). Try the string path first under Bun so the\n\t// release binary uses the embedded worker instead of falling back in-process.\n\tif (typeof process.versions.bun === \"string\") {\n\t\ttry {\n\t\t\treturn await resizeImageInWorker(\"./src/utils/image-resize-worker.ts\", inputBytes, mimeType, options);\n\t\t} catch {}\n\t}\n\n\ttry {\n\t\treturn await resizeImageInWorker(workerUrl, inputBytes, mimeType, options);\n\t} catch {\n\t\treturn resizeImageInProcess(inputBytes, mimeType, options);\n\t}\n}\n\n/**\n * Format a dimension note for resized images.\n * This helps the model understand the coordinate mapping.\n */\nexport function formatDimensionNote(result: ResizedImage): string | undefined {\n\tif (!result.wasResized) {\n\t\treturn undefined;\n\t}\n\n\tconst scale = result.originalWidth / result.width;\n\treturn `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;\n}\n"]}
|
package/docs/cli.md
CHANGED
|
@@ -117,7 +117,7 @@ pi --tools read,grep,find,ls --print "Review this project"
|
|
|
117
117
|
See [Settings](settings.md#tools) for configuring the default tool selection.
|
|
118
118
|
|
|
119
119
|
- `-t`, `--tools <list>`<br>
|
|
120
|
-
Replaces the default selection with a comma-separated allowlist of built-in, extension, or custom tools. Entries are tool names or patterns where `*` matches any characters. MCP tools are kept unless an entry starts with `mcp__` (see [MCP tools](#mcp-tools)).
|
|
120
|
+
Replaces the default selection with a comma-separated allowlist of built-in, extension, or custom tools. Entries are tool names or patterns where `*` matches any characters. MCP tools are kept unless an entry starts with `mcp__` (see [MCP tools](#mcp-tools)). A list of only `+name` and `-name` entries is not an allowlist; it changes the default selection instead.
|
|
121
121
|
- `-xt`, `--exclude-tools <list>`<br>
|
|
122
122
|
Disables comma-separated tool names or patterns after all other selection options, MCP tools included.
|
|
123
123
|
- `-nbt`, `--no-builtin-tools`<br>
|
|
@@ -125,7 +125,7 @@ See [Settings](settings.md#tools) for configuring the default tool selection.
|
|
|
125
125
|
- `-nt`, `--no-tools`<br>
|
|
126
126
|
Starts with all built-in, extension, custom, and MCP tools disabled.
|
|
127
127
|
|
|
128
|
-
Default enabled tools are `read`, `bash`, `edit`, and `write`, unless `defaultTools` changes them. `--tools` replaces the whole selection, so name every tool you want
|
|
128
|
+
Default enabled tools are `read`, `bash`, `edit`, and `write`, unless `defaultTools` changes them. `--tools` with plain names replaces the whole selection, so name every tool you want. Like `defaultTools`, it also accepts a list of only `+name` and `-name` entries, which adds tools to or removes them from the default selection: `pi --tools +codemode,-write` keeps the other default tools, enables `codemode`, and disables `write`. These entries take exact tool names, not `*` patterns; use `--exclude-tools` to disable tools by pattern. Plain names and `+name`/`-name` entries cannot be mixed. `/reload` enables tools newly added to `defaultTools`, but a tool removed with `-name` stays removed.
|
|
129
129
|
|
|
130
130
|
<a id="mcp-tools"></a>
|
|
131
131
|
|
|
@@ -165,10 +165,10 @@ To turn on `codemode` for every session, add it to the default tools in `~/.pi/a
|
|
|
165
165
|
}
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
This keeps `read`, `bash`, `edit`, and `write` and adds `codemode`. For one invocation,
|
|
168
|
+
This keeps `read`, `bash`, `edit`, and `write` and adds `codemode`. For one invocation, add it with `--tools`:
|
|
169
169
|
|
|
170
170
|
```sh
|
|
171
|
-
pi --tools
|
|
171
|
+
pi --tools +codemode
|
|
172
172
|
```
|
|
173
173
|
|
|
174
174
|
Codemode is useful without MCP: scripts can run several tool calls in parallel, filter large output before it reaches the model, call classifier models such as TypeSafe's Jev through `models.classify()` (see [Classifier models](models.md#use-classifier-models)), and generate images through `models.generateImages()` (see [Image models](models.md#use-image-models)).
|
package/docs/codemode.md
CHANGED
|
@@ -15,7 +15,7 @@ A script may start with an options line:
|
|
|
15
15
|
- `max_output_tokens` (default 10000) limits the output. Longer output keeps its start and end, and the full text is written to a temp file whose path is included in the result. A script fails when its output passes 16777216 characters of text and base64 image data or 100000 `text()`, `image()`, and `console` calls; write large data to a file with a tool instead.
|
|
16
16
|
- `timeout_ms` is a hard deadline for the whole script. It is unset by default. Image generation can take minutes, so do not set a short deadline for scripts that generate images.
|
|
17
17
|
|
|
18
|
-
The result starts with `Script completed` or `Script failed`, the wall time, and the output. A failed script keeps its partial output, followed by `Script error:` and the error. Tool calls are real: calls made before a failure are not undone. Calls still running when the script ends are cancelled, and unawaited promises are discarded.
|
|
18
|
+
The result starts with `Script completed` or `Script failed`, the wall time, and the output. Text and image items appear in order, each on its own line. When the output has more than one text item (from `text()` or `return`), each starts with a `==> text N/M <==` line. `console` calls follow in one `<console_output>` block with one line per call. A failed script keeps its partial output, followed by `Script error:` and the error. Tool calls are real: calls made before a failure are not undone. Calls still running when the script ends are cancelled, and unawaited promises are discarded.
|
|
19
19
|
|
|
20
20
|
## Globals
|
|
21
21
|
|
|
@@ -24,7 +24,7 @@ The result starts with `Script completed` or `Script failed`, the wall time, and
|
|
|
24
24
|
| `tools.<name>(args)` | Call a tool. See [Call tools](#call-tools). |
|
|
25
25
|
| `text(value)` | Add a text item to the output. Strings are added as is, other values as JSON. |
|
|
26
26
|
| `image(value)` | Add an image to the output: a base64 `data:` URL, an `{ image_url }` object, or an image block `{ type: "image", data, mimeType }` such as those returned by MCP tools and `models.generateImages()`. Remote URLs are not supported. PNG, JPEG, GIF, and WebP are accepted. Each image is also saved to a temp file, and the result names the path before the image. |
|
|
27
|
-
| `console.log(...)` |
|
|
27
|
+
| `console.log(...)` | Add a line to the `<console_output>` block after the other output. Arguments are joined with spaces; `info`, `warn`, `error`, and `debug` do the same. |
|
|
28
28
|
| `return value` | A top-level `return` adds the value like `text()`. |
|
|
29
29
|
| `exit()` | End the script successfully. |
|
|
30
30
|
| `store(key, value)` / `load(key)` | Keep small JSON values across `codemode` calls. See [Store values](#store-values). |
|
|
@@ -59,7 +59,7 @@ The store is for small state such as IDs, cursors, or summaries. One value may h
|
|
|
59
59
|
|
|
60
60
|
## Models
|
|
61
61
|
|
|
62
|
-
`models` reaches the model catalog and runs non-LLM models with the session's credentials: classifiers, which answer typed questions about JSON state, and image models, which generate images. Chat models are listed but cannot be run from scripts. Which classifier and image models exist is described in [Use classifier models](models.md#use-classifier-models) and [Use image models](models.md#use-image-models).
|
|
62
|
+
`models` reaches the model catalog and runs non-LLM models with the session's credentials: classifiers, which answer typed questions about JSON state and, for some models, images, and image models, which generate images. Chat models are listed but cannot be run from scripts. Which classifier and image models exist is described in [Use classifier models](models.md#use-classifier-models) and [Use image models](models.md#use-image-models).
|
|
63
63
|
|
|
64
64
|
```ts
|
|
65
65
|
type ModelType = "chat" | "image" | "classifier";
|
|
@@ -100,6 +100,8 @@ Model IDs differ between providers, for example `typesafe/jev-latest` and `openr
|
|
|
100
100
|
interface ClassifierContext {
|
|
101
101
|
/** The data to classify. */
|
|
102
102
|
state: Record<string, unknown>;
|
|
103
|
+
/** Images judged together with `state`. Only models whose `input` includes "image" accept them. */
|
|
104
|
+
images?: { type: "image"; data: string; mimeType: string }[];
|
|
103
105
|
/** Questions by ID. One call answers all of them. */
|
|
104
106
|
questions: Record<string, ClassifierQuestion>;
|
|
105
107
|
}
|
|
@@ -163,6 +165,24 @@ return results.map((result, i) =>
|
|
|
163
165
|
);
|
|
164
166
|
```
|
|
165
167
|
|
|
168
|
+
Classifiers whose `input` includes `"image"` also judge images. `tools.read()` returns an image file as an image block that `images` accepts. Other classifiers return an error result when `images` is not empty.
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
const luna = await models.getModelOfType("classifier", "openai", "gpt-6-luna");
|
|
172
|
+
const photo = await tools.read({ path: "screenshot.png" });
|
|
173
|
+
const result = await models.classify(luna, {
|
|
174
|
+
state: { task: "Settings page redesign" },
|
|
175
|
+
images: [photo],
|
|
176
|
+
questions: {
|
|
177
|
+
broken: {
|
|
178
|
+
type: "bool",
|
|
179
|
+
instructions: "Does the screenshot show a broken layout?",
|
|
180
|
+
criteria: { true: "Overlapping, cut-off, or misaligned elements", false: "Clean layout" },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
166
186
|
### Generate images
|
|
167
187
|
|
|
168
188
|
```ts
|
|
@@ -89,6 +89,7 @@ These variables are read by Pi itself:
|
|
|
89
89
|
| `PI_RADIUS_GATEWAY` | Override the Radius gateway origin used by `/bug` uploads and Radius relay connections |
|
|
90
90
|
| `PI_HARDWARE_CURSOR` | Set to `1` to show the hardware cursor; see [Terminal setup](terminal-setup.md) |
|
|
91
91
|
| `PI_HYPERLINKS` | Override OSC 8 hyperlink detection with `1`, `0`, or `auto` |
|
|
92
|
+
| `PI_PROGRAM_STATUS` | Override OSC 7501 program status detection: `1` always reports, `0` never reports; otherwise Pi reports only after the terminal confirms support. See [Terminal setup](terminal-setup.md#program-status) |
|
|
92
93
|
| `PI_IMAGE_PROTOCOL` | Override inline image detection with `kitty`, `iterm2`, `none`, or `auto` |
|
|
93
94
|
| `PI_TRUE_COLOR` | Override truecolor detection with `1`, `0`, or `auto` |
|
|
94
95
|
| `PI_TUI_ESC_TIMEOUT` | How long to wait after a lone ESC before treating it as Escape, in milliseconds; defaults to `100` over SSH and `10` otherwise. Increase if Alt-key input is misread as Escape |
|
package/docs/json.md
CHANGED
|
@@ -42,7 +42,7 @@ A basic run produces records like these:
|
|
|
42
42
|
{"type":"message_end","message":{"role":"assistant","...":"..."}}
|
|
43
43
|
{"type":"turn_end","message":{"role":"assistant","...":"..."},"toolResults":[]}
|
|
44
44
|
{"type":"agent_end","messages":[{"...":"..."}],"willRetry":false}
|
|
45
|
-
{"type":"agent_settled"}
|
|
45
|
+
{"type":"agent_settled","aborted":false}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
`agent_end` closes one low-level agent run. Automatic retry, overflow recovery, compaction retry, steering, or follow-up work can still continue. `agent_settled` means Pi has no remaining automatic work for that session-level run.
|
|
@@ -53,7 +53,7 @@ A basic run produces records like these:
|
|
|
53
53
|
|---|---|---|
|
|
54
54
|
| `agent_start` | None | A low-level agent run started. |
|
|
55
55
|
| `agent_end` | `messages`, `willRetry` | That low-level run ended. `messages` contains messages generated by the run. |
|
|
56
|
-
| `agent_settled` |
|
|
56
|
+
| `agent_settled` | `aborted` | Pi will not continue automatically through retries, compaction recovery, or queued messages. `aborted` is `true` when the run ended because it was aborted. |
|
|
57
57
|
| `turn_start` | None | One assistant turn started. |
|
|
58
58
|
| `turn_end` | `message`, `toolResults` | One assistant response and its resulting tool calls finished. |
|
|
59
59
|
|
|
@@ -104,7 +104,7 @@ The top-level `usage` is the latest cumulative provider-reported usage for the a
|
|
|
104
104
|
|---|---|---|
|
|
105
105
|
| `tool_execution_start` | `toolCallId`, `toolName`, `args` | Tool execution started. |
|
|
106
106
|
| `tool_execution_update` | `toolCallId`, `toolName`, `args`, `partialResult` | The tool reported a partial result. |
|
|
107
|
-
| `tool_execution_end` | `toolCallId`, `toolName`, `result`, `isError` | Tool execution finished. |
|
|
107
|
+
| `tool_execution_end` | `toolCallId`, `toolName`, `result`, `isError`, `durationMs` | Tool execution finished. `durationMs` is how long the tool's `execute()` took, measured with a monotonic clock; absent when the tool did not run. |
|
|
108
108
|
|
|
109
109
|
Use `toolCallId` to correlate the lifecycle. `partialResult` is the latest partial result supplied by the tool. Whether it replaces or extends an earlier update depends on that tool's result contract.
|
|
110
110
|
|
package/docs/llama-cpp.md
CHANGED
|
@@ -88,7 +88,14 @@ If the router disconnects, `/llama` shows **Retry** and **Close**. Retry reconne
|
|
|
88
88
|
|
|
89
89
|
## Classification
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
Classifier models answer typed `choice`, `bool`, and `score` questions about JSON state, like TypeSafe's Jev models. The model reaches them from [`codemode`](cli.md#enable-codemode) scripts, and extensions through `ctx.modelRegistry.classify()`; see [Classifier models](models.md#use-classifier-models). Pi lists llama.cpp models as classifiers in two ways:
|
|
92
|
+
|
|
93
|
+
- **Decision models** such as [Julia-1, Laya, Kev, lev, and OpenJev](https://huggingface.co/collections/ggml-org/decision-models-6abf80cca3c83f127060a769) answer natively through llama.cpp's `/v1/systemone` endpoint. They appear only as classifiers, with the `typesafe-system-one` API, and not in `/model`.
|
|
94
|
+
- **Chat models** are also listed as classifiers with the same ID and the `llama-cpp-classify` API, which reads answers from next-token probabilities as described below.
|
|
95
|
+
|
|
96
|
+
llama.cpp 0.6.0 and later report decision models in the router's model list: their `architecture.output_modalities` contains `decisions`. The router reads this from the GGUF metadata without loading the model, so Pi recognizes unloaded and sleeping decision models too. Older llama.cpp builds do not report it, and Pi lists their decision models as chat models.
|
|
97
|
+
|
|
98
|
+
### Chat models as classifiers
|
|
92
99
|
|
|
93
100
|
The model does not generate an answer. Each question becomes one chat prompt: the state, every question of the request, the state again, and then the question with its answers under single-token labels. Labels are letters for a choice (up to 62 options), `Yes`/`No` for a bool, and digits for a score (up to 10 levels). The second copy of the state is read with the questions in view, which improved accuracy on JevBench with small models. Pi reads the probabilities of the labels as the next token and normalizes them. A choice returns every option's probability and a confidence of `(n * peak - 1) / (n - 1)`; a score returns the expected level.
|
|
94
101
|
|