@oh-my-pi/pi-coding-agent 13.10.1 → 13.11.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 +71 -0
- package/package.json +7 -7
- package/src/commit/agentic/agent.ts +3 -1
- package/src/commit/agentic/index.ts +7 -1
- package/src/commit/analysis/conventional.ts +5 -1
- package/src/commit/analysis/summary.ts +5 -1
- package/src/commit/changelog/generate.ts +5 -1
- package/src/commit/changelog/index.ts +4 -0
- package/src/commit/map-reduce/index.ts +5 -0
- package/src/commit/map-reduce/map-phase.ts +17 -2
- package/src/commit/map-reduce/reduce-phase.ts +5 -1
- package/src/commit/model-selection.ts +38 -26
- package/src/commit/pipeline.ts +22 -11
- package/src/config/model-registry.ts +98 -17
- package/src/config/settings-schema.ts +31 -12
- package/src/config.ts +10 -3
- package/src/discovery/helpers.ts +10 -3
- package/src/exa/index.ts +1 -11
- package/src/exa/search.ts +1 -122
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/config.ts +1 -0
- package/src/lsp/defaults.json +3 -3
- package/src/lsp/index.ts +4 -4
- package/src/lsp/utils.ts +81 -0
- package/src/modes/components/settings-defs.ts +5 -0
- package/src/modes/components/todo-reminder.ts +8 -1
- package/src/modes/controllers/command-controller.ts +77 -3
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +2 -3
- package/src/modes/controllers/selector-controller.ts +18 -17
- package/src/modes/interactive-mode.ts +11 -7
- package/src/modes/theme/theme.ts +30 -27
- package/src/modes/types.ts +2 -1
- package/src/patch/hashline.ts +123 -22
- package/src/prompts/system/eager-todo.md +13 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/code-search.md +45 -0
- package/src/prompts/tools/find.md +1 -0
- package/src/prompts/tools/grep.md +1 -0
- package/src/prompts/tools/hashline.md +26 -111
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/todo-write.md +11 -1
- package/src/sdk.ts +20 -16
- package/src/session/agent-session.ts +85 -7
- package/src/session/streaming-output.ts +17 -54
- package/src/slash-commands/builtin-registry.ts +10 -2
- package/src/task/executor.ts +10 -19
- package/src/task/index.ts +8 -4
- package/src/task/render.ts +5 -10
- package/src/task/template.ts +4 -1
- package/src/task/types.ts +2 -0
- package/src/tools/ast-edit.ts +26 -7
- package/src/tools/ast-grep.ts +26 -9
- package/src/tools/exit-plan-mode.ts +6 -0
- package/src/tools/fetch.ts +37 -6
- package/src/tools/find.ts +13 -64
- package/src/tools/grep.ts +27 -10
- package/src/tools/output-meta.ts +10 -7
- package/src/tools/path-utils.ts +348 -0
- package/src/tools/read.ts +13 -26
- package/src/tools/todo-write.ts +27 -4
- package/src/utils/commit-message-generator.ts +27 -22
- package/src/utils/image-input.ts +1 -1
- package/src/utils/image-resize.ts +4 -4
- package/src/utils/title-generator.ts +36 -23
- package/src/utils/tool-choice.ts +28 -0
- package/src/web/parallel.ts +346 -0
- package/src/web/scrapers/youtube.ts +29 -0
- package/src/web/search/code-search.ts +385 -0
- package/src/web/search/index.ts +25 -280
- package/src/web/search/provider.ts +4 -1
- package/src/web/search/providers/parallel.ts +63 -0
- package/src/web/search/types.ts +29 -0
- package/src/exa/company.ts +0 -26
- package/src/exa/linkedin.ts +0 -26
|
@@ -214,8 +214,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
214
214
|
{
|
|
215
215
|
name: "copy",
|
|
216
216
|
description: "Copy last agent message to clipboard",
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
subcommands: [
|
|
218
|
+
{ name: "last", description: "Copy full last agent message" },
|
|
219
|
+
{ name: "code", description: "Copy last code block" },
|
|
220
|
+
{ name: "all", description: "Copy all code blocks from last message" },
|
|
221
|
+
{ name: "cmd", description: "Copy last bash/python command" },
|
|
222
|
+
],
|
|
223
|
+
allowArgs: true,
|
|
224
|
+
handle: async (command, runtime) => {
|
|
225
|
+
const sub = command.args.trim().toLowerCase() || undefined;
|
|
226
|
+
await runtime.ctx.handleCopyCommand(sub);
|
|
219
227
|
runtime.ctx.editor.setText("");
|
|
220
228
|
},
|
|
221
229
|
},
|
package/src/task/executor.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
|
-
import type { Api, Model, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
9
8
|
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
9
|
import type { TSchema } from "@sinclair/typebox";
|
|
11
10
|
import Ajv, { type ValidateFunction } from "ajv";
|
|
@@ -28,6 +27,7 @@ import { type ContextFileEntry, truncateTail } from "../tools";
|
|
|
28
27
|
import { jtdToJsonSchema } from "../tools/jtd-to-json-schema";
|
|
29
28
|
import { ToolAbortError } from "../tools/tool-errors";
|
|
30
29
|
import type { EventBus } from "../utils/event-bus";
|
|
30
|
+
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
31
31
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
32
32
|
import {
|
|
33
33
|
type AgentDefinition,
|
|
@@ -117,28 +117,13 @@ function getReportFindingKey(value: unknown): string | null {
|
|
|
117
117
|
return `${filePath}:${lineStart}:${lineEnd}:${priority ?? ""}:${title}`;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
function buildSubmitResultToolChoice(model?: Model<Api>): ToolChoice | undefined {
|
|
121
|
-
if (!model) return undefined;
|
|
122
|
-
if (
|
|
123
|
-
model.api === "openai-codex-responses" ||
|
|
124
|
-
model.api === "openai-responses" ||
|
|
125
|
-
model.api === "openai-completions" ||
|
|
126
|
-
model.api === "azure-openai-responses"
|
|
127
|
-
) {
|
|
128
|
-
return { type: "function", name: "submit_result" };
|
|
129
|
-
}
|
|
130
|
-
if (model.api === "anthropic-messages" || model.api === "bedrock-converse-stream") {
|
|
131
|
-
return { type: "tool", name: "submit_result" };
|
|
132
|
-
}
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
120
|
/** Options for subagent execution */
|
|
137
121
|
export interface ExecutorOptions {
|
|
138
122
|
cwd: string;
|
|
139
123
|
worktree?: string;
|
|
140
124
|
agent: AgentDefinition;
|
|
141
125
|
task: string;
|
|
126
|
+
assignment?: string;
|
|
142
127
|
description?: string;
|
|
143
128
|
index: number;
|
|
144
129
|
id: string;
|
|
@@ -458,6 +443,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
458
443
|
cwd,
|
|
459
444
|
agent,
|
|
460
445
|
task,
|
|
446
|
+
assignment,
|
|
461
447
|
index,
|
|
462
448
|
id,
|
|
463
449
|
worktree,
|
|
@@ -478,6 +464,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
478
464
|
agentSource: agent.source,
|
|
479
465
|
status: "running",
|
|
480
466
|
task,
|
|
467
|
+
assignment,
|
|
481
468
|
description: options.description,
|
|
482
469
|
lastIntent: undefined,
|
|
483
470
|
recentTools: [],
|
|
@@ -496,6 +483,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
496
483
|
agent: agent.name,
|
|
497
484
|
agentSource: agent.source,
|
|
498
485
|
task,
|
|
486
|
+
assignment,
|
|
499
487
|
description: options.description,
|
|
500
488
|
exitCode: 1,
|
|
501
489
|
output: "",
|
|
@@ -638,6 +626,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
638
626
|
agent: agent.name,
|
|
639
627
|
agentSource: agent.source,
|
|
640
628
|
task,
|
|
629
|
+
assignment,
|
|
641
630
|
progress: { ...progress },
|
|
642
631
|
});
|
|
643
632
|
}
|
|
@@ -727,6 +716,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
727
716
|
agent: agent.name,
|
|
728
717
|
agentSource: agent.source,
|
|
729
718
|
task,
|
|
719
|
+
assignment,
|
|
730
720
|
event,
|
|
731
721
|
});
|
|
732
722
|
}
|
|
@@ -1091,7 +1081,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1091
1081
|
await session.prompt(task);
|
|
1092
1082
|
await session.waitForIdle();
|
|
1093
1083
|
|
|
1094
|
-
const reminderToolChoice =
|
|
1084
|
+
const reminderToolChoice = buildNamedToolChoice("submit_result", session.model);
|
|
1095
1085
|
|
|
1096
1086
|
let retryCount = 0;
|
|
1097
1087
|
while (!submitResultCalled && retryCount < MAX_SUBMIT_RESULT_RETRIES && !abortSignal.aborted) {
|
|
@@ -1247,12 +1237,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1247
1237
|
agent: agent.name,
|
|
1248
1238
|
agentSource: agent.source,
|
|
1249
1239
|
task,
|
|
1240
|
+
assignment,
|
|
1250
1241
|
description: options.description,
|
|
1251
1242
|
lastIntent: progress.lastIntent,
|
|
1252
1243
|
exitCode,
|
|
1253
1244
|
output: truncatedOutput,
|
|
1254
1245
|
stderr,
|
|
1255
|
-
truncated,
|
|
1246
|
+
truncated: Boolean(truncated),
|
|
1256
1247
|
durationMs: Date.now() - startTime,
|
|
1257
1248
|
tokens: progress.tokens,
|
|
1258
1249
|
modelOverride,
|
package/src/task/index.ts
CHANGED
|
@@ -224,6 +224,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
224
224
|
agentSource: fallbackAgentSource,
|
|
225
225
|
status: "pending",
|
|
226
226
|
task: renderedTask.task,
|
|
227
|
+
assignment: renderedTask.assignment,
|
|
227
228
|
description: renderedTask.description,
|
|
228
229
|
recentTools: [],
|
|
229
230
|
recentOutput: [],
|
|
@@ -732,6 +733,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
732
733
|
agentSource: agent.source,
|
|
733
734
|
status: "pending",
|
|
734
735
|
task: t.task,
|
|
736
|
+
assignment: t.assignment,
|
|
735
737
|
recentTools: [],
|
|
736
738
|
recentOutput: [],
|
|
737
739
|
toolCount: 0,
|
|
@@ -749,6 +751,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
749
751
|
cwd: this.session.cwd,
|
|
750
752
|
agent,
|
|
751
753
|
task: task.task,
|
|
754
|
+
assignment: task.assignment,
|
|
752
755
|
description: task.description,
|
|
753
756
|
index,
|
|
754
757
|
id: task.id,
|
|
@@ -801,6 +804,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
801
804
|
worktree: isolationDir,
|
|
802
805
|
agent,
|
|
803
806
|
task: task.task,
|
|
807
|
+
assignment: task.assignment,
|
|
804
808
|
description: task.description,
|
|
805
809
|
index,
|
|
806
810
|
id: task.id,
|
|
@@ -834,11 +838,10 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
834
838
|
const commitMsg =
|
|
835
839
|
commitStyle === "ai" && this.session.modelRegistry
|
|
836
840
|
? async (diff: string) => {
|
|
837
|
-
const smolModel = this.session.settings.getModelRole("smol");
|
|
838
841
|
return generateCommitMessage(
|
|
839
842
|
diff,
|
|
840
843
|
this.session.modelRegistry!,
|
|
841
|
-
|
|
844
|
+
this.session.settings,
|
|
842
845
|
this.session.getSessionId?.() ?? undefined,
|
|
843
846
|
);
|
|
844
847
|
}
|
|
@@ -887,6 +890,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
887
890
|
agent: agent.name,
|
|
888
891
|
agentSource: agent.source,
|
|
889
892
|
task: task.task,
|
|
893
|
+
assignment: task.assignment,
|
|
890
894
|
description: task.description,
|
|
891
895
|
exitCode: 1,
|
|
892
896
|
output: "",
|
|
@@ -930,6 +934,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
930
934
|
agent: agentName,
|
|
931
935
|
agentSource: agent.source,
|
|
932
936
|
task: task.task,
|
|
937
|
+
assignment: task.assignment,
|
|
933
938
|
description: task.description,
|
|
934
939
|
exitCode: 1,
|
|
935
940
|
output: "",
|
|
@@ -1081,11 +1086,10 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
1081
1086
|
const commitMsg =
|
|
1082
1087
|
commitStyle === "ai" && this.session.modelRegistry
|
|
1083
1088
|
? async (diff: string) => {
|
|
1084
|
-
const smolModel = this.session.settings.getModelRole("smol");
|
|
1085
1089
|
return generateCommitMessage(
|
|
1086
1090
|
diff,
|
|
1087
1091
|
this.session.modelRegistry!,
|
|
1088
|
-
|
|
1092
|
+
this.session.settings,
|
|
1089
1093
|
this.session.getSessionId?.() ?? undefined,
|
|
1090
1094
|
);
|
|
1091
1095
|
}
|
package/src/task/render.ts
CHANGED
|
@@ -374,16 +374,11 @@ function renderTaskSection(
|
|
|
374
374
|
maxExpanded = 20,
|
|
375
375
|
): string[] {
|
|
376
376
|
const lines: string[] = [];
|
|
377
|
-
const trimmed = task.
|
|
377
|
+
const trimmed = task.trim();
|
|
378
378
|
if (!expanded || !trimmed) return lines;
|
|
379
379
|
|
|
380
|
-
// Strip the shared <context>...</context> block — it's the same
|
|
381
|
-
// across all tasks and just adds noise when expanded.
|
|
382
|
-
const stripped = trimmed.replace(/<context>[\s\S]*?<\/context>\s*/, "").trimStart();
|
|
383
|
-
if (!stripped) return lines;
|
|
384
|
-
|
|
385
380
|
lines.push(`${continuePrefix}${theme.fg("dim", "Task")}`);
|
|
386
|
-
const taskLines =
|
|
381
|
+
const taskLines = trimmed.split("\n");
|
|
387
382
|
for (const line of taskLines.slice(0, maxExpanded)) {
|
|
388
383
|
lines.push(`${continuePrefix} ${theme.fg("dim", truncateToWidth(replaceTabs(line), 70))}`);
|
|
389
384
|
}
|
|
@@ -526,7 +521,7 @@ function renderAgentProgress(
|
|
|
526
521
|
|
|
527
522
|
if (progress.status === "running") {
|
|
528
523
|
if (!description) {
|
|
529
|
-
const taskPreview = truncateToWidth(progress.task, 40);
|
|
524
|
+
const taskPreview = truncateToWidth(progress.assignment ?? progress.task, 40);
|
|
530
525
|
statusLine += ` ${theme.fg("muted", taskPreview)}`;
|
|
531
526
|
}
|
|
532
527
|
if (progress.toolCount > 0) {
|
|
@@ -546,7 +541,7 @@ function renderAgentProgress(
|
|
|
546
541
|
|
|
547
542
|
lines.push(statusLine);
|
|
548
543
|
|
|
549
|
-
lines.push(...renderTaskSection(progress.task, continuePrefix, expanded, theme));
|
|
544
|
+
lines.push(...renderTaskSection(progress.assignment ?? progress.task, continuePrefix, expanded, theme));
|
|
550
545
|
|
|
551
546
|
// Current tool (if running) or most recent completed tool
|
|
552
547
|
if (progress.status === "running") {
|
|
@@ -781,7 +776,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
781
776
|
|
|
782
777
|
lines.push(statusLine);
|
|
783
778
|
|
|
784
|
-
lines.push(...renderTaskSection(result.task, continuePrefix, expanded, theme));
|
|
779
|
+
lines.push(...renderTaskSection(result.assignment ?? result.task, continuePrefix, expanded, theme));
|
|
785
780
|
|
|
786
781
|
if (aborted && result.abortReason) {
|
|
787
782
|
lines.push(
|
package/src/task/template.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { TaskItem } from "./types";
|
|
|
5
5
|
interface RenderResult {
|
|
6
6
|
/** Full task text sent to the subagent */
|
|
7
7
|
task: string;
|
|
8
|
+
/** Raw per-task assignment text, without prompt template boilerplate */
|
|
9
|
+
assignment: string;
|
|
8
10
|
id: string;
|
|
9
11
|
description: string;
|
|
10
12
|
}
|
|
@@ -20,10 +22,11 @@ export function renderTemplate(context: string | undefined, task: TaskItem): Ren
|
|
|
20
22
|
context = context?.trim();
|
|
21
23
|
|
|
22
24
|
if (!context || !assignment) {
|
|
23
|
-
return { task: assignment || context!, id, description };
|
|
25
|
+
return { task: assignment || context!, assignment: assignment || context!, id, description };
|
|
24
26
|
}
|
|
25
27
|
return {
|
|
26
28
|
task: renderPromptTemplate(subagentUserPromptTemplate, { context, assignment }),
|
|
29
|
+
assignment,
|
|
27
30
|
id,
|
|
28
31
|
description,
|
|
29
32
|
};
|
package/src/task/types.ts
CHANGED
|
@@ -136,6 +136,7 @@ export interface AgentProgress {
|
|
|
136
136
|
agentSource: AgentSource;
|
|
137
137
|
status: "pending" | "running" | "completed" | "failed" | "aborted";
|
|
138
138
|
task: string;
|
|
139
|
+
assignment?: string;
|
|
139
140
|
description?: string;
|
|
140
141
|
lastIntent?: string;
|
|
141
142
|
currentTool?: string;
|
|
@@ -158,6 +159,7 @@ export interface SingleResult {
|
|
|
158
159
|
agent: string;
|
|
159
160
|
agentSource: AgentSource;
|
|
160
161
|
task: string;
|
|
162
|
+
assignment?: string;
|
|
161
163
|
description?: string;
|
|
162
164
|
lastIntent?: string;
|
|
163
165
|
exitCode: number;
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -14,7 +14,13 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
|
|
|
14
14
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
15
15
|
import type { ToolSession } from ".";
|
|
16
16
|
import type { OutputMeta } from "./output-meta";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
combineSearchGlobs,
|
|
19
|
+
hasGlobPathChars,
|
|
20
|
+
parseSearchPath,
|
|
21
|
+
resolveMultiSearchPath,
|
|
22
|
+
resolveToCwd,
|
|
23
|
+
} from "./path-utils";
|
|
18
24
|
import {
|
|
19
25
|
dedupeParseErrors,
|
|
20
26
|
formatCount,
|
|
@@ -98,7 +104,12 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
98
104
|
}
|
|
99
105
|
const maxFiles = parseInt(process.env.PI_MAX_AST_FILES ?? "", 10) || 1000;
|
|
100
106
|
|
|
107
|
+
const formatScopePath = (targetPath: string): string => {
|
|
108
|
+
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
109
|
+
return relative.length === 0 ? "." : relative;
|
|
110
|
+
};
|
|
101
111
|
let searchPath: string | undefined;
|
|
112
|
+
let scopePath: string | undefined;
|
|
102
113
|
let globFilter = params.glob?.trim() || undefined;
|
|
103
114
|
const rawPath = params.path?.trim();
|
|
104
115
|
if (rawPath) {
|
|
@@ -112,21 +123,29 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
112
123
|
throw new ToolError(`Cannot rewrite internal URL without backing file: ${rawPath}`);
|
|
113
124
|
}
|
|
114
125
|
searchPath = resource.sourcePath;
|
|
126
|
+
scopePath = formatScopePath(searchPath);
|
|
115
127
|
} else {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
128
|
+
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
|
129
|
+
if (multiSearchPath) {
|
|
130
|
+
searchPath = multiSearchPath.basePath;
|
|
131
|
+
globFilter = multiSearchPath.glob;
|
|
132
|
+
scopePath = multiSearchPath.scopePath;
|
|
133
|
+
} else {
|
|
134
|
+
const parsedPath = parseSearchPath(rawPath);
|
|
135
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
136
|
+
globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
|
|
137
|
+
scopePath = formatScopePath(searchPath);
|
|
138
|
+
}
|
|
119
139
|
}
|
|
120
140
|
}
|
|
121
|
-
|
|
122
141
|
const resolvedSearchPath = searchPath ?? resolveToCwd(".", this.session.cwd);
|
|
123
|
-
|
|
142
|
+
scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
|
|
124
143
|
let isDirectory: boolean;
|
|
125
144
|
try {
|
|
126
145
|
const stat = await Bun.file(resolvedSearchPath).stat();
|
|
127
146
|
isDirectory = stat.isDirectory();
|
|
128
147
|
} catch {
|
|
129
|
-
throw new ToolError(`Path not found: ${
|
|
148
|
+
throw new ToolError(`Path not found: ${scopePath}`);
|
|
130
149
|
}
|
|
131
150
|
|
|
132
151
|
const result = await astEdit({
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -14,7 +14,13 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
|
|
|
14
14
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
15
15
|
import type { ToolSession } from ".";
|
|
16
16
|
import type { OutputMeta } from "./output-meta";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
combineSearchGlobs,
|
|
19
|
+
hasGlobPathChars,
|
|
20
|
+
parseSearchPath,
|
|
21
|
+
resolveMultiSearchPath,
|
|
22
|
+
resolveToCwd,
|
|
23
|
+
} from "./path-utils";
|
|
18
24
|
import {
|
|
19
25
|
dedupeParseErrors,
|
|
20
26
|
formatCount,
|
|
@@ -86,7 +92,12 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
86
92
|
throw new ToolError("Context must be a non-negative number");
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
const formatScopePath = (targetPath: string): string => {
|
|
96
|
+
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
97
|
+
return relative.length === 0 ? "." : relative;
|
|
98
|
+
};
|
|
89
99
|
let searchPath: string | undefined;
|
|
100
|
+
let scopePath: string | undefined;
|
|
90
101
|
let globFilter = params.glob?.trim() || undefined;
|
|
91
102
|
const rawPath = params.path?.trim();
|
|
92
103
|
if (rawPath) {
|
|
@@ -100,24 +111,30 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
100
111
|
throw new ToolError(`Cannot search internal URL without backing file: ${rawPath}`);
|
|
101
112
|
}
|
|
102
113
|
searchPath = resource.sourcePath;
|
|
114
|
+
scopePath = formatScopePath(searchPath);
|
|
103
115
|
} else {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
|
117
|
+
if (multiSearchPath) {
|
|
118
|
+
searchPath = multiSearchPath.basePath;
|
|
119
|
+
globFilter = multiSearchPath.glob;
|
|
120
|
+
scopePath = multiSearchPath.scopePath;
|
|
121
|
+
} else {
|
|
122
|
+
const parsedPath = parseSearchPath(rawPath);
|
|
123
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
124
|
+
globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
|
|
125
|
+
scopePath = formatScopePath(searchPath);
|
|
126
|
+
}
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
|
|
110
130
|
const resolvedSearchPath = searchPath ?? resolveToCwd(".", this.session.cwd);
|
|
111
|
-
|
|
112
|
-
const relative = path.relative(this.session.cwd, resolvedSearchPath).replace(/\\/g, "/");
|
|
113
|
-
return relative.length === 0 ? "." : relative;
|
|
114
|
-
})();
|
|
131
|
+
scopePath = scopePath ?? formatScopePath(resolvedSearchPath);
|
|
115
132
|
let isDirectory: boolean;
|
|
116
133
|
try {
|
|
117
134
|
const stat = await Bun.file(resolvedSearchPath).stat();
|
|
118
135
|
isDirectory = stat.isDirectory();
|
|
119
136
|
} catch {
|
|
120
|
-
throw new ToolError(`Path not found: ${
|
|
137
|
+
throw new ToolError(`Path not found: ${scopePath}`);
|
|
121
138
|
}
|
|
122
139
|
|
|
123
140
|
const result = await astGrep({
|
|
@@ -77,6 +77,12 @@ export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, Ex
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
if (!planExists) {
|
|
81
|
+
throw new ToolError(
|
|
82
|
+
`Plan file not found at ${state.planFilePath}. Write the finalized plan to ${state.planFilePath} before calling exit_plan_mode.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
80
86
|
return {
|
|
81
87
|
content: [{ type: "text", text: "Plan ready for approval." }],
|
|
82
88
|
details: {
|
package/src/tools/fetch.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { ptree, truncate } from "@oh-my-pi/pi-utils";
|
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
8
|
import { parseHTML } from "linkedom";
|
|
9
9
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
10
|
+
import type { Settings } from "../config/settings";
|
|
10
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
12
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
12
13
|
import fetchDescription from "../prompts/tools/fetch.md" with { type: "text" };
|
|
@@ -15,6 +16,7 @@ import { renderStatusLine } from "../tui";
|
|
|
15
16
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
16
17
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
17
18
|
import { ensureTool } from "../utils/tools-manager";
|
|
19
|
+
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
|
|
18
20
|
import { specialHandlers } from "../web/scrapers";
|
|
19
21
|
import type { RenderResult } from "../web/scrapers/types";
|
|
20
22
|
import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
|
|
@@ -465,12 +467,13 @@ function parseFeedToMarkdown(content: string, maxItems = 10): string {
|
|
|
465
467
|
}
|
|
466
468
|
|
|
467
469
|
/**
|
|
468
|
-
* Render HTML to markdown using jina, trafilatura, lynx (in order of preference)
|
|
470
|
+
* Render HTML to markdown using Parallel, jina, trafilatura, lynx (in order of preference)
|
|
469
471
|
*/
|
|
470
472
|
async function renderHtmlToText(
|
|
471
473
|
url: string,
|
|
472
474
|
html: string,
|
|
473
475
|
timeout: number,
|
|
476
|
+
settings: Settings,
|
|
474
477
|
userSignal?: AbortSignal,
|
|
475
478
|
): Promise<{ content: string; ok: boolean; method: string }> {
|
|
476
479
|
const signal = ptree.combineSignals(userSignal, timeout * 1000);
|
|
@@ -482,6 +485,28 @@ async function renderHtmlToText(
|
|
|
482
485
|
signal,
|
|
483
486
|
};
|
|
484
487
|
|
|
488
|
+
// Try Parallel extract first when credentials are configured
|
|
489
|
+
if (settings.get("providers.parallelFetch") && (await findParallelApiKey())) {
|
|
490
|
+
try {
|
|
491
|
+
const parallelResult = await extractWithParallel([url], {
|
|
492
|
+
objective: "Extract the main content",
|
|
493
|
+
excerpts: true,
|
|
494
|
+
fullContent: false,
|
|
495
|
+
signal,
|
|
496
|
+
});
|
|
497
|
+
const firstDocument = parallelResult.results[0];
|
|
498
|
+
if (firstDocument) {
|
|
499
|
+
const content = getParallelExtractContent(firstDocument);
|
|
500
|
+
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
501
|
+
return { content, ok: true, method: "parallel" };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} catch {
|
|
505
|
+
// Parallel extract failed, continue to next method
|
|
506
|
+
signal?.throwIfAborted();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
485
510
|
// Try jina first (reader API)
|
|
486
511
|
try {
|
|
487
512
|
const jinaUrl = `https://r.jina.ai/${url}`;
|
|
@@ -608,7 +633,13 @@ async function handleSpecialUrls(
|
|
|
608
633
|
/**
|
|
609
634
|
* Main render function implementing the full pipeline
|
|
610
635
|
*/
|
|
611
|
-
async function renderUrl(
|
|
636
|
+
async function renderUrl(
|
|
637
|
+
url: string,
|
|
638
|
+
timeout: number,
|
|
639
|
+
raw: boolean,
|
|
640
|
+
settings: Settings,
|
|
641
|
+
signal?: AbortSignal,
|
|
642
|
+
): Promise<FetchRenderResult> {
|
|
612
643
|
const notes: string[] = [];
|
|
613
644
|
const fetchedAt = new Date().toISOString();
|
|
614
645
|
if (signal?.aborted) {
|
|
@@ -714,7 +745,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: Ab
|
|
|
714
745
|
}
|
|
715
746
|
|
|
716
747
|
const resized = await resizeImage(
|
|
717
|
-
{ type: "image", data: binary.buffer.toBase64(), mimeType: imageMimeType },
|
|
748
|
+
{ type: "image", data: Buffer.from(binary.buffer).toBase64(), mimeType: imageMimeType },
|
|
718
749
|
{ maxBytes: MAX_INLINE_IMAGE_OUTPUT_BYTES },
|
|
719
750
|
);
|
|
720
751
|
const isDecodedImage =
|
|
@@ -952,7 +983,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: Ab
|
|
|
952
983
|
}
|
|
953
984
|
|
|
954
985
|
// 5E: Render HTML with lynx or html2text
|
|
955
|
-
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, signal);
|
|
986
|
+
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, settings, signal);
|
|
956
987
|
if (!htmlResult.ok) {
|
|
957
988
|
notes.push("html rendering failed (lynx/html2text unavailable)");
|
|
958
989
|
const output = finalizeOutput(rawContent);
|
|
@@ -1092,7 +1123,7 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
|
|
|
1092
1123
|
throw new ToolAbortError();
|
|
1093
1124
|
}
|
|
1094
1125
|
|
|
1095
|
-
const result = await renderUrl(url, effectiveTimeout, raw, signal);
|
|
1126
|
+
const result = await renderUrl(url, effectiveTimeout, raw, this.session.settings, signal);
|
|
1096
1127
|
const truncation = truncateHead(result.content, {
|
|
1097
1128
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
1098
1129
|
maxLines: FETCH_DEFAULT_MAX_LINES,
|
|
@@ -1128,7 +1159,7 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
|
|
|
1128
1159
|
finalUrl: result.finalUrl,
|
|
1129
1160
|
contentType: result.contentType,
|
|
1130
1161
|
method: result.method,
|
|
1131
|
-
truncated: result.truncated || needsArtifact,
|
|
1162
|
+
truncated: Boolean(result.truncated || needsArtifact),
|
|
1132
1163
|
notes: result.notes,
|
|
1133
1164
|
};
|
|
1134
1165
|
|
package/src/tools/find.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import type { ToolSession } from ".";
|
|
25
25
|
import { applyListLimit } from "./list-limit";
|
|
26
26
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
27
|
-
import { resolveToCwd } from "./path-utils";
|
|
27
|
+
import { parseFindPattern, resolveMultiFindPattern, resolveToCwd } from "./path-utils";
|
|
28
28
|
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
|
29
29
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
30
30
|
import { toolResult } from "./tool-result";
|
|
@@ -40,56 +40,6 @@ export type FindToolInput = Static<typeof findSchema>;
|
|
|
40
40
|
const DEFAULT_LIMIT = 1000;
|
|
41
41
|
const GLOB_TIMEOUT_MS = 5000;
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* Parse a pattern to extract the base directory path and glob pattern.
|
|
45
|
-
* Examples:
|
|
46
|
-
* "src/app/**\/*.tsx" → { basePath: "src/app", globPattern: "**\/*.tsx" }
|
|
47
|
-
* "src/app/*.tsx" → { basePath: "src/app", globPattern: "*.tsx" }
|
|
48
|
-
* "*.ts" → { basePath: ".", globPattern: "**\/*.ts" }
|
|
49
|
-
* "**\/*.json" → { basePath: ".", globPattern: "**\/*.json" }
|
|
50
|
-
* "/abs/path/**\/*.ts" → { basePath: "/abs/path", globPattern: "**\/*.ts" }
|
|
51
|
-
*/
|
|
52
|
-
function parsePatternPath(pattern: string): { basePath: string; globPattern: string } {
|
|
53
|
-
// Find the first segment containing glob characters
|
|
54
|
-
const segments = pattern.split("/");
|
|
55
|
-
const globChars = ["*", "?", "[", "{"];
|
|
56
|
-
|
|
57
|
-
let firstGlobIndex = -1;
|
|
58
|
-
for (let i = 0; i < segments.length; i++) {
|
|
59
|
-
if (globChars.some(c => segments[i].includes(c))) {
|
|
60
|
-
firstGlobIndex = i;
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// No glob characters found - treat as literal path with implicit **/*
|
|
66
|
-
if (firstGlobIndex === -1) {
|
|
67
|
-
// Pattern is a directory path like "src/app" - search recursively in it
|
|
68
|
-
return { basePath: pattern, globPattern: "**/*" };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Glob starts at first segment - no base path
|
|
72
|
-
if (firstGlobIndex === 0) {
|
|
73
|
-
// Simple pattern like "*.ts" needs **/ prefix for recursive search
|
|
74
|
-
const needsRecursive = !pattern.startsWith("**/");
|
|
75
|
-
return {
|
|
76
|
-
basePath: ".",
|
|
77
|
-
globPattern: needsRecursive ? `**/${pattern}` : pattern,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Split at the glob boundary
|
|
82
|
-
const basePath = segments.slice(0, firstGlobIndex).join("/");
|
|
83
|
-
const globPattern = segments.slice(firstGlobIndex).join("/");
|
|
84
|
-
|
|
85
|
-
return { basePath, globPattern };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function hasGlobChars(pattern: string): boolean {
|
|
89
|
-
const globChars = ["*", "?", "[", "{"];
|
|
90
|
-
return globChars.some(char => pattern.includes(char));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
43
|
export interface FindToolDetails {
|
|
94
44
|
truncation?: TruncationResult;
|
|
95
45
|
resultLimitReached?: number;
|
|
@@ -149,27 +99,26 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
149
99
|
const { pattern, limit, hidden } = params;
|
|
150
100
|
|
|
151
101
|
return untilAborted(signal, async () => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
102
|
+
const formatScopePath = (targetPath: string): string => {
|
|
103
|
+
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
104
|
+
return relative.length === 0 ? "." : relative;
|
|
105
|
+
};
|
|
155
106
|
const normalizedPattern = pattern.trim().replace(/\\/g, "/");
|
|
156
107
|
if (!normalizedPattern) {
|
|
157
108
|
throw new ToolError("Pattern must not be empty");
|
|
158
109
|
}
|
|
159
110
|
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
const
|
|
111
|
+
const multiPattern = await resolveMultiFindPattern(normalizedPattern, this.session.cwd);
|
|
112
|
+
const parsedPattern = multiPattern ? null : parseFindPattern(normalizedPattern);
|
|
113
|
+
const hasGlob = multiPattern ? true : (parsedPattern?.hasGlob ?? false);
|
|
114
|
+
const globPattern = multiPattern?.globPattern ?? parsedPattern?.globPattern ?? "**/*";
|
|
115
|
+
const searchPath = resolveToCwd(multiPattern?.basePath ?? parsedPattern?.basePath ?? ".", this.session.cwd);
|
|
116
|
+
const scopePath = multiPattern?.scopePath ?? formatScopePath(searchPath);
|
|
163
117
|
|
|
164
118
|
if (searchPath === "/") {
|
|
165
119
|
throw new ToolError("Searching from root directory '/' is not allowed");
|
|
166
120
|
}
|
|
167
121
|
|
|
168
|
-
const scopePath = (() => {
|
|
169
|
-
const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
|
|
170
|
-
return relative.length === 0 ? "." : relative;
|
|
171
|
-
})();
|
|
172
|
-
|
|
173
122
|
const rawLimit = limit ?? DEFAULT_LIMIT;
|
|
174
123
|
const effectiveLimit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : Number.NaN;
|
|
175
124
|
if (!Number.isFinite(effectiveLimit) || effectiveLimit <= 0) {
|
|
@@ -180,7 +129,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
180
129
|
// If custom operations provided with glob, use that instead of fd
|
|
181
130
|
if (this.#customOps?.glob) {
|
|
182
131
|
if (!(await this.#customOps.exists(searchPath))) {
|
|
183
|
-
throw new ToolError(`Path not found: ${
|
|
132
|
+
throw new ToolError(`Path not found: ${scopePath}`);
|
|
184
133
|
}
|
|
185
134
|
|
|
186
135
|
if (!hasGlob && this.#customOps.stat) {
|
|
@@ -245,7 +194,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
245
194
|
searchStat = await fs.promises.stat(searchPath);
|
|
246
195
|
} catch (err) {
|
|
247
196
|
if (isEnoent(err)) {
|
|
248
|
-
throw new ToolError(`Path not found: ${
|
|
197
|
+
throw new ToolError(`Path not found: ${scopePath}`);
|
|
249
198
|
}
|
|
250
199
|
throw err;
|
|
251
200
|
}
|