@oh-my-pi/pi-coding-agent 11.0.3 → 11.2.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 +199 -49
- package/README.md +1 -1
- package/docs/config-usage.md +3 -4
- package/docs/sdk.md +6 -5
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/README.md +1 -1
- package/package.json +19 -11
- package/src/cli/args.ts +11 -94
- package/src/cli/config-cli.ts +1 -1
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/oclif-help.ts +26 -0
- package/src/cli/web-search-cli.ts +148 -0
- package/src/cli.ts +8 -2
- package/src/commands/commit.ts +36 -0
- package/src/commands/config.ts +51 -0
- package/src/commands/grep.ts +41 -0
- package/src/commands/index/index.ts +136 -0
- package/src/commands/jupyter.ts +32 -0
- package/src/commands/plugin.ts +70 -0
- package/src/commands/setup.ts +39 -0
- package/src/commands/shell.ts +29 -0
- package/src/commands/stats.ts +29 -0
- package/src/commands/update.ts +21 -0
- package/src/commands/web-search.ts +50 -0
- package/src/commit/agentic/index.ts +3 -2
- package/src/commit/agentic/tools/analyze-file.ts +1 -3
- package/src/commit/git/errors.ts +4 -6
- package/src/commit/pipeline.ts +3 -2
- package/src/config/keybindings.ts +1 -3
- package/src/config/model-registry.ts +89 -162
- package/src/config/settings-schema.ts +10 -0
- package/src/config.ts +202 -132
- package/src/exa/mcp-client.ts +8 -41
- package/src/export/html/index.ts +1 -1
- package/src/extensibility/extensions/loader.ts +7 -10
- package/src/extensibility/extensions/runner.ts +5 -15
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/extensibility/hooks/runner.ts +6 -9
- package/src/index.ts +0 -1
- package/src/ipy/kernel.ts +10 -22
- package/src/lsp/clients/biome-client.ts +4 -7
- package/src/lsp/clients/lsp-linter-client.ts +4 -6
- package/src/lsp/index.ts +5 -4
- package/src/lsp/utils.ts +18 -0
- package/src/main.ts +86 -181
- package/src/mcp/json-rpc.ts +2 -2
- package/src/mcp/transports/http.ts +12 -49
- package/src/modes/components/armin.ts +1 -3
- package/src/modes/components/assistant-message.ts +4 -4
- package/src/modes/components/bash-execution.ts +5 -3
- package/src/modes/components/branch-summary-message.ts +1 -3
- package/src/modes/components/compaction-summary-message.ts +1 -3
- package/src/modes/components/custom-message.ts +4 -5
- package/src/modes/components/extensions/extension-dashboard.ts +10 -16
- package/src/modes/components/extensions/extension-list.ts +5 -5
- package/src/modes/components/footer.ts +1 -4
- package/src/modes/components/hook-editor.ts +7 -32
- package/src/modes/components/hook-message.ts +4 -5
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/plugin-settings.ts +16 -20
- package/src/modes/components/python-execution.ts +5 -5
- package/src/modes/components/session-selector.ts +6 -7
- package/src/modes/components/settings-defs.ts +49 -40
- package/src/modes/components/settings-selector.ts +8 -17
- package/src/modes/components/skill-message.ts +1 -3
- package/src/modes/components/status-line-segment-editor.ts +1 -3
- package/src/modes/components/status-line.ts +1 -3
- package/src/modes/components/todo-reminder.ts +5 -7
- package/src/modes/components/tree-selector.ts +10 -12
- package/src/modes/components/ttsr-notification.ts +1 -3
- package/src/modes/components/user-message-selector.ts +2 -4
- package/src/modes/components/welcome.ts +6 -18
- package/src/modes/controllers/event-controller.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +7 -34
- package/src/modes/controllers/selector-controller.ts +3 -3
- package/src/modes/interactive-mode.ts +27 -1
- package/src/modes/rpc/rpc-client.ts +2 -5
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/theme/theme.ts +2 -6
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +6 -1
- package/src/patch/index.ts +1 -4
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/frontmatter.md +2 -1
- package/src/prompts/agents/init.md +1 -0
- package/src/prompts/agents/plan.md +1 -0
- package/src/prompts/agents/reviewer.md +1 -0
- package/src/prompts/system/subagent-submit-reminder.md +2 -0
- package/src/prompts/system/subagent-system-prompt.md +2 -0
- package/src/prompts/system/subagent-user-prompt.md +8 -0
- package/src/prompts/system/system-prompt.md +5 -3
- package/src/prompts/system/web-search.md +6 -4
- package/src/prompts/tools/task.md +216 -163
- package/src/sdk.ts +11 -110
- package/src/session/agent-session.ts +117 -83
- package/src/session/auth-storage.ts +10 -51
- package/src/session/messages.ts +17 -3
- package/src/session/session-manager.ts +30 -30
- package/src/session/streaming-output.ts +1 -1
- package/src/ssh/ssh-executor.ts +6 -3
- package/src/task/agents.ts +2 -0
- package/src/task/discovery.ts +1 -1
- package/src/task/executor.ts +5 -10
- package/src/task/index.ts +43 -23
- package/src/task/render.ts +67 -64
- package/src/task/template.ts +17 -34
- package/src/task/types.ts +49 -22
- package/src/tools/ask.ts +1 -3
- package/src/tools/bash.ts +1 -4
- package/src/tools/browser.ts +5 -7
- package/src/tools/exit-plan-mode.ts +1 -4
- package/src/tools/fetch.ts +1 -3
- package/src/tools/find.ts +4 -3
- package/src/tools/gemini-image.ts +24 -55
- package/src/tools/grep.ts +4 -4
- package/src/tools/index.ts +12 -14
- package/src/tools/notebook.ts +1 -5
- package/src/tools/python.ts +4 -3
- package/src/tools/read.ts +2 -4
- package/src/tools/render-utils.ts +23 -0
- package/src/tools/ssh.ts +8 -12
- package/src/tools/todo-write.ts +1 -4
- package/src/tools/tool-errors.ts +1 -4
- package/src/tools/write.ts +1 -3
- package/src/utils/external-editor.ts +59 -0
- package/src/utils/file-mentions.ts +39 -1
- package/src/utils/image-convert.ts +1 -1
- package/src/utils/image-resize.ts +4 -4
- package/src/web/search/auth.ts +3 -33
- package/src/web/search/index.ts +73 -139
- package/src/web/search/provider.ts +58 -0
- package/src/web/search/providers/anthropic.ts +53 -14
- package/src/web/search/providers/base.ts +22 -0
- package/src/web/search/providers/codex.ts +38 -16
- package/src/web/search/providers/exa.ts +30 -6
- package/src/web/search/providers/gemini.ts +56 -20
- package/src/web/search/providers/jina.ts +28 -5
- package/src/web/search/providers/perplexity.ts +103 -36
- package/src/web/search/render.ts +84 -74
- package/src/web/search/types.ts +285 -59
- package/src/migrations.ts +0 -175
- package/src/session/storage-migration.ts +0 -173
|
@@ -21,10 +21,7 @@ export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, Ex
|
|
|
21
21
|
public readonly description: string;
|
|
22
22
|
public readonly parameters = exitPlanModeSchema;
|
|
23
23
|
|
|
24
|
-
private readonly session: ToolSession
|
|
25
|
-
|
|
26
|
-
constructor(session: ToolSession) {
|
|
27
|
-
this.session = session;
|
|
24
|
+
constructor(private readonly session: ToolSession) {
|
|
28
25
|
this.description = renderPromptTemplate(exitPlanModeDescription);
|
|
29
26
|
}
|
|
30
27
|
|
package/src/tools/fetch.ts
CHANGED
|
@@ -855,10 +855,8 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
|
|
|
855
855
|
public readonly label = "Fetch";
|
|
856
856
|
public readonly description: string;
|
|
857
857
|
public readonly parameters = fetchSchema;
|
|
858
|
-
private readonly session: ToolSession;
|
|
859
858
|
|
|
860
|
-
constructor(session: ToolSession) {
|
|
861
|
-
this.session = session;
|
|
859
|
+
constructor(private readonly session: ToolSession) {
|
|
862
860
|
this.description = renderPromptTemplate(fetchDescription);
|
|
863
861
|
}
|
|
864
862
|
|
package/src/tools/find.ts
CHANGED
|
@@ -118,11 +118,12 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
118
118
|
public readonly description: string;
|
|
119
119
|
public readonly parameters = findSchema;
|
|
120
120
|
|
|
121
|
-
private readonly session: ToolSession;
|
|
122
121
|
private readonly customOps?: FindOperations;
|
|
123
122
|
|
|
124
|
-
constructor(
|
|
125
|
-
|
|
123
|
+
constructor(
|
|
124
|
+
private readonly session: ToolSession,
|
|
125
|
+
options?: FindToolOptions,
|
|
126
|
+
) {
|
|
126
127
|
this.customOps = options?.operations;
|
|
127
128
|
this.description = renderPromptTemplate(findDescription);
|
|
128
129
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { getEnvApiKey, StringEnum } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import { $env, ptree, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { $env, ptree, readSseJson, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { type Static, Type } from "@sinclair/typebox";
|
|
6
6
|
import type { ModelRegistry } from "../config/model-registry";
|
|
7
7
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
@@ -316,8 +316,8 @@ async function loadImageFromUrl(imageUrl: string, signal?: AbortSignal): Promise
|
|
|
316
316
|
if (!contentType || !contentType.startsWith("image/")) {
|
|
317
317
|
throw new Error(`Unsupported image type from URL: ${imageUrl}`);
|
|
318
318
|
}
|
|
319
|
-
const buffer =
|
|
320
|
-
return { data: buffer.
|
|
319
|
+
const buffer = await response.bytes();
|
|
320
|
+
return { data: buffer.toBase64(), mimeType: contentType };
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
function collectOpenRouterResponseText(message: OpenRouterMessage | undefined): string | undefined {
|
|
@@ -450,8 +450,8 @@ async function loadImageFromPath(imagePath: string, cwd: string): Promise<Inline
|
|
|
450
450
|
throw new Error(`Unsupported image type: ${imagePath}`);
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
-
const buffer =
|
|
454
|
-
return { data: buffer.
|
|
453
|
+
const buffer = await file.bytes();
|
|
454
|
+
return { data: buffer.toBase64(), mimeType };
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
async function resolveInputImage(input: ImageInput, cwd: string): Promise<InlineImageData> {
|
|
@@ -585,68 +585,37 @@ interface AntigravitySseResult {
|
|
|
585
585
|
usage?: GeminiUsageMetadata;
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
const _prefix = Buffer.from("data: ", "utf-8");
|
|
589
|
+
|
|
588
590
|
async function parseAntigravitySseForImage(response: Response, signal?: AbortSignal): Promise<AntigravitySseResult> {
|
|
589
591
|
if (!response.body) {
|
|
590
592
|
throw new Error("No response body");
|
|
591
593
|
}
|
|
592
594
|
|
|
593
|
-
const reader = response.body.getReader();
|
|
594
|
-
const decoder = new TextDecoder();
|
|
595
|
-
let buffer = "";
|
|
596
595
|
const textParts: string[] = [];
|
|
597
596
|
const images: InlineImageData[] = [];
|
|
598
597
|
let usage: GeminiUsageMetadata | undefined;
|
|
599
598
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (!line.startsWith("data:")) continue;
|
|
615
|
-
const jsonStr = line.slice(5).trim();
|
|
616
|
-
if (!jsonStr) continue;
|
|
617
|
-
|
|
618
|
-
const parsed = Bun.JSONL.parseChunk(`${jsonStr}\n`);
|
|
619
|
-
if (parsed.error || parsed.values.length === 0) continue;
|
|
620
|
-
|
|
621
|
-
for (const value of parsed.values) {
|
|
622
|
-
const chunk = value as AntigravityResponseChunk;
|
|
623
|
-
const responseData = chunk.response;
|
|
624
|
-
if (!responseData?.candidates) continue;
|
|
625
|
-
|
|
626
|
-
if (responseData.usageMetadata) {
|
|
627
|
-
usage = responseData.usageMetadata;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
for (const candidate of responseData.candidates) {
|
|
631
|
-
const parts = candidate.content?.parts;
|
|
632
|
-
if (!parts) continue;
|
|
633
|
-
for (const part of parts) {
|
|
634
|
-
if (part.text) {
|
|
635
|
-
textParts.push(part.text);
|
|
636
|
-
}
|
|
637
|
-
if (part.inlineData?.data && part.inlineData?.mimeType) {
|
|
638
|
-
images.push({
|
|
639
|
-
data: part.inlineData.data,
|
|
640
|
-
mimeType: part.inlineData.mimeType,
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
599
|
+
for await (const chunk of readSseJson<AntigravityResponseChunk>(response.body, signal)) {
|
|
600
|
+
const responseData = chunk.response;
|
|
601
|
+
if (!responseData) continue;
|
|
602
|
+
if (!responseData.candidates) continue;
|
|
603
|
+
for (const candidate of responseData.candidates) {
|
|
604
|
+
const parts = candidate.content?.parts;
|
|
605
|
+
if (!parts) continue;
|
|
606
|
+
for (const part of parts) {
|
|
607
|
+
if (part.text) {
|
|
608
|
+
textParts.push(part.text);
|
|
609
|
+
}
|
|
610
|
+
const inlineData = part.inlineData;
|
|
611
|
+
if (inlineData?.data && inlineData.mimeType) {
|
|
612
|
+
images.push({ data: inlineData.data, mimeType: inlineData.mimeType });
|
|
645
613
|
}
|
|
646
614
|
}
|
|
647
615
|
}
|
|
648
|
-
|
|
649
|
-
|
|
616
|
+
if (responseData.usageMetadata) {
|
|
617
|
+
usage = responseData.usageMetadata;
|
|
618
|
+
}
|
|
650
619
|
}
|
|
651
620
|
|
|
652
621
|
return { images, text: textParts, usage };
|
package/src/tools/grep.ts
CHANGED
|
@@ -66,10 +66,10 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
66
66
|
public readonly description: string;
|
|
67
67
|
public readonly parameters = grepSchema;
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
constructor(
|
|
70
|
+
private readonly session: ToolSession,
|
|
71
|
+
_options?: GrepToolOptions,
|
|
72
|
+
) {
|
|
73
73
|
this.description = renderPromptTemplate(grepDescription);
|
|
74
74
|
}
|
|
75
75
|
|
package/src/tools/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { TaskTool } from "../task";
|
|
|
14
14
|
import type { AgentOutputManager } from "../task/output-manager";
|
|
15
15
|
import type { EventBus } from "../utils/event-bus";
|
|
16
16
|
import { time } from "../utils/timings";
|
|
17
|
-
import {
|
|
17
|
+
import { SearchTool } from "../web/search";
|
|
18
18
|
import { AskTool } from "./ask";
|
|
19
19
|
import { BashTool } from "./bash";
|
|
20
20
|
import { BrowserTool } from "./browser";
|
|
@@ -51,16 +51,14 @@ export {
|
|
|
51
51
|
export { EditTool, type EditToolDetails } from "../patch";
|
|
52
52
|
export { BUNDLED_AGENTS, TaskTool } from "../task";
|
|
53
53
|
export {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
type
|
|
61
|
-
|
|
62
|
-
WebSearchTool,
|
|
63
|
-
type WebSearchToolsOptions,
|
|
54
|
+
companySearchTools,
|
|
55
|
+
exaSearchTools,
|
|
56
|
+
getSearchTools,
|
|
57
|
+
type SearchProvider,
|
|
58
|
+
type SearchResponse,
|
|
59
|
+
SearchTool,
|
|
60
|
+
type SearchToolsOptions,
|
|
61
|
+
setPreferredSearchProvider,
|
|
64
62
|
webSearchCodeContextTool,
|
|
65
63
|
webSearchCompanyTool,
|
|
66
64
|
webSearchCrawlTool,
|
|
@@ -179,7 +177,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
179
177
|
task: TaskTool.create,
|
|
180
178
|
todo_write: s => new TodoWriteTool(s),
|
|
181
179
|
fetch: s => new FetchTool(s),
|
|
182
|
-
web_search: s => new
|
|
180
|
+
web_search: s => new SearchTool(s),
|
|
183
181
|
write: s => new WriteTool(s),
|
|
184
182
|
};
|
|
185
183
|
|
|
@@ -314,9 +312,9 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
314
312
|
const slowTools: Array<{ name: string; ms: number }> = [];
|
|
315
313
|
const results = await Promise.all(
|
|
316
314
|
entries.map(async ([name, factory]) => {
|
|
317
|
-
const start =
|
|
315
|
+
const start = Bun.nanoseconds();
|
|
318
316
|
const tool = await factory(session);
|
|
319
|
-
const elapsed =
|
|
317
|
+
const elapsed = (Bun.nanoseconds() - start) / 1e6;
|
|
320
318
|
if (elapsed > 5) {
|
|
321
319
|
slowTools.push({ name, ms: Math.round(elapsed) });
|
|
322
320
|
}
|
package/src/tools/notebook.ts
CHANGED
|
@@ -67,11 +67,7 @@ export class NotebookTool implements AgentTool<typeof notebookSchema, NotebookTo
|
|
|
67
67
|
public readonly parameters = notebookSchema;
|
|
68
68
|
public readonly concurrency = "exclusive";
|
|
69
69
|
|
|
70
|
-
private readonly session: ToolSession
|
|
71
|
-
|
|
72
|
-
constructor(session: ToolSession) {
|
|
73
|
-
this.session = session;
|
|
74
|
-
}
|
|
70
|
+
constructor(private readonly session: ToolSession) {}
|
|
75
71
|
|
|
76
72
|
public async execute(
|
|
77
73
|
_toolCallId: string,
|
package/src/tools/python.ts
CHANGED
|
@@ -148,11 +148,12 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
148
148
|
public readonly parameters = pythonSchema;
|
|
149
149
|
public readonly concurrency = "exclusive";
|
|
150
150
|
|
|
151
|
-
private readonly session: ToolSession | null;
|
|
152
151
|
private readonly proxyExecutor?: PythonProxyExecutor;
|
|
153
152
|
|
|
154
|
-
constructor(
|
|
155
|
-
|
|
153
|
+
constructor(
|
|
154
|
+
private readonly session: ToolSession | null,
|
|
155
|
+
options?: PythonToolOptions,
|
|
156
|
+
) {
|
|
156
157
|
this.proxyExecutor = options?.proxyExecutor;
|
|
157
158
|
this.description = getPythonToolDescription();
|
|
158
159
|
}
|
package/src/tools/read.ts
CHANGED
|
@@ -538,12 +538,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
538
538
|
public readonly parameters = readSchema;
|
|
539
539
|
public readonly nonAbortable = true;
|
|
540
540
|
|
|
541
|
-
private readonly session: ToolSession;
|
|
542
541
|
private readonly autoResizeImages: boolean;
|
|
543
542
|
private readonly defaultLineNumbers: boolean;
|
|
544
543
|
|
|
545
|
-
constructor(session: ToolSession) {
|
|
546
|
-
this.session = session;
|
|
544
|
+
constructor(private readonly session: ToolSession) {
|
|
547
545
|
this.autoResizeImages = session.settings.get("images.autoResize");
|
|
548
546
|
this.defaultLineNumbers = session.settings.get("readLineNumbers");
|
|
549
547
|
this.description = renderPromptTemplate(readDescription, {
|
|
@@ -633,7 +631,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
633
631
|
const maxStr = formatSize(MAX_IMAGE_SIZE);
|
|
634
632
|
throw new ToolError(`Image file too large: ${sizeStr} exceeds ${maxStr} limit.`);
|
|
635
633
|
} else {
|
|
636
|
-
const base64 =
|
|
634
|
+
const base64 = new Uint8Array(buffer).toBase64();
|
|
637
635
|
|
|
638
636
|
if (this.autoResizeImages) {
|
|
639
637
|
// Resize image if needed - catch errors from Photon
|
|
@@ -327,6 +327,19 @@ interface ParsedDiagnostic {
|
|
|
327
327
|
code?: string;
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
function getSeverityRank(severity: ParsedDiagnostic["severity"]): number {
|
|
331
|
+
switch (severity) {
|
|
332
|
+
case "error":
|
|
333
|
+
return 0;
|
|
334
|
+
case "warning":
|
|
335
|
+
return 1;
|
|
336
|
+
case "info":
|
|
337
|
+
return 2;
|
|
338
|
+
case "hint":
|
|
339
|
+
return 3;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
330
343
|
function parseDiagnosticMessage(msg: string): ParsedDiagnostic | null {
|
|
331
344
|
const match = msg.match(/^(.+?):(\d+):(\d+)\s+\[(\w+)\]\s+(?:\[([^\]]+)\]\s+)?(.+?)(?:\s+\(([^)]+)\))?$/);
|
|
332
345
|
if (!match) return null;
|
|
@@ -363,6 +376,16 @@ export function formatDiagnostics(
|
|
|
363
376
|
}
|
|
364
377
|
}
|
|
365
378
|
|
|
379
|
+
for (const diagnostics of byFile.values()) {
|
|
380
|
+
diagnostics.sort((a, b) => {
|
|
381
|
+
const severityCompare = getSeverityRank(a.severity) - getSeverityRank(b.severity);
|
|
382
|
+
if (severityCompare !== 0) return severityCompare;
|
|
383
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
384
|
+
if (a.col !== b.col) return a.col - b.col;
|
|
385
|
+
return a.message.localeCompare(b.message);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
366
389
|
const headerIcon = diag.errored
|
|
367
390
|
? theme.styledSymbol("status.error", "error")
|
|
368
391
|
: theme.styledSymbol("status.warning", "warning");
|
package/src/tools/ssh.ts
CHANGED
|
@@ -126,22 +126,18 @@ interface SshToolParams {
|
|
|
126
126
|
export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
|
|
127
127
|
public readonly name = "ssh";
|
|
128
128
|
public readonly label = "SSH";
|
|
129
|
-
public readonly description: string;
|
|
130
129
|
public readonly parameters = sshSchema;
|
|
131
130
|
public readonly concurrency = "exclusive";
|
|
132
131
|
|
|
133
|
-
private readonly session: ToolSession;
|
|
134
|
-
|
|
135
132
|
private readonly allowedHosts: Set<string>;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.allowedHosts = new Set(hostNames);
|
|
144
|
-
this.description = description;
|
|
133
|
+
|
|
134
|
+
constructor(
|
|
135
|
+
private readonly session: ToolSession,
|
|
136
|
+
private readonly hostNames: string[],
|
|
137
|
+
private readonly hostsByName: Map<string, SSHHost>,
|
|
138
|
+
public readonly description: string,
|
|
139
|
+
) {
|
|
140
|
+
this.allowedHosts = new Set(this.hostNames);
|
|
145
141
|
}
|
|
146
142
|
|
|
147
143
|
public async execute(
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -156,10 +156,7 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
|
|
|
156
156
|
public readonly parameters = todoWriteSchema;
|
|
157
157
|
public readonly concurrency = "exclusive";
|
|
158
158
|
|
|
159
|
-
private readonly session: ToolSession
|
|
160
|
-
|
|
161
|
-
constructor(session: ToolSession) {
|
|
162
|
-
this.session = session;
|
|
159
|
+
constructor(private readonly session: ToolSession) {
|
|
163
160
|
this.description = renderPromptTemplate(todoWriteDescription);
|
|
164
161
|
}
|
|
165
162
|
|
package/src/tools/tool-errors.ts
CHANGED
|
@@ -36,12 +36,9 @@ export interface ErrorEntry {
|
|
|
36
36
|
* Error with multiple entries (e.g., multiple validation failures, batch errors).
|
|
37
37
|
*/
|
|
38
38
|
export class MultiError extends ToolError {
|
|
39
|
-
readonly errors: ErrorEntry[]
|
|
40
|
-
|
|
41
|
-
constructor(errors: ErrorEntry[]) {
|
|
39
|
+
constructor(readonly errors: ErrorEntry[]) {
|
|
42
40
|
super(errors.map(e => e.message).join("; "));
|
|
43
41
|
this.name = "MultiError";
|
|
44
|
-
this.errors = errors;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
render(): string {
|
package/src/tools/write.ts
CHANGED
|
@@ -74,11 +74,9 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
74
74
|
public readonly nonAbortable = true;
|
|
75
75
|
public readonly concurrency = "exclusive";
|
|
76
76
|
|
|
77
|
-
private readonly session: ToolSession;
|
|
78
77
|
private readonly writethrough: WritethroughCallback;
|
|
79
78
|
|
|
80
|
-
constructor(session: ToolSession) {
|
|
81
|
-
this.session = session;
|
|
79
|
+
constructor(private readonly session: ToolSession) {
|
|
82
80
|
const enableLsp = session.enableLsp ?? true;
|
|
83
81
|
const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
|
|
84
82
|
const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnWrite");
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for launching an external text editor ($VISUAL / $EDITOR).
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { $env, Snowflake } from "@oh-my-pi/pi-utils";
|
|
9
|
+
|
|
10
|
+
/** Returns the user's preferred editor command, or undefined if not configured. */
|
|
11
|
+
export function getEditorCommand(): string | undefined {
|
|
12
|
+
return $env.VISUAL || $env.EDITOR || undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface OpenInEditorOptions {
|
|
16
|
+
/** File extension for the temp file (default: ".md"). */
|
|
17
|
+
extension?: string;
|
|
18
|
+
/** Custom stdio configuration (default: all "inherit"). */
|
|
19
|
+
stdio?: [number | "inherit", number | "inherit", number | "inherit"];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Opens `content` in the user's external editor and returns the edited text.
|
|
24
|
+
* Returns `null` if the editor exits with a non-zero code.
|
|
25
|
+
*
|
|
26
|
+
* The caller is responsible for stopping/starting the TUI around this call.
|
|
27
|
+
*/
|
|
28
|
+
export async function openInEditor(
|
|
29
|
+
editorCmd: string,
|
|
30
|
+
content: string,
|
|
31
|
+
options?: OpenInEditorOptions,
|
|
32
|
+
): Promise<string | null> {
|
|
33
|
+
const ext = options?.extension ?? ".md";
|
|
34
|
+
const tmpFile = path.join(os.tmpdir(), `omp-editor-${Snowflake.next()}${ext}`);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await Bun.write(tmpFile, content);
|
|
38
|
+
|
|
39
|
+
const [editor, ...editorArgs] = editorCmd.split(" ");
|
|
40
|
+
const stdio = options?.stdio ?? ["inherit", "inherit", "inherit"];
|
|
41
|
+
|
|
42
|
+
const child = spawn(editor, [...editorArgs, tmpFile], { stdio });
|
|
43
|
+
const exitCode = await new Promise<number>((resolve, reject) => {
|
|
44
|
+
child.once("exit", (code, signal) => resolve(code ?? (signal ? -1 : 0)));
|
|
45
|
+
child.once("error", error => reject(error));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (exitCode === 0) {
|
|
49
|
+
return (await Bun.file(tmpFile).text()).replace(/\n$/, "");
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
} finally {
|
|
53
|
+
try {
|
|
54
|
+
await fs.rm(tmpFile, { force: true });
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore cleanup errors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* we automatically inject the file contents as a FileMentionMessage
|
|
6
6
|
* so the agent doesn't need to read them manually.
|
|
7
7
|
*/
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
10
11
|
import type { FileMentionMessage } from "../session/messages";
|
|
11
12
|
import { resolveReadPath } from "../tools/path-utils";
|
|
12
13
|
import { formatAge } from "../tools/render-utils";
|
|
13
14
|
import { DEFAULT_MAX_BYTES, formatSize, truncateHead, truncateStringToBytesFromStart } from "../tools/truncate";
|
|
15
|
+
import { formatDimensionNote, resizeImage } from "./image-resize";
|
|
16
|
+
import { detectSupportedImageMimeTypeFromFile } from "./mime";
|
|
14
17
|
|
|
15
18
|
/** Regex to match @filepath patterns in text */
|
|
16
19
|
const FILE_MENTION_REGEX = /@([^\s@]+)/g;
|
|
@@ -156,9 +159,15 @@ export function extractFileMentions(text: string): string[] {
|
|
|
156
159
|
* Generate a FileMentionMessage containing the contents of mentioned files.
|
|
157
160
|
* Returns empty array if no files could be read.
|
|
158
161
|
*/
|
|
159
|
-
export async function generateFileMentionMessages(
|
|
162
|
+
export async function generateFileMentionMessages(
|
|
163
|
+
filePaths: string[],
|
|
164
|
+
cwd: string,
|
|
165
|
+
options?: { autoResizeImages?: boolean },
|
|
166
|
+
): Promise<AgentMessage[]> {
|
|
160
167
|
if (filePaths.length === 0) return [];
|
|
161
168
|
|
|
169
|
+
const autoResizeImages = options?.autoResizeImages ?? true;
|
|
170
|
+
|
|
162
171
|
const files: FileMentionMessage["files"] = [];
|
|
163
172
|
|
|
164
173
|
for (const filePath of filePaths) {
|
|
@@ -172,6 +181,35 @@ export async function generateFileMentionMessages(filePaths: string[], cwd: stri
|
|
|
172
181
|
continue;
|
|
173
182
|
}
|
|
174
183
|
|
|
184
|
+
const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
|
|
185
|
+
if (mimeType) {
|
|
186
|
+
const buffer = await fs.readFile(absolutePath);
|
|
187
|
+
if (buffer.length === 0) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const base64Content = buffer.toBase64();
|
|
192
|
+
let image = { type: "image" as const, mimeType, data: base64Content };
|
|
193
|
+
let dimensionNote: string | undefined;
|
|
194
|
+
|
|
195
|
+
if (autoResizeImages) {
|
|
196
|
+
try {
|
|
197
|
+
const resized = await resizeImage({ type: "image", data: base64Content, mimeType });
|
|
198
|
+
dimensionNote = formatDimensionNote(resized);
|
|
199
|
+
image = {
|
|
200
|
+
type: "image" as const,
|
|
201
|
+
mimeType: resized.mimeType,
|
|
202
|
+
data: resized.data,
|
|
203
|
+
};
|
|
204
|
+
} catch {
|
|
205
|
+
image = { type: "image" as const, mimeType, data: base64Content };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
files.push({ path: filePath, content: dimensionNote ?? "", image });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
175
213
|
const content = await Bun.file(absolutePath).text();
|
|
176
214
|
const { output, lineCount } = buildTextOutput(content);
|
|
177
215
|
files.push({ path: filePath, content: output, lineCount });
|
|
@@ -17,7 +17,7 @@ export async function convertToPng(
|
|
|
17
17
|
const image = await PhotonImage.parse(new Uint8Array(Buffer.from(base64Data, "base64")));
|
|
18
18
|
const pngBuffer = await image.encode(ImageFormat.PNG, 100);
|
|
19
19
|
return {
|
|
20
|
-
data: Buffer.from(pngBuffer).
|
|
20
|
+
data: Buffer.from(pngBuffer).toBase64(),
|
|
21
21
|
mimeType: "image/png",
|
|
22
22
|
};
|
|
23
23
|
} catch {
|
|
@@ -119,7 +119,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
119
119
|
|
|
120
120
|
if (best.buffer.length <= opts.maxBytes) {
|
|
121
121
|
return {
|
|
122
|
-
data:
|
|
122
|
+
data: best.buffer.toBase64(),
|
|
123
123
|
mimeType: best.mimeType,
|
|
124
124
|
originalWidth,
|
|
125
125
|
originalHeight,
|
|
@@ -135,7 +135,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
135
135
|
|
|
136
136
|
if (best.buffer.length <= opts.maxBytes) {
|
|
137
137
|
return {
|
|
138
|
-
data:
|
|
138
|
+
data: best.buffer.toBase64(),
|
|
139
139
|
mimeType: best.mimeType,
|
|
140
140
|
originalWidth,
|
|
141
141
|
originalHeight,
|
|
@@ -160,7 +160,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
160
160
|
|
|
161
161
|
if (best.buffer.length <= opts.maxBytes) {
|
|
162
162
|
return {
|
|
163
|
-
data:
|
|
163
|
+
data: best.buffer.toBase64(),
|
|
164
164
|
mimeType: best.mimeType,
|
|
165
165
|
originalWidth,
|
|
166
166
|
originalHeight,
|
|
@@ -174,7 +174,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
174
174
|
|
|
175
175
|
// Last resort: return smallest version we produced
|
|
176
176
|
return {
|
|
177
|
-
data:
|
|
177
|
+
data: best.buffer.toBase64(),
|
|
178
178
|
mimeType: best.mimeType,
|
|
179
179
|
originalWidth,
|
|
180
180
|
originalHeight,
|
package/src/web/search/auth.ts
CHANGED
|
@@ -7,13 +7,11 @@
|
|
|
7
7
|
* 3. OAuth credentials in ~/.omp/agent/agent.db (with expiry check)
|
|
8
8
|
* 4. ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL fallback
|
|
9
9
|
*/
|
|
10
|
-
import * as path from "node:path";
|
|
11
10
|
import { buildAnthropicHeaders as buildProviderAnthropicHeaders, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
12
11
|
import { $env, logger } from "@oh-my-pi/pi-utils";
|
|
13
12
|
import { getAgentDbPath, getConfigDirPaths } from "../../config";
|
|
14
13
|
import { AgentStorage } from "../../session/agent-storage";
|
|
15
|
-
import type { AuthCredential
|
|
16
|
-
import { migrateJsonStorage } from "../../session/storage-migration";
|
|
14
|
+
import type { AuthCredential } from "../../session/auth-storage";
|
|
17
15
|
import type { AnthropicAuthConfig, AnthropicOAuthCredential, ModelsJson } from "./types";
|
|
18
16
|
|
|
19
17
|
const DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
@@ -60,36 +58,12 @@ function toAnthropicOAuthCredential(credential: AuthCredential): AnthropicOAuthC
|
|
|
60
58
|
};
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
function normalizeAuthEntry(entry: AuthCredentialEntry | undefined): AuthCredential[] {
|
|
64
|
-
if (!entry) return [];
|
|
65
|
-
return Array.isArray(entry) ? entry : [entry];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function readLegacyAnthropicOAuthCredentials(configDir: string): Promise<AnthropicOAuthCredential[]> {
|
|
69
|
-
const authJson = await readJson<AuthStorageData>(path.join(configDir, "auth.json"));
|
|
70
|
-
if (!authJson) return [];
|
|
71
|
-
const entry = authJson.anthropic as AuthCredentialEntry | undefined;
|
|
72
|
-
const credentials = normalizeAuthEntry(entry);
|
|
73
|
-
const results: AnthropicOAuthCredential[] = [];
|
|
74
|
-
for (const credential of credentials) {
|
|
75
|
-
const mapped = toAnthropicOAuthCredential(credential);
|
|
76
|
-
if (mapped) results.push(mapped);
|
|
77
|
-
}
|
|
78
|
-
return results;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
61
|
/**
|
|
82
|
-
* Reads Anthropic OAuth credentials from agent.db
|
|
62
|
+
* Reads Anthropic OAuth credentials from agent.db.
|
|
83
63
|
* @param configDir - Path to the config directory containing agent.db
|
|
84
64
|
* @returns Array of valid Anthropic OAuth credentials
|
|
85
65
|
*/
|
|
86
66
|
async function readAnthropicOAuthCredentials(configDir: string): Promise<AnthropicOAuthCredential[]> {
|
|
87
|
-
await migrateJsonStorage({
|
|
88
|
-
agentDir: configDir,
|
|
89
|
-
settingsPath: path.join(configDir, "settings.json"),
|
|
90
|
-
authPaths: [path.join(configDir, "auth.json")],
|
|
91
|
-
});
|
|
92
|
-
|
|
93
67
|
const storage = await AgentStorage.open(getAgentDbPath(configDir));
|
|
94
68
|
const records = storage.listAuthCredentials("anthropic");
|
|
95
69
|
const credentials: AnthropicOAuthCredential[] = [];
|
|
@@ -100,10 +74,6 @@ async function readAnthropicOAuthCredentials(configDir: string): Promise<Anthrop
|
|
|
100
74
|
}
|
|
101
75
|
}
|
|
102
76
|
|
|
103
|
-
if (credentials.length === 0) {
|
|
104
|
-
return readLegacyAnthropicOAuthCredentials(configDir);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
77
|
return credentials;
|
|
108
78
|
}
|
|
109
79
|
|
|
@@ -132,7 +102,7 @@ export async function findAnthropicAuth(): Promise<AnthropicAuthConfig | null> {
|
|
|
132
102
|
|
|
133
103
|
// 2. Provider with api="anthropic-messages" in models.json (check all config dirs)
|
|
134
104
|
for (const configDir of configDirs) {
|
|
135
|
-
const modelsJson = await readJson<ModelsJson>(
|
|
105
|
+
const modelsJson = await readJson<ModelsJson>(`${configDir}/models.json`);
|
|
136
106
|
if (modelsJson?.providers) {
|
|
137
107
|
// First pass: look for providers with actual API keys
|
|
138
108
|
for (const [_name, provider] of Object.entries(modelsJson.providers)) {
|