@oh-my-pi/pi-coding-agent 3.21.0 → 3.24.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 (66) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/docs/sdk.md +47 -50
  3. package/examples/custom-tools/README.md +0 -15
  4. package/examples/hooks/custom-compaction.ts +1 -3
  5. package/examples/sdk/README.md +6 -10
  6. package/package.json +5 -5
  7. package/src/cli/args.ts +9 -6
  8. package/src/core/agent-session.ts +3 -3
  9. package/src/core/custom-tools/wrapper.ts +0 -1
  10. package/src/core/extensions/index.ts +1 -6
  11. package/src/core/extensions/wrapper.ts +0 -7
  12. package/src/core/file-mentions.ts +5 -8
  13. package/src/core/sdk.ts +41 -111
  14. package/src/core/session-manager.ts +7 -0
  15. package/src/core/system-prompt.ts +22 -33
  16. package/src/core/tools/ask.ts +14 -7
  17. package/src/core/tools/bash-interceptor.ts +4 -4
  18. package/src/core/tools/bash.ts +19 -9
  19. package/src/core/tools/context.ts +7 -0
  20. package/src/core/tools/edit.ts +8 -15
  21. package/src/core/tools/exa/render.ts +4 -16
  22. package/src/core/tools/find.ts +7 -18
  23. package/src/core/tools/git.ts +13 -3
  24. package/src/core/tools/grep.ts +7 -18
  25. package/src/core/tools/index.test.ts +180 -0
  26. package/src/core/tools/index.ts +94 -237
  27. package/src/core/tools/ls.ts +4 -9
  28. package/src/core/tools/lsp/index.ts +32 -29
  29. package/src/core/tools/lsp/render.ts +7 -28
  30. package/src/core/tools/notebook.ts +3 -5
  31. package/src/core/tools/output.ts +5 -17
  32. package/src/core/tools/read.ts +8 -19
  33. package/src/core/tools/review.ts +0 -18
  34. package/src/core/tools/rulebook.ts +8 -2
  35. package/src/core/tools/task/agents.ts +28 -7
  36. package/src/core/tools/task/discovery.ts +0 -6
  37. package/src/core/tools/task/executor.ts +264 -254
  38. package/src/core/tools/task/index.ts +45 -220
  39. package/src/core/tools/task/render.ts +21 -11
  40. package/src/core/tools/task/types.ts +6 -11
  41. package/src/core/tools/task/worker-protocol.ts +17 -0
  42. package/src/core/tools/task/worker.ts +238 -0
  43. package/src/core/tools/web-fetch.ts +4 -36
  44. package/src/core/tools/web-search/index.ts +2 -1
  45. package/src/core/tools/web-search/render.ts +1 -4
  46. package/src/core/tools/write.ts +7 -15
  47. package/src/discovery/helpers.test.ts +1 -1
  48. package/src/index.ts +5 -16
  49. package/src/main.ts +4 -4
  50. package/src/modes/interactive/theme/theme.ts +4 -4
  51. package/src/prompts/task.md +0 -7
  52. package/src/prompts/tools/output.md +2 -2
  53. package/src/prompts/tools/task.md +68 -0
  54. package/examples/custom-tools/question/index.ts +0 -84
  55. package/examples/custom-tools/subagent/README.md +0 -172
  56. package/examples/custom-tools/subagent/agents/planner.md +0 -37
  57. package/examples/custom-tools/subagent/agents/scout.md +0 -50
  58. package/examples/custom-tools/subagent/agents/worker.md +0 -24
  59. package/examples/custom-tools/subagent/agents.ts +0 -156
  60. package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
  61. package/examples/custom-tools/subagent/commands/implement.md +0 -10
  62. package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
  63. package/examples/custom-tools/subagent/index.ts +0 -1002
  64. package/examples/sdk/05-tools.ts +0 -94
  65. package/examples/sdk/12-full-control.ts +0 -95
  66. package/src/prompts/browser.md +0 -71
@@ -78,10 +78,7 @@ export function renderExaResult(
78
78
  }
79
79
 
80
80
  if (remaining > 0) {
81
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
82
- "muted",
83
- formatMoreItems(remaining, "line", uiTheme),
84
- )}`;
81
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", formatMoreItems(remaining, "line", uiTheme))}`;
85
82
  }
86
83
 
87
84
  return new Text(text, 0, 0);
@@ -168,17 +165,11 @@ export function renderExaResult(
168
165
  text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", title)}${domainPart}`;
169
166
 
170
167
  if (res.url) {
171
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
172
- "mdLinkUrl",
173
- res.url,
174
- )}`;
168
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("mdLinkUrl", res.url)}`;
175
169
  }
176
170
 
177
171
  if (res.author) {
178
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
179
- "muted",
180
- `Author: ${res.author}`,
181
- )}`;
172
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("muted", `Author: ${res.author}`)}`;
182
173
  }
183
174
 
184
175
  if (res.publishedDate) {
@@ -206,10 +197,7 @@ export function renderExaResult(
206
197
  }
207
198
 
208
199
  if (res.highlights?.length) {
209
- text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
210
- "accent",
211
- "Highlights",
212
- )}`;
200
+ text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg("accent", "Highlights")}`;
213
201
  const maxHighlights = Math.min(res.highlights.length, 3);
214
202
  for (let j = 0; j < maxHighlights; j++) {
215
203
  const h = res.highlights[j];
@@ -10,6 +10,7 @@ import findDescription from "../../prompts/tools/find.md" with { type: "text" };
10
10
  import { ensureTool } from "../../utils/tools-manager";
11
11
  import type { RenderResultOptions } from "../custom-tools/types";
12
12
  import { untilAborted } from "../utils";
13
+ import type { ToolSession } from "./index";
13
14
  import { resolveToCwd } from "./path-utils";
14
15
  import {
15
16
  formatCount,
@@ -55,7 +56,7 @@ export interface FindToolDetails {
55
56
  error?: string;
56
57
  }
57
58
 
58
- export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
59
+ export function createFindTool(session: ToolSession): AgentTool<typeof findSchema> {
59
60
  return {
60
61
  name: "find",
61
62
  label: "Find",
@@ -87,9 +88,9 @@ export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
87
88
  throw new Error("fd is not available and could not be downloaded");
88
89
  }
89
90
 
90
- const searchPath = resolveToCwd(searchDir || ".", cwd);
91
+ const searchPath = resolveToCwd(searchDir || ".", session.cwd);
91
92
  const scopePath = (() => {
92
- const relative = path.relative(cwd, searchPath).replace(/\\/g, "/");
93
+ const relative = path.relative(session.cwd, searchPath).replace(/\\/g, "/");
93
94
  return relative.length === 0 ? "." : relative;
94
95
  })();
95
96
  const effectiveLimit = limit ?? DEFAULT_LIMIT;
@@ -261,9 +262,6 @@ export function createFindTool(cwd: string): AgentTool<typeof findSchema> {
261
262
  };
262
263
  }
263
264
 
264
- /** Default find tool using process.cwd() - for backwards compatibility */
265
- export const findTool = createFindTool(process.cwd());
266
-
267
265
  // =============================================================================
268
266
  // TUI Renderer
269
267
  // =============================================================================
@@ -332,10 +330,7 @@ export const findToolRenderer = {
332
330
  text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", displayLines[i])}`;
333
331
  }
334
332
  if (remaining > 0) {
335
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
336
- "muted",
337
- formatMoreItems(remaining, "file", uiTheme),
338
- )}`;
333
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", formatMoreItems(remaining, "file", uiTheme))}`;
339
334
  }
340
335
  return new Text(text, 0, 0);
341
336
  }
@@ -355,10 +350,7 @@ export const findToolRenderer = {
355
350
  const hasMoreFiles = files.length > maxFiles;
356
351
  const expandHint = formatExpandHint(expanded, hasMoreFiles, uiTheme);
357
352
 
358
- let text = `${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(
359
- truncated,
360
- uiTheme,
361
- )}${scopeLabel}${expandHint}`;
353
+ let text = `${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(truncated, uiTheme)}${scopeLabel}${expandHint}`;
362
354
 
363
355
  const truncationReasons: string[] = [];
364
356
  if (details?.resultLimitReached) {
@@ -394,10 +386,7 @@ export const findToolRenderer = {
394
386
  }
395
387
 
396
388
  if (hasTruncation) {
397
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
398
- "warning",
399
- `truncated: ${truncationReasons.join(", ")}`,
400
- )}`;
389
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)}`;
401
390
  }
402
391
 
403
392
  return new Text(text, 0, 0);
@@ -2,6 +2,7 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
2
  import { type GitParams, gitTool as gitToolCore, type ToolResponse } from "@oh-my-pi/pi-git-tool";
3
3
  import { type Static, Type } from "@sinclair/typebox";
4
4
  import gitDescription from "../../prompts/tools/git.md" with { type: "text" };
5
+ import type { ToolSession } from "./index";
5
6
 
6
7
  const gitSchema = Type.Object({
7
8
  operation: Type.Union([
@@ -192,7 +193,10 @@ const gitSchema = Type.Object({
192
193
 
193
194
  export type GitToolDetails = ToolResponse<unknown>;
194
195
 
195
- export function createGitTool(cwd: string): AgentTool<typeof gitSchema, GitToolDetails> {
196
+ export function createGitTool(session: ToolSession): AgentTool<typeof gitSchema, GitToolDetails> | null {
197
+ if (session.settings?.getGitToolEnabled() === false) {
198
+ return null;
199
+ }
196
200
  return {
197
201
  name: "git",
198
202
  label: "Git",
@@ -203,7 +207,7 @@ export function createGitTool(cwd: string): AgentTool<typeof gitSchema, GitToolD
203
207
  throw new Error("Git commit requires a message to avoid an interactive editor. Provide `message`.");
204
208
  }
205
209
 
206
- const result = await gitToolCore(params as GitParams, cwd);
210
+ const result = await gitToolCore(params as GitParams, session.cwd);
207
211
  if ("error" in result) {
208
212
  const message = result._rendered ?? result.error;
209
213
  return { content: [{ type: "text", text: message }], details: result };
@@ -217,4 +221,10 @@ export function createGitTool(cwd: string): AgentTool<typeof gitSchema, GitToolD
217
221
  };
218
222
  }
219
223
 
220
- export const gitTool = createGitTool(process.cwd());
224
+ export const gitTool = createGitTool({
225
+ cwd: process.cwd(),
226
+ hasUI: false,
227
+ rulebookRules: [],
228
+ getSessionFile: () => null,
229
+ getSessionSpawns: () => null,
230
+ })!;
@@ -9,6 +9,7 @@ import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/t
9
9
  import grepDescription from "../../prompts/tools/grep.md" with { type: "text" };
10
10
  import { ensureTool } from "../../utils/tools-manager";
11
11
  import type { RenderResultOptions } from "../custom-tools/types";
12
+ import type { ToolSession } from "./index";
12
13
  import { resolveToCwd } from "./path-utils";
13
14
  import {
14
15
  formatCount,
@@ -79,7 +80,7 @@ export interface GrepToolDetails {
79
80
  error?: string;
80
81
  }
81
82
 
82
- export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
83
+ export function createGrepTool(session: ToolSession): AgentTool<typeof grepSchema> {
83
84
  return {
84
85
  name: "grep",
85
86
  label: "Grep",
@@ -127,9 +128,9 @@ export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
127
128
  throw new Error("ripgrep (rg) is not available and could not be downloaded");
128
129
  }
129
130
 
130
- const searchPath = resolveToCwd(searchDir || ".", cwd);
131
+ const searchPath = resolveToCwd(searchDir || ".", session.cwd);
131
132
  const scopePath = (() => {
132
- const relative = nodePath.relative(cwd, searchPath).replace(/\\/g, "/");
133
+ const relative = nodePath.relative(session.cwd, searchPath).replace(/\\/g, "/");
133
134
  return relative.length === 0 ? "." : relative;
134
135
  })();
135
136
  let searchStat: Stats;
@@ -595,9 +596,6 @@ export function createGrepTool(cwd: string): AgentTool<typeof grepSchema> {
595
596
  };
596
597
  }
597
598
 
598
- /** Default grep tool using process.cwd() - for backwards compatibility */
599
- export const grepTool = createGrepTool(process.cwd());
600
-
601
599
  // =============================================================================
602
600
  // TUI Renderer
603
601
  // =============================================================================
@@ -681,10 +679,7 @@ export const grepToolRenderer = {
681
679
  }
682
680
 
683
681
  if (remaining > 0) {
684
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
685
- "muted",
686
- formatMoreItems(remaining, "item", uiTheme),
687
- )}`;
682
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", formatMoreItems(remaining, "item", uiTheme))}`;
688
683
  }
689
684
 
690
685
  return new Text(text, 0, 0);
@@ -715,10 +710,7 @@ export const grepToolRenderer = {
715
710
  const hasMoreFiles = fileEntries.length > maxFiles;
716
711
  const expandHint = formatExpandHint(expanded, hasMoreFiles, uiTheme);
717
712
 
718
- let text = `${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(
719
- truncated,
720
- uiTheme,
721
- )}${scopeLabel}${expandHint}`;
713
+ let text = `${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(truncated, uiTheme)}${scopeLabel}${expandHint}`;
722
714
 
723
715
  const truncationReasons: string[] = [];
724
716
  if (details?.matchLimitReached) {
@@ -764,10 +756,7 @@ export const grepToolRenderer = {
764
756
  }
765
757
 
766
758
  if (hasTruncation) {
767
- text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
768
- "warning",
769
- `truncated: ${truncationReasons.join(", ")}`,
770
- )}`;
759
+ text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`)}`;
771
760
  }
772
761
 
773
762
  return new Text(text, 0, 0);
@@ -0,0 +1,180 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { BUILTIN_TOOLS, createTools, HIDDEN_TOOLS, type ToolSession } from "./index";
3
+
4
+ function createTestSession(overrides: Partial<ToolSession> = {}): ToolSession {
5
+ return {
6
+ cwd: "/tmp/test",
7
+ hasUI: false,
8
+ rulebookRules: [],
9
+ getSessionFile: () => null,
10
+ getSessionSpawns: () => "*",
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ describe("createTools", () => {
16
+ it("creates all builtin tools by default", async () => {
17
+ const session = createTestSession();
18
+ const tools = await createTools(session);
19
+ const names = tools.map((t) => t.name);
20
+
21
+ // Core tools should always be present
22
+ expect(names).toContain("bash");
23
+ expect(names).toContain("read");
24
+ expect(names).toContain("edit");
25
+ expect(names).toContain("write");
26
+ expect(names).toContain("grep");
27
+ expect(names).toContain("find");
28
+ expect(names).toContain("ls");
29
+ expect(names).toContain("lsp");
30
+ expect(names).toContain("notebook");
31
+ expect(names).toContain("task");
32
+ expect(names).toContain("output");
33
+ expect(names).toContain("web_fetch");
34
+ expect(names).toContain("web_search");
35
+ });
36
+
37
+ it("respects requested tool subset", async () => {
38
+ const session = createTestSession();
39
+ const tools = await createTools(session, ["read", "write"]);
40
+ const names = tools.map((t) => t.name);
41
+
42
+ expect(names).toEqual(["read", "write"]);
43
+ });
44
+
45
+ it("includes hidden tools when explicitly requested", async () => {
46
+ const session = createTestSession();
47
+ const tools = await createTools(session, ["report_finding"]);
48
+ const names = tools.map((t) => t.name);
49
+
50
+ expect(names).toEqual(["report_finding"]);
51
+ });
52
+
53
+ it("excludes ask tool when hasUI is false", async () => {
54
+ const session = createTestSession({ hasUI: false });
55
+ const tools = await createTools(session);
56
+ const names = tools.map((t) => t.name);
57
+
58
+ expect(names).not.toContain("ask");
59
+ });
60
+
61
+ it("includes ask tool when hasUI is true", async () => {
62
+ const session = createTestSession({ hasUI: true });
63
+ const tools = await createTools(session);
64
+ const names = tools.map((t) => t.name);
65
+
66
+ expect(names).toContain("ask");
67
+ });
68
+
69
+ it("excludes rulebook tool when no rules provided", async () => {
70
+ const session = createTestSession({ rulebookRules: [] });
71
+ const tools = await createTools(session);
72
+ const names = tools.map((t) => t.name);
73
+
74
+ expect(names).not.toContain("rulebook");
75
+ });
76
+
77
+ it("includes rulebook tool when rules provided", async () => {
78
+ const session = createTestSession({
79
+ rulebookRules: [
80
+ {
81
+ path: "/test/rule.md",
82
+ name: "Test Rule",
83
+ content: "Test content",
84
+ description: "A test rule",
85
+ _source: { provider: "test", providerName: "Test", path: "/test", level: "project" },
86
+ },
87
+ ],
88
+ });
89
+ const tools = await createTools(session);
90
+ const names = tools.map((t) => t.name);
91
+
92
+ expect(names).toContain("rulebook");
93
+ });
94
+
95
+ it("excludes git tool when disabled in settings", async () => {
96
+ const session = createTestSession({
97
+ settings: {
98
+ getImageAutoResize: () => true,
99
+ getLspFormatOnWrite: () => true,
100
+ getLspDiagnosticsOnWrite: () => true,
101
+ getLspDiagnosticsOnEdit: () => false,
102
+ getEditFuzzyMatch: () => true,
103
+ getGitToolEnabled: () => false,
104
+ getBashInterceptorEnabled: () => true,
105
+ },
106
+ });
107
+ const tools = await createTools(session);
108
+ const names = tools.map((t) => t.name);
109
+
110
+ expect(names).not.toContain("git");
111
+ });
112
+
113
+ it("includes git tool when enabled in settings", async () => {
114
+ const session = createTestSession({
115
+ settings: {
116
+ getImageAutoResize: () => true,
117
+ getLspFormatOnWrite: () => true,
118
+ getLspDiagnosticsOnWrite: () => true,
119
+ getLspDiagnosticsOnEdit: () => false,
120
+ getEditFuzzyMatch: () => true,
121
+ getGitToolEnabled: () => true,
122
+ getBashInterceptorEnabled: () => true,
123
+ },
124
+ });
125
+ const tools = await createTools(session);
126
+ const names = tools.map((t) => t.name);
127
+
128
+ expect(names).toContain("git");
129
+ });
130
+
131
+ it("includes git tool when no settings provided (default enabled)", async () => {
132
+ const session = createTestSession({ settings: undefined });
133
+ const tools = await createTools(session);
134
+ const names = tools.map((t) => t.name);
135
+
136
+ expect(names).toContain("git");
137
+ });
138
+
139
+ it("always includes output tool when task tool is present", async () => {
140
+ const session = createTestSession();
141
+ const tools = await createTools(session);
142
+ const names = tools.map((t) => t.name);
143
+
144
+ // Both should be present together
145
+ expect(names).toContain("task");
146
+ expect(names).toContain("output");
147
+ });
148
+
149
+ it("BUILTIN_TOOLS contains all expected tools", () => {
150
+ const expectedTools = [
151
+ "ask",
152
+ "bash",
153
+ "edit",
154
+ "find",
155
+ "git",
156
+ "grep",
157
+ "ls",
158
+ "lsp",
159
+ "notebook",
160
+ "output",
161
+ "read",
162
+ "rulebook",
163
+ "task",
164
+ "web_fetch",
165
+ "web_search",
166
+ "write",
167
+ ];
168
+
169
+ for (const tool of expectedTools) {
170
+ expect(BUILTIN_TOOLS).toHaveProperty(tool);
171
+ }
172
+
173
+ // Ensure we haven't missed any
174
+ expect(Object.keys(BUILTIN_TOOLS).sort()).toEqual(expectedTools.sort());
175
+ });
176
+
177
+ it("HIDDEN_TOOLS contains review tools", () => {
178
+ expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["report_finding", "submit_review"]);
179
+ });
180
+ });