@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +10 -29
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +1 -4
- package/src/config/settings-schema.ts +71 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/renderer.ts +7 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +2 -2
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +89 -223
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +9 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +5 -6
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/main.ts +11 -2
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +79 -45
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +13 -4
- package/src/modes/controllers/event-controller.ts +36 -7
- package/src/modes/controllers/input-controller.ts +13 -0
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +624 -52
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +14 -87
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +14 -3
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +10 -3
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +102 -114
- package/src/prompts/tools/read.md +1 -0
- package/src/prompts/tools/resolve.md +6 -5
- package/src/sdk.ts +12 -5
- package/src/session/agent-session.ts +428 -106
- package/src/session/blob-store.ts +36 -3
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/builtin-registry.ts +18 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +17 -7
- package/src/task/index.ts +3 -0
- package/src/task/render.ts +21 -15
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +27 -4
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +15 -4
- package/src/tools/eval.ts +3 -1
- package/src/tools/find.ts +20 -38
- package/src/tools/gh.ts +7 -6
- package/src/tools/index.ts +22 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +516 -233
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +44 -9
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import { ImageFormat, PhotonImage, SamplingFilter } from "@oh-my-pi/pi-natives";
|
|
3
2
|
|
|
4
3
|
export interface ImageResizeOptions {
|
|
5
|
-
maxWidth?: number;
|
|
6
|
-
maxHeight?: number;
|
|
7
|
-
maxBytes?: number;
|
|
8
|
-
jpegQuality?: number;
|
|
4
|
+
maxWidth?: number;
|
|
5
|
+
maxHeight?: number;
|
|
6
|
+
maxBytes?: number;
|
|
7
|
+
jpegQuality?: number;
|
|
9
8
|
}
|
|
10
9
|
|
|
11
10
|
export interface ResizedImage {
|
|
@@ -24,12 +23,12 @@ export interface ResizedImage {
|
|
|
24
23
|
const DEFAULT_MAX_BYTES = 500 * 1024;
|
|
25
24
|
|
|
26
25
|
const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// Anthropic's "internal recommended size" — Claude internally caps images at
|
|
27
|
+
// 1568px on the longest edge before vision processing.
|
|
29
28
|
maxWidth: 1568,
|
|
30
29
|
maxHeight: 1568,
|
|
31
30
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
32
|
-
jpegQuality:
|
|
31
|
+
jpegQuality: 80,
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
/** Pick the smallest of N encoded buffers. */
|
|
@@ -48,43 +47,34 @@ Buffer.prototype.toBase64 = function (this: Buffer) {
|
|
|
48
47
|
/**
|
|
49
48
|
* Resize and recompress an image to fit within the specified max dimensions and file size.
|
|
50
49
|
*
|
|
51
|
-
* Defaults target Anthropic's internal 1568px downscale threshold and produce small
|
|
52
|
-
* lossy JPEG output suitable for tool-call payloads (~100–500KB typical).
|
|
53
|
-
*
|
|
54
50
|
* Strategy:
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* 3. If still too large, JPEG-only quality ladder (PNG quality is a no-op).
|
|
61
|
-
* 4. If still too large, progressively reduce dimensions and retry the JPEG ladder.
|
|
62
|
-
* 5. Last resort: ship the smallest variant produced.
|
|
51
|
+
* 1. Probe metadata. If already within all limits, return original.
|
|
52
|
+
* 2. Resize to fit max dimensions and encode at high quality across PNG/JPEG/WebP — return smallest.
|
|
53
|
+
* 3. If still too large, walk a lossy JPEG/WebP quality ladder.
|
|
54
|
+
* 4. If still too large, walk a dimension-scale ladder × quality ladder.
|
|
55
|
+
* 5. If still too large, return the smallest variant produced.
|
|
63
56
|
*
|
|
64
|
-
*
|
|
57
|
+
* Backed by `Bun.Image`: a chainable native pipeline that runs decode/transform/encode
|
|
58
|
+
* off the JS thread when the terminal (`.bytes()`) is awaited.
|
|
65
59
|
*/
|
|
66
60
|
export async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {
|
|
67
61
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
68
62
|
const inputBuffer = Buffer.from(img.data, "base64");
|
|
69
63
|
|
|
70
64
|
try {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
const originalWidth = image.width;
|
|
74
|
-
const originalHeight = image.height;
|
|
75
|
-
const format = img.mimeType?.split("/")[1] ?? "png";
|
|
65
|
+
const { width: originalWidth, height: originalHeight, format } = await new Bun.Image(inputBuffer).metadata();
|
|
66
|
+
const sourceMime = img.mimeType ?? `image/${format}`;
|
|
76
67
|
|
|
77
|
-
//
|
|
78
|
-
const originalSize = inputBuffer.length;
|
|
79
|
-
// Fast path: skip if already within dimensions AND well under budget.
|
|
68
|
+
// Fast path: already within dimensions AND well under budget.
|
|
80
69
|
// Threshold is 1/4 of budget — if already that compact, don't re-encode.
|
|
81
70
|
// Avoids wasted work on tiny icons/diagrams while ensuring larger PNGs
|
|
82
71
|
// still get JPEG-compressed.
|
|
72
|
+
const originalSize = inputBuffer.length;
|
|
83
73
|
const comfortableSize = opts.maxBytes / 4;
|
|
84
74
|
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= comfortableSize) {
|
|
85
75
|
return {
|
|
86
76
|
buffer: inputBuffer,
|
|
87
|
-
mimeType:
|
|
77
|
+
mimeType: sourceMime,
|
|
88
78
|
originalWidth,
|
|
89
79
|
originalHeight,
|
|
90
80
|
width: originalWidth,
|
|
@@ -117,14 +107,11 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
117
107
|
height: number,
|
|
118
108
|
quality: number,
|
|
119
109
|
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
|
120
|
-
const resized = await image.resize(width, height, SamplingFilter.Lanczos3);
|
|
121
|
-
|
|
122
110
|
const [pngBuffer, jpegBuffer, webpBuffer] = await Promise.all([
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
111
|
+
new Bun.Image(inputBuffer).resize(width, height).png().bytes(),
|
|
112
|
+
new Bun.Image(inputBuffer).resize(width, height).jpeg({ quality }).bytes(),
|
|
113
|
+
new Bun.Image(inputBuffer).resize(width, height).webp({ quality }).bytes(),
|
|
126
114
|
]);
|
|
127
|
-
|
|
128
115
|
return pickSmallest(
|
|
129
116
|
{ buffer: pngBuffer, mimeType: "image/png" },
|
|
130
117
|
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
@@ -140,10 +127,9 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
140
127
|
height: number,
|
|
141
128
|
quality: number,
|
|
142
129
|
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
|
143
|
-
const resized = await image.resize(width, height, SamplingFilter.Lanczos3);
|
|
144
130
|
const [jpegBuffer, webpBuffer] = await Promise.all([
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
new Bun.Image(inputBuffer).resize(width, height).jpeg({ quality }).bytes(),
|
|
132
|
+
new Bun.Image(inputBuffer).resize(width, height).webp({ quality }).bytes(),
|
|
147
133
|
]);
|
|
148
134
|
return pickSmallest(
|
|
149
135
|
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
@@ -159,7 +145,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
159
145
|
let finalWidth = targetWidth;
|
|
160
146
|
let finalHeight = targetHeight;
|
|
161
147
|
|
|
162
|
-
// First attempt: resize to target, try
|
|
148
|
+
// First attempt: resize to target, try PNG/JPEG/WebP, pick smallest
|
|
163
149
|
best = await encodeSmallest(targetWidth, targetHeight, opts.jpegQuality);
|
|
164
150
|
|
|
165
151
|
if (best.buffer.length <= opts.maxBytes) {
|
|
@@ -264,9 +250,12 @@ export function formatDimensionNote(result: ResizedImage): string | undefined {
|
|
|
264
250
|
if (!result.wasResized) {
|
|
265
251
|
return undefined;
|
|
266
252
|
}
|
|
267
|
-
|
|
253
|
+
if (!result.originalWidth || !result.originalHeight || !result.width || !result.height) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
if (result.width === result.originalWidth && result.height === result.originalHeight) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
268
259
|
const scale = result.originalWidth / result.width;
|
|
269
|
-
return `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${
|
|
270
|
-
result.height
|
|
271
|
-
}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;
|
|
260
|
+
return `[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.]`;
|
|
272
261
|
}
|
package/src/vim/parser.ts
CHANGED
|
@@ -124,10 +124,6 @@ export function parseKeySequences(sequences: string[]): VimKeyToken[] {
|
|
|
124
124
|
return tokens;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
export function tokensToReplay(tokens: readonly VimKeyToken[]): string[] {
|
|
128
|
-
return tokens.map(token => token.value);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
127
|
export function replayTokens(values: readonly string[]): VimKeyToken[] {
|
|
132
128
|
return values.map((value, index) => ({
|
|
133
129
|
value,
|
|
@@ -136,16 +132,3 @@ export function replayTokens(values: readonly string[]): VimKeyToken[] {
|
|
|
136
132
|
offset: index,
|
|
137
133
|
}));
|
|
138
134
|
}
|
|
139
|
-
|
|
140
|
-
export function formatVimError(error: unknown): string {
|
|
141
|
-
if (!(error instanceof VimError)) {
|
|
142
|
-
return error instanceof Error ? error.message : String(error);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const base = error.message;
|
|
146
|
-
if (!error.location) {
|
|
147
|
-
return base;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return `${base} (sequence ${error.location.sequenceIndex + 1}, token ${error.location.offset + 1})`;
|
|
151
|
-
}
|
package/src/vim/render.ts
CHANGED
package/src/vim/types.ts
CHANGED
|
@@ -164,7 +164,7 @@ export function clonePosition(position: Position): Position {
|
|
|
164
164
|
return { line: position.line, col: position.col };
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
function comparePositions(left: Position, right: Position): number {
|
|
168
168
|
if (left.line !== right.line) {
|
|
169
169
|
return left.line - right.line;
|
|
170
170
|
}
|
|
@@ -5,15 +5,11 @@
|
|
|
5
5
|
* Requires OAuth credentials stored in agent.db for provider "google-gemini-cli" or "google-antigravity".
|
|
6
6
|
* Returns synthesized answers with citations and source metadata from grounding chunks.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
ANTIGRAVITY_SYSTEM_INSTRUCTION,
|
|
10
|
-
extractRetryDelay,
|
|
11
|
-
getAntigravityUserAgent,
|
|
12
|
-
getGeminiCliHeaders,
|
|
13
|
-
} from "@oh-my-pi/pi-ai";
|
|
8
|
+
import { ANTIGRAVITY_SYSTEM_INSTRUCTION, getAntigravityUserAgent, getGeminiCliHeaders } from "@oh-my-pi/pi-ai";
|
|
14
9
|
import { refreshAntigravityToken } from "@oh-my-pi/pi-ai/utils/oauth/google-antigravity";
|
|
15
10
|
import { refreshGoogleCloudToken } from "@oh-my-pi/pi-ai/utils/oauth/google-gemini-cli";
|
|
16
|
-
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { fetchWithRetry, getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
12
|
+
|
|
17
13
|
import { AgentStorage } from "../../../session/agent-storage";
|
|
18
14
|
import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types";
|
|
19
15
|
import { SearchProviderError } from "../../../web/search/types";
|
|
@@ -303,98 +299,42 @@ async function callGeminiSearch(
|
|
|
303
299
|
}
|
|
304
300
|
(requestBody.request as Record<string, unknown>).generationConfig = generationConfig;
|
|
305
301
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
} catch (error) {
|
|
326
|
-
if (attempt < MAX_RETRIES) {
|
|
327
|
-
await Bun.sleep(BASE_DELAY_MS * 2 ** attempt);
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (auth.isAntigravity && endpointIndex < endpoints.length - 1) {
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
throw error;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (response.ok) {
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const errorText = await response.text();
|
|
343
|
-
const canRefreshAuth =
|
|
344
|
-
response.status === 401 ||
|
|
345
|
-
response.status === 403 ||
|
|
346
|
-
(response.status === 400 &&
|
|
347
|
-
/api key not valid|invalid credentials|invalid authentication/i.test(errorText));
|
|
348
|
-
if (canRefreshAuth && attempt === 0 && (await refreshGeminiAuth(auth))) {
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
const isRetryableStatus =
|
|
352
|
-
response.status === 429 ||
|
|
353
|
-
response.status === 500 ||
|
|
354
|
-
response.status === 502 ||
|
|
355
|
-
response.status === 503 ||
|
|
356
|
-
response.status === 504;
|
|
357
|
-
|
|
358
|
-
if (isRetryableStatus && attempt < MAX_RETRIES) {
|
|
359
|
-
const serverDelay = extractRetryDelay(errorText, response);
|
|
360
|
-
if (response.status === 429) {
|
|
361
|
-
if (serverDelay && rateLimitTimeSpent + serverDelay <= RATE_LIMIT_BUDGET_MS) {
|
|
362
|
-
rateLimitTimeSpent += serverDelay;
|
|
363
|
-
await Bun.sleep(serverDelay);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (!serverDelay) {
|
|
367
|
-
await Bun.sleep(BASE_DELAY_MS * 2 ** attempt);
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
} else {
|
|
371
|
-
await Bun.sleep(serverDelay ?? BASE_DELAY_MS * 2 ** attempt);
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
lastError = new SearchProviderError(
|
|
377
|
-
"gemini",
|
|
378
|
-
`Gemini Cloud Code API error (${response.status}): ${errorText}`,
|
|
379
|
-
response.status,
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
if (auth.isAntigravity && isRetryableStatus && endpointIndex < endpoints.length - 1) {
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
throw lastError;
|
|
387
|
-
}
|
|
302
|
+
const buildInit = (): RequestInit => ({
|
|
303
|
+
method: "POST",
|
|
304
|
+
headers: {
|
|
305
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
306
|
+
"Content-Type": "application/json",
|
|
307
|
+
Accept: "text/event-stream",
|
|
308
|
+
...headers,
|
|
309
|
+
},
|
|
310
|
+
body: JSON.stringify(requestBody),
|
|
311
|
+
});
|
|
312
|
+
const urlFor = (attempt: number) =>
|
|
313
|
+
`${endpoints[Math.min(attempt, endpoints.length - 1)]}/v1internal:streamGenerateContent?alt=sse`;
|
|
314
|
+
|
|
315
|
+
let response = await fetchWithRetry(urlFor, {
|
|
316
|
+
...buildInit(),
|
|
317
|
+
maxAttempts: MAX_RETRIES + 1,
|
|
318
|
+
defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
|
|
319
|
+
maxDelayMs: RATE_LIMIT_BUDGET_MS,
|
|
320
|
+
});
|
|
388
321
|
|
|
389
|
-
|
|
390
|
-
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
const errorText = await response.clone().text();
|
|
324
|
+
const canRefreshAuth =
|
|
325
|
+
response.status === 401 ||
|
|
326
|
+
response.status === 403 ||
|
|
327
|
+
(response.status === 400 && /api key not valid|invalid credentials|invalid authentication/i.test(errorText));
|
|
328
|
+
if (canRefreshAuth && (await refreshGeminiAuth(auth))) {
|
|
329
|
+
response = await fetchWithRetry(urlFor, {
|
|
330
|
+
...buildInit(),
|
|
331
|
+
maxAttempts: MAX_RETRIES + 1,
|
|
332
|
+
defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
|
|
333
|
+
maxDelayMs: RATE_LIMIT_BUDGET_MS,
|
|
334
|
+
});
|
|
391
335
|
}
|
|
392
336
|
}
|
|
393
337
|
|
|
394
|
-
if (!response) {
|
|
395
|
-
throw new SearchProviderError("gemini", "Gemini API request failed", 500);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
338
|
if (!response.ok) {
|
|
399
339
|
const errorText = await response.text();
|
|
400
340
|
throw new SearchProviderError(
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
Submits a finalized implementation plan for user approval.
|
|
2
|
-
|
|
3
|
-
Write the plan to `local://PLAN.md` first, then call this with `title` (e.g. `WP_MIGRATION_PLAN`); on approval the file is renamed to `local://<title>.md` and full tool access is restored.
|
|
4
|
-
- Use only after planning implementation steps; not for pure research.
|
|
5
|
-
- NEVER call before the plan file exists.
|
|
6
|
-
- NEVER use `ask` to request plan approval — this tool does that.
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
|
-
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import { isEnoent, prompt } from "@oh-my-pi/pi-utils";
|
|
4
|
-
import { type Static, Type } from "@sinclair/typebox";
|
|
5
|
-
import exitPlanModeDescription from "../prompts/tools/exit-plan-mode.md" with { type: "text" };
|
|
6
|
-
import type { ToolSession } from ".";
|
|
7
|
-
import { resolvePlanPath } from "./plan-mode-guard";
|
|
8
|
-
import { ToolError } from "./tool-errors";
|
|
9
|
-
|
|
10
|
-
const exitPlanModeSchema = Type.Object({
|
|
11
|
-
title: Type.String({ description: "final plan title", examples: ["WP_MIGRATION_PLAN"] }),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
type ExitPlanModeParams = Static<typeof exitPlanModeSchema>;
|
|
15
|
-
|
|
16
|
-
function normalizePlanTitle(title: string): { title: string; fileName: string } {
|
|
17
|
-
const trimmed = title.trim();
|
|
18
|
-
if (!trimmed) {
|
|
19
|
-
throw new ToolError("Title is required and must not be empty.");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("..")) {
|
|
23
|
-
throw new ToolError("Title must not contain path separators or '..'.");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const withExtension = trimmed.toLowerCase().endsWith(".md") ? trimmed : `${trimmed}.md`;
|
|
27
|
-
if (!/^[A-Za-z0-9_-]+\.md$/.test(withExtension)) {
|
|
28
|
-
throw new ToolError("Title may only contain letters, numbers, underscores, or hyphens.");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const normalizedTitle = withExtension.slice(0, -3);
|
|
32
|
-
return { title: normalizedTitle, fileName: withExtension };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ExitPlanModeDetails {
|
|
36
|
-
planFilePath: string;
|
|
37
|
-
planExists: boolean;
|
|
38
|
-
title: string;
|
|
39
|
-
finalPlanFilePath: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, ExitPlanModeDetails> {
|
|
43
|
-
readonly name = "exit_plan_mode";
|
|
44
|
-
readonly label = "ExitPlanMode";
|
|
45
|
-
readonly description: string;
|
|
46
|
-
readonly parameters = exitPlanModeSchema;
|
|
47
|
-
readonly strict = true;
|
|
48
|
-
readonly concurrency = "exclusive";
|
|
49
|
-
readonly intent = (): string => "present plan";
|
|
50
|
-
|
|
51
|
-
constructor(private readonly session: ToolSession) {
|
|
52
|
-
this.description = prompt.render(exitPlanModeDescription);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async execute(
|
|
56
|
-
_toolCallId: string,
|
|
57
|
-
params: ExitPlanModeParams,
|
|
58
|
-
_signal?: AbortSignal,
|
|
59
|
-
_onUpdate?: AgentToolUpdateCallback<ExitPlanModeDetails>,
|
|
60
|
-
_context?: AgentToolContext,
|
|
61
|
-
): Promise<AgentToolResult<ExitPlanModeDetails>> {
|
|
62
|
-
const state = this.session.getPlanModeState?.();
|
|
63
|
-
if (!state?.enabled) {
|
|
64
|
-
throw new ToolError("Plan mode is not active.");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const normalized = normalizePlanTitle(params.title);
|
|
68
|
-
const finalPlanFilePath = `local://${normalized.fileName}`;
|
|
69
|
-
const resolvedPlanPath = resolvePlanPath(this.session, state.planFilePath);
|
|
70
|
-
resolvePlanPath(this.session, finalPlanFilePath);
|
|
71
|
-
let planExists = false;
|
|
72
|
-
try {
|
|
73
|
-
const stat = await fs.stat(resolvedPlanPath);
|
|
74
|
-
planExists = stat.isFile();
|
|
75
|
-
} catch (error) {
|
|
76
|
-
if (!isEnoent(error)) {
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!planExists) {
|
|
82
|
-
throw new ToolError(
|
|
83
|
-
`Plan file not found at ${state.planFilePath}. Write the finalized plan to ${state.planFilePath} before calling exit_plan_mode.`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
content: [{ type: "text", text: "Plan ready for approval." }],
|
|
89
|
-
details: {
|
|
90
|
-
planFilePath: state.planFilePath,
|
|
91
|
-
planExists,
|
|
92
|
-
title: normalized.title,
|
|
93
|
-
finalPlanFilePath,
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
package/src/utils/fuzzy.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// Fuzzy search. Matches if all query characters appear in order (not necessarily consecutive).
|
|
2
|
-
// Lower score = better match.
|
|
3
|
-
|
|
4
|
-
export interface FuzzyMatch {
|
|
5
|
-
matches: boolean;
|
|
6
|
-
score: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function fuzzyMatch(query: string, text: string): FuzzyMatch {
|
|
10
|
-
const queryLower = query.toLowerCase();
|
|
11
|
-
const textLower = text.toLowerCase();
|
|
12
|
-
|
|
13
|
-
if (queryLower.length === 0) {
|
|
14
|
-
return { matches: true, score: 0 };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (queryLower.length > textLower.length) {
|
|
18
|
-
return { matches: false, score: 0 };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let queryIndex = 0;
|
|
22
|
-
let score = 0;
|
|
23
|
-
let lastMatchIndex = -1;
|
|
24
|
-
let consecutiveMatches = 0;
|
|
25
|
-
|
|
26
|
-
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
|
|
27
|
-
if (textLower[i] === queryLower[queryIndex]) {
|
|
28
|
-
const isWordBoundary = i === 0 || /[\s\-_./]/.test(textLower[i - 1]!);
|
|
29
|
-
|
|
30
|
-
// Reward consecutive character matches (e.g., typing "foo" matches "foobar" better than "f_o_o")
|
|
31
|
-
if (lastMatchIndex === i - 1) {
|
|
32
|
-
consecutiveMatches++;
|
|
33
|
-
score -= consecutiveMatches * 5;
|
|
34
|
-
} else {
|
|
35
|
-
consecutiveMatches = 0;
|
|
36
|
-
// Penalize gaps between matched characters
|
|
37
|
-
if (lastMatchIndex >= 0) {
|
|
38
|
-
score += (i - lastMatchIndex - 1) * 2;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Reward matches at word boundaries (start of words are more likely intentional targets)
|
|
43
|
-
if (isWordBoundary) {
|
|
44
|
-
score -= 10;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Slight penalty for matches later in the string (prefer earlier matches)
|
|
48
|
-
score += i * 0.1;
|
|
49
|
-
|
|
50
|
-
lastMatchIndex = i;
|
|
51
|
-
queryIndex++;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Not all query characters were found in order
|
|
56
|
-
if (queryIndex < queryLower.length) {
|
|
57
|
-
return { matches: false, score: 0 };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return { matches: true, score };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Filter and sort items by fuzzy match quality (best matches first)
|
|
64
|
-
// Supports space-separated tokens: all tokens must match, sorted by match count then score
|
|
65
|
-
export function fuzzyFilter<T>(items: T[], query: string, getText: (item: T) => string): T[] {
|
|
66
|
-
if (!query.trim()) {
|
|
67
|
-
return items;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Split query into tokens
|
|
71
|
-
const tokens = query
|
|
72
|
-
.trim()
|
|
73
|
-
.split(/\s+/)
|
|
74
|
-
.filter(t => t.length > 0);
|
|
75
|
-
|
|
76
|
-
if (tokens.length === 0) {
|
|
77
|
-
return items;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const results: { item: T; totalScore: number }[] = [];
|
|
81
|
-
|
|
82
|
-
for (const item of items) {
|
|
83
|
-
const text = getText(item);
|
|
84
|
-
let totalScore = 0;
|
|
85
|
-
let allMatch = true;
|
|
86
|
-
|
|
87
|
-
// Check each token against the text - ALL must match
|
|
88
|
-
for (const token of tokens) {
|
|
89
|
-
const match = fuzzyMatch(token, text);
|
|
90
|
-
if (match.matches) {
|
|
91
|
-
totalScore += match.score;
|
|
92
|
-
} else {
|
|
93
|
-
allMatch = false;
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Only include if all tokens match
|
|
99
|
-
if (allMatch) {
|
|
100
|
-
results.push({ item, totalScore });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Sort by score (asc, lower is better)
|
|
105
|
-
results.sort((a, b) => a.totalScore - b.totalScore);
|
|
106
|
-
|
|
107
|
-
return results.map(r => r.item);
|
|
108
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { ImageFormat, PhotonImage } from "@oh-my-pi/pi-natives";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Convert image to PNG format for terminal display.
|
|
5
|
-
* Kitty graphics protocol requires PNG format (f=100).
|
|
6
|
-
*/
|
|
7
|
-
export async function convertToPng(
|
|
8
|
-
base64Data: string,
|
|
9
|
-
mimeType: string,
|
|
10
|
-
): Promise<{ data: string; mimeType: string } | null> {
|
|
11
|
-
// Already PNG, no conversion needed
|
|
12
|
-
if (mimeType === "image/png") {
|
|
13
|
-
return { data: base64Data, mimeType };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const image = await PhotonImage.parse(new Uint8Array(Buffer.from(base64Data, "base64")));
|
|
18
|
-
const pngBuffer = await image.encode(ImageFormat.PNG, 100);
|
|
19
|
-
return {
|
|
20
|
-
data: Buffer.from(pngBuffer).toBase64(),
|
|
21
|
-
mimeType: "image/png",
|
|
22
|
-
};
|
|
23
|
-
} catch {
|
|
24
|
-
// Conversion failed
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|