@oh-my-pi/pi-coding-agent 13.3.13 → 13.4.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +97 -7
  2. package/examples/sdk/README.md +22 -0
  3. package/package.json +7 -7
  4. package/src/capability/index.ts +1 -11
  5. package/src/commit/analysis/index.ts +4 -4
  6. package/src/config/settings-schema.ts +18 -15
  7. package/src/config/settings.ts +2 -20
  8. package/src/discovery/index.ts +1 -11
  9. package/src/exa/index.ts +1 -10
  10. package/src/extensibility/custom-commands/index.ts +2 -15
  11. package/src/extensibility/custom-tools/index.ts +3 -18
  12. package/src/extensibility/custom-tools/loader.ts +28 -5
  13. package/src/extensibility/custom-tools/types.ts +18 -1
  14. package/src/extensibility/extensions/index.ts +9 -130
  15. package/src/extensibility/extensions/types.ts +2 -1
  16. package/src/extensibility/hooks/index.ts +3 -14
  17. package/src/extensibility/plugins/index.ts +6 -31
  18. package/src/index.ts +28 -220
  19. package/src/internal-urls/docs-index.generated.ts +3 -2
  20. package/src/internal-urls/index.ts +11 -16
  21. package/src/mcp/index.ts +11 -37
  22. package/src/mcp/tool-bridge.ts +3 -42
  23. package/src/mcp/transports/index.ts +2 -2
  24. package/src/modes/components/extensions/index.ts +3 -3
  25. package/src/modes/components/index.ts +35 -40
  26. package/src/modes/interactive-mode.ts +4 -1
  27. package/src/modes/rpc/rpc-mode.ts +1 -7
  28. package/src/modes/theme/theme.ts +11 -10
  29. package/src/modes/types.ts +1 -1
  30. package/src/patch/index.ts +4 -20
  31. package/src/prompts/system/system-prompt.md +18 -4
  32. package/src/prompts/tools/ast-edit.md +33 -0
  33. package/src/prompts/tools/ast-grep.md +34 -0
  34. package/src/prompts/tools/bash.md +2 -2
  35. package/src/prompts/tools/hashline.md +1 -0
  36. package/src/prompts/tools/resolve.md +8 -0
  37. package/src/sdk.ts +27 -7
  38. package/src/session/agent-session.ts +25 -36
  39. package/src/session/session-manager.ts +0 -30
  40. package/src/slash-commands/builtin-registry.ts +4 -2
  41. package/src/stt/index.ts +3 -3
  42. package/src/task/types.ts +2 -2
  43. package/src/tools/ast-edit.ts +480 -0
  44. package/src/tools/ast-grep.ts +435 -0
  45. package/src/tools/bash.ts +3 -2
  46. package/src/tools/gemini-image.ts +3 -3
  47. package/src/tools/grep.ts +26 -8
  48. package/src/tools/index.ts +55 -57
  49. package/src/tools/pending-action.ts +33 -0
  50. package/src/tools/render-utils.ts +10 -0
  51. package/src/tools/renderers.ts +6 -4
  52. package/src/tools/resolve.ts +156 -0
  53. package/src/tools/submit-result.ts +1 -1
  54. package/src/web/search/index.ts +6 -4
  55. package/src/web/search/providers/anthropic.ts +2 -2
  56. package/src/web/search/providers/base.ts +3 -0
  57. package/src/web/search/providers/exa.ts +11 -5
  58. package/src/web/search/providers/gemini.ts +112 -24
  59. package/src/patch/normative.ts +0 -72
  60. package/src/prompts/tools/ast-find.md +0 -20
  61. package/src/prompts/tools/ast-replace.md +0 -21
  62. package/src/tools/ast-find.ts +0 -316
  63. package/src/tools/ast-replace.ts +0 -294
@@ -1,294 +0,0 @@
1
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import { astReplace } from "@oh-my-pi/pi-natives";
3
- import type { Component } from "@oh-my-pi/pi-tui";
4
- import { Text } from "@oh-my-pi/pi-tui";
5
- import { untilAborted } from "@oh-my-pi/pi-utils";
6
- import { type Static, Type } from "@sinclair/typebox";
7
- import { renderPromptTemplate } from "../config/prompt-templates";
8
- import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
- import type { Theme } from "../modes/theme/theme";
10
- import astReplaceDescription from "../prompts/tools/ast-replace.md" with { type: "text" };
11
- import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
12
- import type { ToolSession } from ".";
13
- import type { OutputMeta } from "./output-meta";
14
- import { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
15
- import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
16
- import { ToolError } from "./tool-errors";
17
- import { toolResult } from "./tool-result";
18
-
19
- const astReplaceSchema = Type.Object({
20
- pattern: Type.String({ description: "AST pattern to match" }),
21
- rewrite: Type.String({ description: "Rewrite template" }),
22
- lang: Type.Optional(Type.String({ description: "Language override" })),
23
- path: Type.Optional(Type.String({ description: "File, directory, or glob pattern to rewrite (default: cwd)" })),
24
- selector: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
25
- dry_run: Type.Optional(Type.Boolean({ description: "Preview only (default: true)" })),
26
- max_replacements: Type.Optional(Type.Number({ description: "Safety cap on total replacements" })),
27
- max_files: Type.Optional(Type.Number({ description: "Safety cap on touched files" })),
28
- });
29
-
30
- export interface AstReplaceToolDetails {
31
- totalReplacements: number;
32
- filesTouched: number;
33
- filesSearched: number;
34
- applied: boolean;
35
- limitReached: boolean;
36
- parseErrors?: string[];
37
- meta?: OutputMeta;
38
- }
39
-
40
- export class AstReplaceTool implements AgentTool<typeof astReplaceSchema, AstReplaceToolDetails> {
41
- readonly name = "ast_replace";
42
- readonly label = "AST Replace";
43
- readonly description: string;
44
- readonly parameters = astReplaceSchema;
45
- readonly strict = true;
46
-
47
- constructor(private readonly session: ToolSession) {
48
- this.description = renderPromptTemplate(astReplaceDescription);
49
- }
50
-
51
- async execute(
52
- _toolCallId: string,
53
- params: Static<typeof astReplaceSchema>,
54
- signal?: AbortSignal,
55
- _onUpdate?: AgentToolUpdateCallback<AstReplaceToolDetails>,
56
- _context?: AgentToolContext,
57
- ): Promise<AgentToolResult<AstReplaceToolDetails>> {
58
- return untilAborted(signal, async () => {
59
- const pattern = params.pattern?.trim();
60
- if (!pattern) {
61
- throw new ToolError("`pattern` is required");
62
- }
63
- if (!params.rewrite?.trim()) {
64
- throw new ToolError("`rewrite` is required");
65
- }
66
-
67
- const maxReplacements =
68
- params.max_replacements === undefined ? undefined : Math.floor(params.max_replacements);
69
- if (maxReplacements !== undefined && (!Number.isFinite(maxReplacements) || maxReplacements < 1)) {
70
- throw new ToolError("max_replacements must be a positive number");
71
- }
72
- const maxFiles = params.max_files === undefined ? undefined : Math.floor(params.max_files);
73
- if (maxFiles !== undefined && (!Number.isFinite(maxFiles) || maxFiles < 1)) {
74
- throw new ToolError("max_files must be a positive number");
75
- }
76
-
77
- let searchPath: string | undefined;
78
- let globFilter: string | undefined;
79
- const rawPath = params.path?.trim();
80
- if (rawPath) {
81
- const internalRouter = this.session.internalRouter;
82
- if (internalRouter?.canHandle(rawPath)) {
83
- if (hasGlobPathChars(rawPath)) {
84
- throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPath}`);
85
- }
86
- const resource = await internalRouter.resolve(rawPath);
87
- if (!resource.sourcePath) {
88
- throw new ToolError(`Cannot rewrite internal URL without backing file: ${rawPath}`);
89
- }
90
- searchPath = resource.sourcePath;
91
- } else {
92
- const parsedPath = parseSearchPath(rawPath);
93
- searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
94
- globFilter = parsedPath.glob;
95
- }
96
- }
97
-
98
- const result = await astReplace({
99
- pattern,
100
- rewrite: params.rewrite?.trim(),
101
- lang: params.lang?.trim(),
102
- path: searchPath,
103
- glob: globFilter,
104
- selector: params.selector?.trim(),
105
- dryRun: params.dry_run,
106
- maxReplacements,
107
- maxFiles,
108
- failOnParseError: false,
109
- signal,
110
- });
111
-
112
- const details: AstReplaceToolDetails = {
113
- totalReplacements: result.totalReplacements,
114
- filesTouched: result.filesTouched,
115
- filesSearched: result.filesSearched,
116
- applied: result.applied,
117
- limitReached: result.limitReached,
118
- parseErrors: result.parseErrors,
119
- };
120
-
121
- const action = result.applied ? "Applied" : "Would apply";
122
- const lines = [
123
- `${action} ${result.totalReplacements} replacements across ${result.filesTouched} files (searched ${result.filesSearched})`,
124
- ];
125
- if (result.fileChanges.length > 0) {
126
- lines.push("", "File changes:");
127
- for (const file of result.fileChanges) {
128
- lines.push(`- ${file.path}: ${file.count}`);
129
- }
130
- }
131
- if (result.changes.length > 0) {
132
- lines.push("", "Preview:");
133
- for (const change of result.changes.slice(0, 30)) {
134
- const before = (change.before.split("\n", 1)[0] ?? "").slice(0, 80);
135
- const after = (change.after.split("\n", 1)[0] ?? "").slice(0, 80);
136
- lines.push(`${change.path}:${change.startLine}:${change.startColumn} ${before} -> ${after}`);
137
- }
138
- if (result.changes.length > 30) {
139
- lines.push(`... ${result.changes.length - 30} more changes`);
140
- }
141
- }
142
- if (result.limitReached) {
143
- lines.push("", "Safety cap reached; narrow path pattern or increase max_files/max_replacements.");
144
- }
145
- if (result.parseErrors?.length) {
146
- lines.push("", "Parse issues:", ...result.parseErrors.map(err => `- ${err}`));
147
- }
148
-
149
- return toolResult(details).text(lines.join("\n")).done();
150
- });
151
- }
152
- }
153
-
154
- // =============================================================================
155
- // TUI Renderer
156
- // =============================================================================
157
-
158
- interface AstReplaceRenderArgs {
159
- pattern?: string;
160
- rewrite?: string;
161
- lang?: string;
162
- path?: string;
163
- selector?: string;
164
- dry_run?: boolean;
165
- max_replacements?: number;
166
- max_files?: number;
167
- fail_on_parse_error?: boolean;
168
- }
169
-
170
- const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
171
-
172
- export const astReplaceToolRenderer = {
173
- inline: true,
174
- renderCall(args: AstReplaceRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
175
- const meta: string[] = [];
176
- if (args.lang) meta.push(`lang:${args.lang}`);
177
- if (args.path) meta.push(`in ${args.path}`);
178
- if (args.dry_run !== false) meta.push("dry run");
179
- if (args.max_replacements !== undefined) meta.push(`max:${args.max_replacements}`);
180
- if (args.max_files !== undefined) meta.push(`max files:${args.max_files}`);
181
-
182
- const description = args.pattern || "?";
183
- const text = renderStatusLine({ icon: "pending", title: "AST Replace", description, meta }, uiTheme);
184
- return new Text(text, 0, 0);
185
- },
186
-
187
- renderResult(
188
- result: { content: Array<{ type: string; text?: string }>; details?: AstReplaceToolDetails; isError?: boolean },
189
- options: RenderResultOptions,
190
- uiTheme: Theme,
191
- args?: AstReplaceRenderArgs,
192
- ): Component {
193
- const details = result.details;
194
-
195
- if (result.isError) {
196
- const errorText = result.content?.find(c => c.type === "text")?.text || "Unknown error";
197
- return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
198
- }
199
-
200
- const totalReplacements = details?.totalReplacements ?? 0;
201
- const filesTouched = details?.filesTouched ?? 0;
202
- const filesSearched = details?.filesSearched ?? 0;
203
- const applied = details?.applied ?? false;
204
- const limitReached = details?.limitReached ?? false;
205
-
206
- if (totalReplacements === 0) {
207
- const description = args?.pattern;
208
- const meta = ["0 replacements"];
209
- if (filesSearched > 0) meta.push(`searched ${filesSearched}`);
210
- const header = renderStatusLine({ icon: "warning", title: "AST Replace", description, meta }, uiTheme);
211
- return new Text([header, formatEmptyMessage("No replacements made", uiTheme)].join("\n"), 0, 0);
212
- }
213
-
214
- const summaryParts = [
215
- formatCount("replacement", totalReplacements),
216
- formatCount("file", filesTouched),
217
- `searched ${filesSearched}`,
218
- ];
219
- const meta = [...summaryParts];
220
- if (limitReached) meta.push(uiTheme.fg("warning", "limit reached"));
221
- const description = args?.pattern;
222
- const badge = applied
223
- ? { label: "applied", color: "success" as const }
224
- : { label: "dry run", color: "warning" as const };
225
- const header = renderStatusLine(
226
- { icon: limitReached ? "warning" : "success", title: "AST Replace", description, badge, meta },
227
- uiTheme,
228
- );
229
-
230
- // Parse text content into display groups
231
- const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
232
- const rawLines = textContent.split("\n");
233
- // Skip the summary line and group by blank-line separators
234
- const contentLines = rawLines.slice(1);
235
- const allGroups: string[][] = [];
236
- let current: string[] = [];
237
- for (const line of contentLines) {
238
- if (line.trim().length === 0) {
239
- if (current.length > 0) {
240
- allGroups.push(current);
241
- current = [];
242
- }
243
- continue;
244
- }
245
- current.push(line);
246
- }
247
- if (current.length > 0) allGroups.push(current);
248
-
249
- // Filter out trailing metadata groups (safety cap / parse issues) — shown via details
250
- const displayGroups = allGroups.filter(
251
- group => !group[0]?.startsWith("Safety cap") && !group[0]?.startsWith("Parse issues"),
252
- );
253
-
254
- const extraLines: string[] = [];
255
- if (limitReached) {
256
- extraLines.push(uiTheme.fg("warning", "safety cap reached; narrow scope or increase limits"));
257
- }
258
- if (details?.parseErrors?.length) {
259
- extraLines.push(uiTheme.fg("warning", `${details.parseErrors.length} parse issue(s)`));
260
- }
261
-
262
- let cached: RenderCache | undefined;
263
- return {
264
- render(width: number): string[] {
265
- const { expanded } = options;
266
- const key = new Hasher().bool(expanded).u32(width).digest();
267
- if (cached?.key === key) return cached.lines;
268
- const matchLines = renderTreeList(
269
- {
270
- items: displayGroups,
271
- expanded,
272
- maxCollapsed: expanded ? displayGroups.length : COLLAPSED_CHANGE_LIMIT,
273
- itemType: "section",
274
- renderItem: group =>
275
- group.map(line => {
276
- if (line === "File changes:" || line === "Preview:") return uiTheme.fg("accent", line);
277
- if (line.startsWith("- ")) return uiTheme.fg("toolOutput", line);
278
- if (line.startsWith("...")) return uiTheme.fg("dim", line);
279
- return uiTheme.fg("toolOutput", line);
280
- }),
281
- },
282
- uiTheme,
283
- );
284
- const result = [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
285
- cached = { key, lines: result };
286
- return result;
287
- },
288
- invalidate() {
289
- cached = undefined;
290
- },
291
- };
292
- },
293
- mergeCallAndResult: true,
294
- };