@fleetagent/pi-coding-agent 0.0.6 → 0.0.7
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 +17 -0
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +2 -3
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -2
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +9 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +86 -15
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/export-html/template.js +6 -3
- package/dist/core/extensions/runner.d.ts +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +8 -2
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +4 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +65 -13
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/output-guard.d.ts +1 -0
- package/dist/core/output-guard.d.ts.map +1 -1
- package/dist/core/output-guard.js +52 -22
- package/dist/core/output-guard.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +31 -12
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/pi-agent.d.ts.map +1 -1
- package/dist/core/pi-agent.js +12 -3
- package/dist/core/pi-agent.js.map +1 -1
- package/dist/core/resolve-config-value.d.ts +9 -1
- package/dist/core/resolve-config-value.d.ts.map +1 -1
- package/dist/core/resolve-config-value.js +134 -11
- package/dist/core/resolve-config-value.js.map +1 -1
- package/dist/core/session/jsonl-helpers.d.ts +2 -1
- package/dist/core/session/jsonl-helpers.d.ts.map +1 -1
- package/dist/core/session/jsonl-helpers.js +6 -3
- package/dist/core/session/jsonl-helpers.js.map +1 -1
- package/dist/core/session/local-session-manager.d.ts +1 -0
- package/dist/core/session/local-session-manager.d.ts.map +1 -1
- package/dist/core/session/local-session-manager.js +12 -4
- package/dist/core/session/local-session-manager.js.map +1 -1
- package/dist/core/session/session-manager.d.ts +1 -0
- package/dist/core/session/session-manager.d.ts.map +1 -1
- package/dist/core/session/session-manager.js.map +1 -1
- package/dist/core/session/stores/jsonl-session-store.d.ts +2 -1
- package/dist/core/session/stores/jsonl-session-store.d.ts.map +1 -1
- package/dist/core/session/stores/jsonl-session-store.js +105 -78
- package/dist/core/session/stores/jsonl-session-store.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +14 -9
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +73 -63
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +45 -76
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +27 -12
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +11 -2
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +3 -3
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +13 -4
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +1 -0
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +37 -0
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +7 -6
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +24 -32
- package/dist/core/tools/write.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -2
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +118 -1
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +14 -5
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +1 -1
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +30 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +10 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +3 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +64 -7
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +15 -3
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts +3 -1
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js +14 -8
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/deprecation.d.ts +4 -0
- package/dist/utils/deprecation.d.ts.map +1 -0
- package/dist/utils/deprecation.js +13 -0
- package/dist/utils/deprecation.js.map +1 -0
- package/dist/utils/image-resize-core.d.ts +30 -0
- package/dist/utils/image-resize-core.d.ts.map +1 -0
- package/dist/utils/image-resize-core.js +124 -0
- package/dist/utils/image-resize-core.js.map +1 -0
- package/dist/utils/image-resize-worker.d.ts +2 -0
- package/dist/utils/image-resize-worker.d.ts.map +1 -0
- package/dist/utils/image-resize-worker.js +31 -0
- package/dist/utils/image-resize-worker.js.map +1 -0
- package/dist/utils/image-resize.d.ts +6 -27
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +60 -116
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/json.d.ts +3 -0
- package/dist/utils/json.d.ts.map +1 -0
- package/dist/utils/json.js +7 -0
- package/dist/utils/json.js.map +1 -0
- package/docs/custom-provider.md +22 -9
- package/docs/extensions.md +4 -3
- package/docs/models.md +34 -12
- package/docs/packages.md +5 -4
- package/docs/providers.md +13 -5
- package/docs/sdk.md +56 -0
- package/docs/settings.md +3 -1
- package/docs/terminal-setup.md +6 -0
- package/docs/usage.md +2 -2
- package/examples/extensions/README.md +1 -0
- package/examples/extensions/custom-provider-anthropic/index.ts +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/index.ts +54 -3
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/git-merge-and-resolve.ts +115 -0
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +13 -12
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize.d.ts","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"image-resize.d.ts","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE/E,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AA8E/E;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,kBAAkB,GAC1B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAE9B;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAO5E","sourcesContent":["import { Worker } from \"node:worker_threads\";\nimport type { ImageResizeOptions, ResizedImage } from \"./image-resize-core.ts\";\n\nexport type { ImageResizeOptions, ResizedImage } from \"./image-resize-core.ts\";\n\ninterface ResizeImageWorkerResponse {\n\tresult?: ResizedImage | null;\n\terror?: string;\n}\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 value !== null && typeof value === \"object\";\n}\n\nfunction createResizeWorker(): Worker {\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\treturn new Worker(workerUrl);\n}\n\nasync function resizeImageInWorker(\n\tinputBytes: Uint8Array,\n\tmimeType: string,\n\toptions?: ImageResizeOptions,\n): Promise<ResizedImage | null> {\n\tconst worker = createResizeWorker();\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.once(\"message\", (message: unknown) => {\n\t\t\t\tif (!isResizeImageWorkerResponse(message)) {\n\t\t\t\t\tfail(new Error(\"Invalid image resize worker response\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (message.error) {\n\t\t\t\t\tfail(new Error(message.error));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsettle(message.result ?? null);\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. Worker failures are propagated instead of retried on\n * the main thread.\n */\nexport async function resizeImage(\n\tinputBytes: Uint8Array,\n\tmimeType: string,\n\toptions?: ImageResizeOptions,\n): Promise<ResizedImage | null> {\n\treturn resizeImageInWorker(inputBytes, mimeType, options);\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"]}
|
|
@@ -1,128 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
maxWidth: 2000,
|
|
7
|
-
maxHeight: 2000,
|
|
8
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
9
|
-
jpegQuality: 80,
|
|
10
|
-
};
|
|
11
|
-
function encodeCandidate(buffer, mimeType) {
|
|
12
|
-
const data = Buffer.from(buffer).toString("base64");
|
|
13
|
-
return {
|
|
14
|
-
data,
|
|
15
|
-
encodedSize: Buffer.byteLength(data, "utf-8"),
|
|
16
|
-
mimeType,
|
|
17
|
-
};
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
function toTransferableBytes(input) {
|
|
3
|
+
// Transfer detaches the buffer, so transfer a worker-owned copy and leave the
|
|
4
|
+
// caller's bytes intact.
|
|
5
|
+
return new Uint8Array(input);
|
|
18
6
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* 3. If still too large, try JPEG with decreasing quality
|
|
30
|
-
* 4. If still too large, progressively reduce dimensions until 1x1
|
|
31
|
-
*/
|
|
32
|
-
export async function resizeImage(img, options) {
|
|
33
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
34
|
-
const inputBuffer = Buffer.from(img.data, "base64");
|
|
35
|
-
const inputBase64Size = Buffer.byteLength(img.data, "utf-8");
|
|
36
|
-
const photon = await loadPhoton();
|
|
37
|
-
if (!photon) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
let image;
|
|
7
|
+
function isResizeImageWorkerResponse(value) {
|
|
8
|
+
return value !== null && typeof value === "object";
|
|
9
|
+
}
|
|
10
|
+
function createResizeWorker() {
|
|
11
|
+
const isTypeScriptRuntime = import.meta.url.endsWith(".ts");
|
|
12
|
+
const workerUrl = new URL(isTypeScriptRuntime ? "./image-resize-worker.ts" : "./image-resize-worker.js", import.meta.url);
|
|
13
|
+
return new Worker(workerUrl);
|
|
14
|
+
}
|
|
15
|
+
async function resizeImageInWorker(inputBytes, mimeType, options) {
|
|
16
|
+
const worker = createResizeWorker();
|
|
41
17
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
originalHeight,
|
|
57
|
-
width: originalWidth,
|
|
58
|
-
height: originalHeight,
|
|
59
|
-
wasResized: false,
|
|
18
|
+
const inputBytesForWorker = toTransferableBytes(inputBytes);
|
|
19
|
+
return await new Promise((resolve, reject) => {
|
|
20
|
+
let settled = false;
|
|
21
|
+
const settle = (result) => {
|
|
22
|
+
if (settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
25
|
+
resolve(result);
|
|
26
|
+
};
|
|
27
|
+
const fail = (error) => {
|
|
28
|
+
if (settled)
|
|
29
|
+
return;
|
|
30
|
+
settled = true;
|
|
31
|
+
reject(error);
|
|
60
32
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (targetWidth > opts.maxWidth) {
|
|
66
|
-
targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);
|
|
67
|
-
targetWidth = opts.maxWidth;
|
|
68
|
-
}
|
|
69
|
-
if (targetHeight > opts.maxHeight) {
|
|
70
|
-
targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);
|
|
71
|
-
targetHeight = opts.maxHeight;
|
|
72
|
-
}
|
|
73
|
-
function tryEncodings(width, height, jpegQualities) {
|
|
74
|
-
const resized = photon.resize(image, width, height, photon.SamplingFilter.Lanczos3);
|
|
75
|
-
try {
|
|
76
|
-
const candidates = [encodeCandidate(resized.get_bytes(), "image/png")];
|
|
77
|
-
for (const quality of jpegQualities) {
|
|
78
|
-
candidates.push(encodeCandidate(resized.get_bytes_jpeg(quality), "image/jpeg"));
|
|
33
|
+
worker.once("message", (message) => {
|
|
34
|
+
if (!isResizeImageWorkerResponse(message)) {
|
|
35
|
+
fail(new Error("Invalid image resize worker response"));
|
|
36
|
+
return;
|
|
79
37
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
resized.free();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const qualitySteps = Array.from(new Set([opts.jpegQuality, 85, 70, 55, 40]));
|
|
87
|
-
let currentWidth = targetWidth;
|
|
88
|
-
let currentHeight = targetHeight;
|
|
89
|
-
while (true) {
|
|
90
|
-
const candidates = tryEncodings(currentWidth, currentHeight, qualitySteps);
|
|
91
|
-
for (const candidate of candidates) {
|
|
92
|
-
if (candidate.encodedSize < opts.maxBytes) {
|
|
93
|
-
return {
|
|
94
|
-
data: candidate.data,
|
|
95
|
-
mimeType: candidate.mimeType,
|
|
96
|
-
originalWidth,
|
|
97
|
-
originalHeight,
|
|
98
|
-
width: currentWidth,
|
|
99
|
-
height: currentHeight,
|
|
100
|
-
wasResized: true,
|
|
101
|
-
};
|
|
38
|
+
if (message.error) {
|
|
39
|
+
fail(new Error(message.error));
|
|
40
|
+
return;
|
|
102
41
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
catch {
|
|
118
|
-
return null;
|
|
42
|
+
settle(message.result ?? null);
|
|
43
|
+
});
|
|
44
|
+
worker.once("error", fail);
|
|
45
|
+
worker.once("exit", (code) => {
|
|
46
|
+
if (!settled) {
|
|
47
|
+
fail(new Error(`Image resize worker exited with code ${code}`));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
worker.postMessage({
|
|
51
|
+
inputBytes: inputBytesForWorker,
|
|
52
|
+
mimeType,
|
|
53
|
+
options,
|
|
54
|
+
}, [inputBytesForWorker.buffer]);
|
|
55
|
+
});
|
|
119
56
|
}
|
|
120
57
|
finally {
|
|
121
|
-
|
|
122
|
-
image.free();
|
|
123
|
-
}
|
|
58
|
+
void worker.terminate().catch(() => undefined);
|
|
124
59
|
}
|
|
125
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Resize an image to fit within the specified max dimensions and encoded file size.
|
|
63
|
+
* Runs Photon in a worker thread so WASM decoding, resizing, and encoding do not
|
|
64
|
+
* block the TUI event loop. Worker failures are propagated instead of retried on
|
|
65
|
+
* the main thread.
|
|
66
|
+
*/
|
|
67
|
+
export async function resizeImage(inputBytes, mimeType, options) {
|
|
68
|
+
return resizeImageInWorker(inputBytes, mimeType, options);
|
|
69
|
+
}
|
|
126
70
|
/**
|
|
127
71
|
* Format a dimension note for resized images.
|
|
128
72
|
* This helps the model understand the coordinate mapping.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAmBzC,0EAA0E;AAC1E,MAAM,iBAAiB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C,MAAM,eAAe,GAAiC;IACrD,QAAQ,EAAE,IAAI;IACd,SAAS,EAAE,IAAI;IACf,QAAQ,EAAE,iBAAiB;IAC3B,WAAW,EAAE,EAAE;CACf,CAAC;AAQF,SAAS,eAAe,CAAC,MAAkB,EAAE,QAAgB,EAAoB;IAChF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACpD,OAAO;QACN,IAAI;QACJ,WAAW,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC;QAC7C,QAAQ;KACR,CAAC;AAAA,CACF;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAiB,EAAE,OAA4B,EAAgC;IAChH,MAAM,IAAI,GAAG,EAAE,GAAG,eAAe,EAAE,GAAG,OAAO,EAAE,CAAC;IAChD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACpD,MAAM,eAAe,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAE7D,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;IAClC,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACb,CAAC;IAED,IAAI,KAA2E,CAAC;IAChF,IAAI,CAAC;QACJ,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC;QAC/C,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QACnE,KAAK,GAAG,oBAAoB,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC3D,IAAI,KAAK,KAAK,QAAQ;YAAE,QAAQ,CAAC,IAAI,EAAE,CAAC;QAExC,MAAM,aAAa,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;QACxC,MAAM,cAAc,GAAG,KAAK,CAAC,UAAU,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC;QAEpD,mEAAmE;QACnE,IAAI,aAAa,IAAI,IAAI,CAAC,QAAQ,IAAI,cAAc,IAAI,IAAI,CAAC,SAAS,IAAI,eAAe,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3G,OAAO;gBACN,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,QAAQ,EAAE,GAAG,CAAC,QAAQ,IAAI,SAAS,MAAM,EAAE;gBAC3C,aAAa;gBACb,cAAc;gBACd,KAAK,EAAE,aAAa;gBACpB,MAAM,EAAE,cAAc;gBACtB,UAAU,EAAE,KAAK;aACjB,CAAC;QACH,CAAC;QAED,qDAAqD;QACrD,IAAI,WAAW,GAAG,aAAa,CAAC;QAChC,IAAI,YAAY,GAAG,cAAc,CAAC;QAElC,IAAI,WAAW,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjC,YAAY,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,WAAW,CAAC,CAAC;YACxE,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC7B,CAAC;QACD,IAAI,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YACnC,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,YAAY,CAAC,CAAC;YACxE,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;QAC/B,CAAC;QAED,SAAS,YAAY,CAAC,KAAa,EAAE,MAAc,EAAE,aAAuB,EAAsB;YACjG,MAAM,OAAO,GAAG,MAAO,CAAC,MAAM,CAAC,KAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;YAEvF,IAAI,CAAC;gBACJ,MAAM,UAAU,GAAuB,CAAC,eAAe,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC;gBAC3F,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;oBACrC,UAAU,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC;gBACjF,CAAC;gBACD,OAAO,UAAU,CAAC;YACnB,CAAC;oBAAS,CAAC;gBACV,OAAO,CAAC,IAAI,EAAE,CAAC;YAChB,CAAC;QAAA,CACD;QAED,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7E,IAAI,YAAY,GAAG,WAAW,CAAC;QAC/B,IAAI,aAAa,GAAG,YAAY,CAAC;QAEjC,OAAO,IAAI,EAAE,CAAC;YACb,MAAM,UAAU,GAAG,YAAY,CAAC,YAAY,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC;YAC3E,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;gBACpC,IAAI,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;oBAC3C,OAAO;wBACN,IAAI,EAAE,SAAS,CAAC,IAAI;wBACpB,QAAQ,EAAE,SAAS,CAAC,QAAQ;wBAC5B,aAAa;wBACb,cAAc;wBACd,KAAK,EAAE,YAAY;wBACnB,MAAM,EAAE,aAAa;wBACrB,UAAU,EAAE,IAAI;qBAChB,CAAC;gBACH,CAAC;YACF,CAAC;YAED,IAAI,YAAY,KAAK,CAAC,IAAI,aAAa,KAAK,CAAC,EAAE,CAAC;gBAC/C,MAAM;YACP,CAAC;YAED,MAAM,SAAS,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC;YACxF,MAAM,UAAU,GAAG,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC;YAC3F,IAAI,SAAS,KAAK,YAAY,IAAI,UAAU,KAAK,aAAa,EAAE,CAAC;gBAChE,MAAM;YACP,CAAC;YAED,YAAY,GAAG,SAAS,CAAC;YACzB,aAAa,GAAG,UAAU,CAAC;QAC5B,CAAC;QAED,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;YAAS,CAAC;QACV,IAAI,KAAK,EAAE,CAAC;YACX,KAAK,CAAC,IAAI,EAAE,CAAC;QACd,CAAC;IACF,CAAC;AAAA,CACD;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAoB,EAAsB;IAC7E,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC;IAClD,OAAO,oBAAoB,MAAM,CAAC,aAAa,IAAI,MAAM,CAAC,cAAc,kBAAkB,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,6BAA6B,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,6BAA6B,CAAC;AAAA,CAClM","sourcesContent":["import type { ImageContent } from \"@fleetagent/pi-ai\";\nimport { 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 resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage | null> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst inputBuffer = Buffer.from(img.data, \"base64\");\n\tconst inputBase64Size = Buffer.byteLength(img.data, \"utf-8\");\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 inputBytes = new Uint8Array(inputBuffer);\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 = img.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: img.data,\n\t\t\t\tmimeType: img.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\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"]}
|
|
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 type { ImageResizeOptions, ResizedImage } from \"./image-resize-core.ts\";\n\nexport type { ImageResizeOptions, ResizedImage } from \"./image-resize-core.ts\";\n\ninterface ResizeImageWorkerResponse {\n\tresult?: ResizedImage | null;\n\terror?: string;\n}\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 value !== null && typeof value === \"object\";\n}\n\nfunction createResizeWorker(): Worker {\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\treturn new Worker(workerUrl);\n}\n\nasync function resizeImageInWorker(\n\tinputBytes: Uint8Array,\n\tmimeType: string,\n\toptions?: ImageResizeOptions,\n): Promise<ResizedImage | null> {\n\tconst worker = createResizeWorker();\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.once(\"message\", (message: unknown) => {\n\t\t\t\tif (!isResizeImageWorkerResponse(message)) {\n\t\t\t\t\tfail(new Error(\"Invalid image resize worker response\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (message.error) {\n\t\t\t\t\tfail(new Error(message.error));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsettle(message.result ?? null);\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. Worker failures are propagated instead of retried on\n * the main thread.\n */\nexport async function resizeImage(\n\tinputBytes: Uint8Array,\n\tmimeType: string,\n\toptions?: ImageResizeOptions,\n): Promise<ResizedImage | null> {\n\treturn resizeImageInWorker(inputBytes, mimeType, options);\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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../src/utils/json.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvD","sourcesContent":["/** Strip `//` line comments and trailing commas from JSON, leaving string literals untouched. */\nexport function stripJsonComments(input: string): string {\n\treturn input\n\t\t.replace(/\"(?:\\\\.|[^\"\\\\])*\"|\\/\\/[^\\n]*/g, (m) => (m[0] === '\"' ? m : \"\"))\n\t\t.replace(/\"(?:\\\\.|[^\"\\\\])*\"|,(\\s*[}\\]])/g, (m, tail) => tail ?? (m[0] === '\"' ? m : \"\"));\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Strip `//` line comments and trailing commas from JSON, leaving string literals untouched. */
|
|
2
|
+
export function stripJsonComments(input) {
|
|
3
|
+
return input
|
|
4
|
+
.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : ""))
|
|
5
|
+
.replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? (m[0] === '"' ? m : ""));
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=json.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/utils/json.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAU;IACxD,OAAO,KAAK;SACV,OAAO,CAAC,+BAA+B,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;SACxE,OAAO,CAAC,gCAAgC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAAA,CAC1F","sourcesContent":["/** Strip `//` line comments and trailing commas from JSON, leaving string literals untouched. */\nexport function stripJsonComments(input: string): string {\n\treturn input\n\t\t.replace(/\"(?:\\\\.|[^\"\\\\])*\"|\\/\\/[^\\n]*/g, (m) => (m[0] === '\"' ? m : \"\"))\n\t\t.replace(/\"(?:\\\\.|[^\"\\\\])*\"|,(\\s*[}\\]])/g, (m, tail) => tail ?? (m[0] === '\"' ? m : \"\"));\n}\n"]}
|
package/docs/custom-provider.md
CHANGED
|
@@ -43,7 +43,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
43
43
|
pi.registerProvider("my-provider", {
|
|
44
44
|
name: "My Provider",
|
|
45
45
|
baseUrl: "https://api.example.com",
|
|
46
|
-
apiKey: "MY_API_KEY",
|
|
46
|
+
apiKey: "$MY_API_KEY",
|
|
47
47
|
api: "openai-completions",
|
|
48
48
|
models: [
|
|
49
49
|
{
|
|
@@ -83,7 +83,7 @@ pi.registerProvider("openai", {
|
|
|
83
83
|
pi.registerProvider("google", {
|
|
84
84
|
baseUrl: "https://ai-gateway.corp.com/google",
|
|
85
85
|
headers: {
|
|
86
|
-
"X-Corp-Auth": "CORP_AUTH_TOKEN" // env var or literal
|
|
86
|
+
"X-Corp-Auth": "$CORP_AUTH_TOKEN" // env var or literal
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
```
|
|
@@ -112,7 +112,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
112
112
|
|
|
113
113
|
pi.registerProvider("local-openai", {
|
|
114
114
|
baseUrl: "http://localhost:1234/v1",
|
|
115
|
-
apiKey: "LOCAL_OPENAI_API_KEY",
|
|
115
|
+
apiKey: "$LOCAL_OPENAI_API_KEY",
|
|
116
116
|
api: "openai-completions",
|
|
117
117
|
models: payload.data.map((model) => ({
|
|
118
118
|
id: model.id,
|
|
@@ -132,7 +132,7 @@ This registers the fetched models before startup finishes.
|
|
|
132
132
|
```typescript
|
|
133
133
|
pi.registerProvider("my-llm", {
|
|
134
134
|
baseUrl: "https://api.my-llm.com/v1",
|
|
135
|
-
apiKey: "MY_LLM_API_KEY", // env var
|
|
135
|
+
apiKey: "$MY_LLM_API_KEY", // env var reference
|
|
136
136
|
api: "openai-completions", // which streaming API to use
|
|
137
137
|
models: [
|
|
138
138
|
{
|
|
@@ -155,6 +155,8 @@ pi.registerProvider("my-llm", {
|
|
|
155
155
|
|
|
156
156
|
When `models` is provided, it **replaces** all existing models for that provider.
|
|
157
157
|
|
|
158
|
+
`apiKey` and custom header values use the same config value syntax as `models.json`: `!command` at the start executes a command for the whole value, `$ENV_VAR` and `${ENV_VAR}` interpolate environment variables, `$$` emits a literal `$`, and `$!` emits a literal `!`.
|
|
159
|
+
|
|
158
160
|
## Unregister Provider
|
|
159
161
|
|
|
160
162
|
Use `pi.unregisterProvider(name)` to remove a provider that was previously registered via `pi.registerProvider(name, ...)`:
|
|
@@ -163,7 +165,7 @@ Use `pi.unregisterProvider(name)` to remove a provider that was previously regis
|
|
|
163
165
|
// Register
|
|
164
166
|
pi.registerProvider("my-llm", {
|
|
165
167
|
baseUrl: "https://api.my-llm.com/v1",
|
|
166
|
-
apiKey: "MY_LLM_API_KEY",
|
|
168
|
+
apiKey: "$MY_LLM_API_KEY",
|
|
167
169
|
api: "openai-completions",
|
|
168
170
|
models: [
|
|
169
171
|
{
|
|
@@ -230,6 +232,9 @@ models: [{
|
|
|
230
232
|
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` instead for local Qwen-compatible servers that read `chat_template_kwargs.enable_thinking`.
|
|
231
233
|
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 text content.
|
|
232
234
|
|
|
235
|
+
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.
|
|
236
|
+
|
|
237
|
+
|
|
233
238
|
> Migration note: Mistral moved from `openai-completions` to `mistral-conversations`.
|
|
234
239
|
> Use `mistral-conversations` for native Mistral models.
|
|
235
240
|
> If you intentionally route Mistral-compatible/custom endpoints through `openai-completions`, set `compat` flags explicitly as needed.
|
|
@@ -241,7 +246,7 @@ If your provider expects `Authorization: Bearer <key>` but doesn't use a standar
|
|
|
241
246
|
```typescript
|
|
242
247
|
pi.registerProvider("custom-api", {
|
|
243
248
|
baseUrl: "https://api.example.com",
|
|
244
|
-
apiKey: "MY_API_KEY",
|
|
249
|
+
apiKey: "$MY_API_KEY",
|
|
245
250
|
authHeader: true, // adds Authorization: Bearer header
|
|
246
251
|
api: "openai-completions",
|
|
247
252
|
models: [...]
|
|
@@ -568,7 +573,7 @@ Register your stream function:
|
|
|
568
573
|
```typescript
|
|
569
574
|
pi.registerProvider("my-provider", {
|
|
570
575
|
baseUrl: "https://api.example.com",
|
|
571
|
-
apiKey: "MY_API_KEY",
|
|
576
|
+
apiKey: "$MY_API_KEY",
|
|
572
577
|
api: "my-custom-api",
|
|
573
578
|
models: [...],
|
|
574
579
|
streamSimple: streamMyProvider
|
|
@@ -605,7 +610,7 @@ interface ProviderConfig {
|
|
|
605
610
|
/** API endpoint URL. Required when defining models. */
|
|
606
611
|
baseUrl?: string;
|
|
607
612
|
|
|
608
|
-
/** API key or
|
|
613
|
+
/** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or !command. Required when defining models (unless oauth). */
|
|
609
614
|
apiKey?: string;
|
|
610
615
|
|
|
611
616
|
/** API type for streaming. Required at provider or model level when defining models. */
|
|
@@ -618,7 +623,7 @@ interface ProviderConfig {
|
|
|
618
623
|
options?: SimpleStreamOptions
|
|
619
624
|
) => AssistantMessageEventStream;
|
|
620
625
|
|
|
621
|
-
/** Custom headers to include in requests. Values
|
|
626
|
+
/** Custom headers to include in requests. Values use the same resolution syntax as apiKey. */
|
|
622
627
|
headers?: Record<string, string>;
|
|
623
628
|
|
|
624
629
|
/** If true, adds Authorization: Bearer header with the resolved API key. */
|
|
@@ -693,6 +698,14 @@ interface ProviderModelConfig {
|
|
|
693
698
|
requiresReasoningContentOnAssistantMessages?: boolean;
|
|
694
699
|
thinkingFormat?: "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template";
|
|
695
700
|
cacheControlFormat?: "anthropic";
|
|
701
|
+
// anthropic-messages
|
|
702
|
+
supportsEagerToolInputStreaming?: boolean;
|
|
703
|
+
supportsLongCacheRetention?: boolean;
|
|
704
|
+
sendSessionAffinityHeaders?: boolean;
|
|
705
|
+
supportsCacheControlOnTools?: boolean;
|
|
706
|
+
forceAdaptiveThinking?: boolean;
|
|
707
|
+
allowEmptySignature?: boolean;
|
|
708
|
+
|
|
696
709
|
};
|
|
697
710
|
}
|
|
698
711
|
```
|
package/docs/extensions.md
CHANGED
|
@@ -199,7 +199,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
199
199
|
|
|
200
200
|
pi.registerProvider("local-openai", {
|
|
201
201
|
baseUrl: "http://localhost:1234/v1",
|
|
202
|
-
apiKey: "LOCAL_OPENAI_API_KEY",
|
|
202
|
+
apiKey: "$LOCAL_OPENAI_API_KEY",
|
|
203
203
|
api: "openai-completions",
|
|
204
204
|
models: payload.data.map((model) => ({
|
|
205
205
|
id: model.id,
|
|
@@ -1555,7 +1555,7 @@ If you need to discover models from a remote endpoint, prefer an async extension
|
|
|
1555
1555
|
pi.registerProvider("my-proxy", {
|
|
1556
1556
|
name: "My Proxy",
|
|
1557
1557
|
baseUrl: "https://proxy.example.com",
|
|
1558
|
-
apiKey: "PROXY_API_KEY", // env var
|
|
1558
|
+
apiKey: "$PROXY_API_KEY", // env var reference
|
|
1559
1559
|
api: "anthropic-messages",
|
|
1560
1560
|
models: [
|
|
1561
1561
|
{
|
|
@@ -1602,7 +1602,7 @@ pi.registerProvider("corporate-ai", {
|
|
|
1602
1602
|
**Config options:**
|
|
1603
1603
|
- `name` - Display name for the provider in UI such as `/login`.
|
|
1604
1604
|
- `baseUrl` - API endpoint URL. Required when defining models.
|
|
1605
|
-
- `apiKey` - API key
|
|
1605
|
+
- `apiKey` - API key literal, environment interpolation (`$ENV_VAR` or `${ENV_VAR}`), or leading `!command`. Required when defining models (unless `oauth` provided). `$$` escapes `$`, and `$!` escapes a literal `!` without triggering command execution.
|
|
1606
1606
|
- `api` - API type: `"anthropic-messages"`, `"openai-completions"`, `"openai-responses"`, etc.
|
|
1607
1607
|
- `headers` - Custom headers to include in requests.
|
|
1608
1608
|
- `authHeader` - If true, adds `Authorization: Bearer` header automatically.
|
|
@@ -2557,6 +2557,7 @@ All examples in [examples/extensions/](../examples/extensions/).
|
|
|
2557
2557
|
| `custom-compaction.ts` | Custom compaction summary | `on("session_before_compact")` |
|
|
2558
2558
|
| `trigger-compact.ts` | Trigger compaction manually | `compact()` |
|
|
2559
2559
|
| `git-checkpoint.ts` | Git stash on turns | `on("turn_start")`, `on("session_before_fork")`, `exec` |
|
|
2560
|
+
| `git-merge-and-resolve.ts` | Fetch, merge, and resolve conflicts | `on("agent_end")`, `exec`, `sendUserMessage` |
|
|
2560
2561
|
| `auto-commit-on-exit.ts` | Commit on shutdown | `on("session_shutdown")`, `exec` |
|
|
2561
2562
|
| **UI Components** |||
|
|
2562
2563
|
| `status-line.ts` | Footer status indicator | `setStatus`, session events |
|
package/docs/models.md
CHANGED
|
@@ -101,7 +101,7 @@ Use `google-generative-ai` with a `baseUrl` to add models from Google AI Studio,
|
|
|
101
101
|
"my-google": {
|
|
102
102
|
"baseUrl": "https://generativelanguage.googleapis.com/v1beta",
|
|
103
103
|
"api": "google-generative-ai",
|
|
104
|
-
"apiKey": "GEMINI_API_KEY",
|
|
104
|
+
"apiKey": "$GEMINI_API_KEY",
|
|
105
105
|
"models": [
|
|
106
106
|
{
|
|
107
107
|
"id": "gemma-4-31b-it",
|
|
@@ -143,22 +143,31 @@ Set `api` at provider level (default for all models) or model level (override pe
|
|
|
143
143
|
|
|
144
144
|
### Value Resolution
|
|
145
145
|
|
|
146
|
-
The `apiKey` and `headers` fields support
|
|
146
|
+
The `apiKey` and `headers` fields support command execution, environment interpolation, and literals:
|
|
147
147
|
|
|
148
|
-
- **Shell command:** `"!command"` executes and uses stdout
|
|
148
|
+
- **Shell command:** `"!command"` at the start executes the whole value as a command and uses stdout
|
|
149
149
|
```json
|
|
150
150
|
"apiKey": "!security find-generic-password -ws 'anthropic'"
|
|
151
151
|
"apiKey": "!op read 'op://vault/item/credential'"
|
|
152
152
|
```
|
|
153
|
-
- **Environment
|
|
153
|
+
- **Environment interpolation:** `"$ENV_VAR"` or `"${ENV_VAR}"` uses the value of the named variable. Interpolation works inside larger literals.
|
|
154
154
|
```json
|
|
155
|
-
"apiKey": "MY_API_KEY"
|
|
155
|
+
"apiKey": "$MY_API_KEY"
|
|
156
|
+
"apiKey": "${KEY_PREFIX}_${KEY_SUFFIX}"
|
|
157
|
+
```
|
|
158
|
+
`$FOO_BAR` is the variable `FOO_BAR`; use `${FOO}_BAR` when `BAR` is literal text. Missing environment variables make the value unresolved.
|
|
159
|
+
- **Escapes:** `"$$"` emits a literal `"$"`; `"$!"` emits a literal `"!"` without triggering command execution.
|
|
160
|
+
```json
|
|
161
|
+
"apiKey": "$$literal-dollar-prefix"
|
|
162
|
+
"apiKey": "$!literal-bang-prefix"
|
|
156
163
|
```
|
|
157
164
|
- **Literal value:** Used directly
|
|
158
165
|
```json
|
|
159
166
|
"apiKey": "sk-..."
|
|
160
167
|
```
|
|
161
168
|
|
|
169
|
+
Legacy uppercase env-var-like values such as `MY_API_KEY` are migrated to `$MY_API_KEY` on startup.
|
|
170
|
+
|
|
162
171
|
For `models.json`, shell commands are resolved at request time. pi intentionally does not apply built-in TTL, stale reuse, or recovery logic for arbitrary commands. Different commands need different caching and failure strategies, and pi cannot infer the right one.
|
|
163
172
|
|
|
164
173
|
If your command is slow, expensive, rate-limited, or should keep using a previous value on transient failures, wrap it in your own script or command that implements the caching or TTL behavior you want.
|
|
@@ -172,10 +181,10 @@ If your command is slow, expensive, rate-limited, or should keep using a previou
|
|
|
172
181
|
"providers": {
|
|
173
182
|
"custom-proxy": {
|
|
174
183
|
"baseUrl": "https://proxy.example.com/v1",
|
|
175
|
-
"apiKey": "MY_API_KEY",
|
|
184
|
+
"apiKey": "$MY_API_KEY",
|
|
176
185
|
"api": "anthropic-messages",
|
|
177
186
|
"headers": {
|
|
178
|
-
"x-portkey-api-key": "PORTKEY_API_KEY",
|
|
187
|
+
"x-portkey-api-key": "$PORTKEY_API_KEY",
|
|
179
188
|
"x-secret": "!op read 'op://vault/item/secret'"
|
|
180
189
|
},
|
|
181
190
|
"models": [...]
|
|
@@ -268,7 +277,7 @@ To merge custom models into a built-in provider, include the `models` array:
|
|
|
268
277
|
"providers": {
|
|
269
278
|
"anthropic": {
|
|
270
279
|
"baseUrl": "https://my-proxy.example.com/v1",
|
|
271
|
-
"apiKey": "ANTHROPIC_API_KEY",
|
|
280
|
+
"apiKey": "$ANTHROPIC_API_KEY",
|
|
272
281
|
"api": "anthropic-messages",
|
|
273
282
|
"models": [...]
|
|
274
283
|
}
|
|
@@ -319,16 +328,24 @@ For providers or proxies using `api: "anthropic-messages"`, use `compat.supports
|
|
|
319
328
|
|
|
320
329
|
By default pi sends per-tool `eager_input_streaming: true`. If a proxy or Anthropic-compatible backend rejects that field, set `supportsEagerToolInputStreaming` to `false`. Pi will omit `tools[].eager_input_streaming` and send the legacy `fine-grained-tool-streaming-2025-05-14` beta header for tool-enabled requests instead.
|
|
321
330
|
|
|
331
|
+
Some Anthropic models require adaptive thinking (`thinking.type: "adaptive"` plus `output_config.effort`) instead of the legacy budget-based thinking payload. Built-in models set this automatically. For custom providers or aliases that route to those models, set `forceAdaptiveThinking` to `true`.
|
|
332
|
+
|
|
333
|
+
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.
|
|
334
|
+
|
|
335
|
+
|
|
322
336
|
```json
|
|
323
337
|
{
|
|
324
338
|
"providers": {
|
|
325
339
|
"anthropic-proxy": {
|
|
326
340
|
"baseUrl": "https://proxy.example.com",
|
|
327
341
|
"api": "anthropic-messages",
|
|
328
|
-
"apiKey": "ANTHROPIC_PROXY_KEY",
|
|
342
|
+
"apiKey": "$ANTHROPIC_PROXY_KEY",
|
|
329
343
|
"compat": {
|
|
330
344
|
"supportsEagerToolInputStreaming": false,
|
|
331
|
-
"supportsLongCacheRetention": true
|
|
345
|
+
"supportsLongCacheRetention": true,
|
|
346
|
+
"forceAdaptiveThinking": true,
|
|
347
|
+
"allowEmptySignature": true
|
|
348
|
+
|
|
332
349
|
},
|
|
333
350
|
"models": [
|
|
334
351
|
{
|
|
@@ -346,6 +363,11 @@ By default pi sends per-tool `eager_input_streaming: true`. If a proxy or Anthro
|
|
|
346
363
|
|-------|-------------|
|
|
347
364
|
| `supportsEagerToolInputStreaming` | Whether the provider accepts per-tool `eager_input_streaming`. Default: `true`. Set to `false` to omit that field and use the legacy fine-grained tool streaming beta header on tool-enabled requests. |
|
|
348
365
|
| `supportsLongCacheRetention` | Whether the provider accepts Anthropic long cache retention (`cache_control.ttl: "1h"`) when cache retention is `long`. Default: `true`. |
|
|
366
|
+
| `sendSessionAffinityHeaders` | Whether to send `x-session-affinity` from the session id when caching is enabled. Default: auto-detected for known providers. |
|
|
367
|
+
| `supportsCacheControlOnTools` | Whether the provider accepts Anthropic-style `cache_control` markers on tool definitions. Default: `true`. |
|
|
368
|
+
| `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`. |
|
|
369
|
+
| `allowEmptySignature` | Whether to replay empty thinking signatures as `signature: ""` instead of converting thinking to text. Default: `false`. |
|
|
370
|
+
|
|
349
371
|
|
|
350
372
|
## OpenAI Compatibility
|
|
351
373
|
|
|
@@ -399,7 +421,7 @@ Example:
|
|
|
399
421
|
"providers": {
|
|
400
422
|
"openrouter": {
|
|
401
423
|
"baseUrl": "https://openrouter.ai/api/v1",
|
|
402
|
-
"apiKey": "OPENROUTER_API_KEY",
|
|
424
|
+
"apiKey": "$OPENROUTER_API_KEY",
|
|
403
425
|
"api": "openai-completions",
|
|
404
426
|
"models": [
|
|
405
427
|
{
|
|
@@ -449,7 +471,7 @@ Vercel AI Gateway example:
|
|
|
449
471
|
"providers": {
|
|
450
472
|
"vercel-ai-gateway": {
|
|
451
473
|
"baseUrl": "https://ai-gateway.vercel.sh/v1",
|
|
452
|
-
"apiKey": "AI_GATEWAY_API_KEY",
|
|
474
|
+
"apiKey": "$AI_GATEWAY_API_KEY",
|
|
453
475
|
"api": "openai-completions",
|
|
454
476
|
"models": [
|
|
455
477
|
{
|
package/docs/packages.md
CHANGED
|
@@ -28,8 +28,8 @@ pi install ./relative/path/to/package
|
|
|
28
28
|
|
|
29
29
|
pi remove npm:@foo/bar
|
|
30
30
|
pi list # show installed packages from settings
|
|
31
|
-
pi update # update pi and
|
|
32
|
-
pi update --extensions # update
|
|
31
|
+
pi update # update pi, update packages, and reconcile pinned git refs
|
|
32
|
+
pi update --extensions # update packages and reconcile pinned git refs only
|
|
33
33
|
pi update --self # update pi only
|
|
34
34
|
pi update --self --force # reinstall pi even if current
|
|
35
35
|
pi update npm:@foo/bar # update one package
|
|
@@ -85,9 +85,10 @@ ssh://git@github.com/user/repo@v1
|
|
|
85
85
|
- HTTPS and SSH URLs are both supported.
|
|
86
86
|
- SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`).
|
|
87
87
|
- For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast.
|
|
88
|
-
- Refs are pinned tags or commits
|
|
88
|
+
- Refs are pinned tags or commits. `pi update` and `pi update --extensions` do not move them to newer refs, but they do reconcile an existing clone to the configured ref.
|
|
89
|
+
- Use `pi install git:host/user/repo@new-ref` to update settings and move an existing package to a new pinned ref.
|
|
89
90
|
- Cloned to `~/.pi/agent/git/<host>/<path>` (global) or `.pi/git/<host>/<path>` (project).
|
|
90
|
-
-
|
|
91
|
+
- When reconciliation changes the checkout, pi resets and cleans the clone, then runs `npm install` if `package.json` exists.
|
|
91
92
|
|
|
92
93
|
**SSH examples:**
|
|
93
94
|
```bash
|
package/docs/providers.md
CHANGED
|
@@ -101,23 +101,31 @@ The file is created with `0600` permissions (user read/write only). Auth file cr
|
|
|
101
101
|
|
|
102
102
|
### Key Resolution
|
|
103
103
|
|
|
104
|
-
The `key` field supports
|
|
104
|
+
The `key` field supports command execution, environment interpolation, and literals:
|
|
105
105
|
|
|
106
|
-
- **Shell command:** `"!command"` executes and uses stdout (cached for process lifetime)
|
|
106
|
+
- **Shell command:** `"!command"` at the start executes the whole value as a command and uses stdout (cached for process lifetime)
|
|
107
107
|
```json
|
|
108
108
|
{ "type": "api_key", "key": "!security find-generic-password -ws 'anthropic'" }
|
|
109
109
|
{ "type": "api_key", "key": "!op read 'op://vault/item/credential'" }
|
|
110
110
|
```
|
|
111
|
-
- **Environment
|
|
111
|
+
- **Environment interpolation:** `"$ENV_VAR"` or `"${ENV_VAR}"` uses the value of the named variable. Interpolation works inside larger literals.
|
|
112
112
|
```json
|
|
113
|
-
{ "type": "api_key", "key": "MY_ANTHROPIC_KEY" }
|
|
113
|
+
{ "type": "api_key", "key": "$MY_ANTHROPIC_KEY" }
|
|
114
|
+
{ "type": "api_key", "key": "${KEY_PREFIX}_${KEY_SUFFIX}" }
|
|
115
|
+
```
|
|
116
|
+
`$FOO_BAR` is the variable `FOO_BAR`; use `${FOO}_BAR` when `BAR` is literal text. Missing environment variables make the value unresolved.
|
|
117
|
+
- **Escapes:** `"$$"` emits a literal `"$"`; `"$!"` emits a literal `"!"` without triggering command execution.
|
|
118
|
+
```json
|
|
119
|
+
{ "type": "api_key", "key": "$$literal-dollar-prefix" }
|
|
120
|
+
{ "type": "api_key", "key": "$!literal-bang-prefix" }
|
|
114
121
|
```
|
|
115
122
|
- **Literal value:** Used directly
|
|
116
123
|
```json
|
|
117
124
|
{ "type": "api_key", "key": "sk-ant-..." }
|
|
125
|
+
{ "type": "api_key", "key": "public" }
|
|
118
126
|
```
|
|
119
127
|
|
|
120
|
-
OAuth credentials are also stored here after `/login` and managed automatically.
|
|
128
|
+
Legacy uppercase env-var-like values such as `MY_API_KEY` are migrated to `$MY_API_KEY` on startup. OAuth credentials are also stored here after `/login` and managed automatically.
|
|
121
129
|
|
|
122
130
|
## Cloud Providers
|
|
123
131
|
|