@oh-my-pi/pi-coding-agent 3.21.0 → 3.25.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 +55 -1
- package/docs/sdk.md +47 -50
- package/examples/custom-tools/README.md +0 -15
- package/examples/hooks/custom-compaction.ts +1 -3
- package/examples/sdk/README.md +6 -10
- package/package.json +5 -5
- package/src/cli/args.ts +9 -6
- package/src/core/agent-session.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +3 -0
- package/src/core/custom-tools/wrapper.ts +0 -1
- package/src/core/extensions/index.ts +1 -6
- package/src/core/extensions/wrapper.ts +0 -7
- package/src/core/file-mentions.ts +5 -8
- package/src/core/sdk.ts +48 -111
- package/src/core/session-manager.ts +7 -0
- package/src/core/system-prompt.ts +22 -33
- package/src/core/tools/ask.ts +14 -7
- package/src/core/tools/bash-interceptor.ts +4 -4
- package/src/core/tools/bash.ts +19 -9
- package/src/core/tools/complete.ts +131 -0
- package/src/core/tools/context.ts +7 -0
- package/src/core/tools/edit.ts +8 -15
- package/src/core/tools/exa/render.ts +4 -16
- package/src/core/tools/find.ts +7 -18
- package/src/core/tools/git.ts +13 -3
- package/src/core/tools/grep.ts +7 -18
- package/src/core/tools/index.test.ts +188 -0
- package/src/core/tools/index.ts +106 -236
- package/src/core/tools/jtd-to-json-schema.ts +274 -0
- package/src/core/tools/ls.ts +4 -9
- package/src/core/tools/lsp/index.ts +32 -29
- package/src/core/tools/lsp/render.ts +7 -28
- package/src/core/tools/notebook.ts +3 -5
- package/src/core/tools/output.ts +130 -31
- package/src/core/tools/read.ts +8 -19
- package/src/core/tools/review.ts +0 -18
- package/src/core/tools/rulebook.ts +8 -2
- package/src/core/tools/task/agents.ts +28 -7
- package/src/core/tools/task/artifacts.ts +6 -9
- package/src/core/tools/task/discovery.ts +0 -6
- package/src/core/tools/task/executor.ts +306 -257
- package/src/core/tools/task/index.ts +65 -235
- package/src/core/tools/task/name-generator.ts +247 -0
- package/src/core/tools/task/render.ts +158 -19
- package/src/core/tools/task/types.ts +13 -11
- package/src/core/tools/task/worker-protocol.ts +18 -0
- package/src/core/tools/task/worker.ts +270 -0
- package/src/core/tools/web-fetch.ts +4 -36
- package/src/core/tools/web-search/index.ts +2 -1
- package/src/core/tools/web-search/render.ts +1 -4
- package/src/core/tools/write.ts +7 -15
- package/src/discovery/helpers.test.ts +1 -1
- package/src/index.ts +5 -16
- package/src/main.ts +4 -4
- package/src/modes/interactive/theme/theme.ts +4 -4
- package/src/prompts/task.md +14 -57
- package/src/prompts/tools/output.md +4 -3
- package/src/prompts/tools/task.md +70 -0
- package/examples/custom-tools/question/index.ts +0 -84
- package/examples/custom-tools/subagent/README.md +0 -172
- package/examples/custom-tools/subagent/agents/planner.md +0 -37
- package/examples/custom-tools/subagent/agents/scout.md +0 -50
- package/examples/custom-tools/subagent/agents/worker.md +0 -24
- package/examples/custom-tools/subagent/agents.ts +0 -156
- package/examples/custom-tools/subagent/commands/implement-and-review.md +0 -10
- package/examples/custom-tools/subagent/commands/implement.md +0 -10
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +0 -9
- package/examples/custom-tools/subagent/index.ts +0 -1002
- package/examples/sdk/05-tools.ts +0 -94
- package/examples/sdk/12-full-control.ts +0 -95
- package/src/prompts/browser.md +0 -71
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete tool for structured subagent output.
|
|
3
|
+
*
|
|
4
|
+
* Subagents must call this tool to finish and return structured JSON output.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
|
|
10
|
+
import type { ToolSession } from "./index";
|
|
11
|
+
import { jtdToJsonSchema } from "./jtd-to-json-schema";
|
|
12
|
+
import { subprocessToolRegistry } from "./task/subprocess-tool-registry";
|
|
13
|
+
|
|
14
|
+
export interface CompleteDetails {
|
|
15
|
+
data: unknown;
|
|
16
|
+
status: "success" | "aborted";
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
21
|
+
|
|
22
|
+
function normalizeSchema(schema: unknown): { normalized?: unknown; error?: string } {
|
|
23
|
+
if (schema === undefined || schema === null) return {};
|
|
24
|
+
if (typeof schema === "string") {
|
|
25
|
+
try {
|
|
26
|
+
return { normalized: JSON.parse(schema) };
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { normalized: schema };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatSchema(schema: unknown): string {
|
|
35
|
+
if (schema === undefined) return "No schema provided.";
|
|
36
|
+
if (typeof schema === "string") return schema;
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(schema, null, 2);
|
|
39
|
+
} catch {
|
|
40
|
+
return "[unserializable schema]";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {
|
|
45
|
+
if (!errors || errors.length === 0) return "Unknown schema validation error.";
|
|
46
|
+
return errors
|
|
47
|
+
.map((err) => {
|
|
48
|
+
const path = err.instancePath ? `${err.instancePath}: ` : "";
|
|
49
|
+
return `${path}${err.message ?? "invalid"}`;
|
|
50
|
+
})
|
|
51
|
+
.join("; ");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createCompleteTool(session: ToolSession) {
|
|
55
|
+
const schemaResult = normalizeSchema(session.outputSchema);
|
|
56
|
+
// Convert JTD to JSON Schema if needed (auto-detected)
|
|
57
|
+
const normalizedSchema =
|
|
58
|
+
schemaResult.normalized !== undefined ? jtdToJsonSchema(schemaResult.normalized) : undefined;
|
|
59
|
+
let validate: ValidateFunction | undefined;
|
|
60
|
+
let schemaError = schemaResult.error;
|
|
61
|
+
|
|
62
|
+
if (normalizedSchema !== undefined && !schemaError) {
|
|
63
|
+
try {
|
|
64
|
+
validate = ajv.compile(normalizedSchema as any);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
schemaError = err instanceof Error ? err.message : String(err);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const schemaHint = formatSchema(normalizedSchema ?? session.outputSchema);
|
|
71
|
+
|
|
72
|
+
// Use actual schema if provided, otherwise fall back to Type.Any
|
|
73
|
+
// Merge description into the JSON schema for better tool documentation
|
|
74
|
+
const dataSchema = normalizedSchema
|
|
75
|
+
? Type.Unsafe({
|
|
76
|
+
...(normalizedSchema as object),
|
|
77
|
+
description: "Structured output matching the schema:\n" + schemaHint,
|
|
78
|
+
})
|
|
79
|
+
: Type.Any({ description: "Structured JSON output (no schema specified)" });
|
|
80
|
+
|
|
81
|
+
const completeParams = Type.Object({
|
|
82
|
+
data: dataSchema,
|
|
83
|
+
status: Type.Optional(
|
|
84
|
+
Type.Union([Type.Literal("success"), Type.Literal("aborted")], {
|
|
85
|
+
default: "success",
|
|
86
|
+
description: "Use 'aborted' if the task cannot be completed",
|
|
87
|
+
}),
|
|
88
|
+
),
|
|
89
|
+
error: Type.Optional(Type.String({ description: "Error message when status is 'aborted'" })),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const tool: AgentTool<typeof completeParams, CompleteDetails> = {
|
|
93
|
+
name: "complete",
|
|
94
|
+
label: "Complete",
|
|
95
|
+
description:
|
|
96
|
+
"Finish the task with structured JSON output. Call exactly once at the end of the task.\n\n" +
|
|
97
|
+
"If you cannot complete the task, call with status='aborted' and an error message.",
|
|
98
|
+
parameters: completeParams,
|
|
99
|
+
execute: async (_toolCallId, params) => {
|
|
100
|
+
const status = params.status ?? "success";
|
|
101
|
+
|
|
102
|
+
// Skip schema validation when aborting - the agent is giving up
|
|
103
|
+
if (status === "success") {
|
|
104
|
+
if (schemaError) {
|
|
105
|
+
throw new Error(`Invalid output schema: ${schemaError}`);
|
|
106
|
+
}
|
|
107
|
+
if (validate && !validate(params.data)) {
|
|
108
|
+
throw new Error(`Output does not match schema: ${formatAjvErrors(validate.errors)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const responseText =
|
|
113
|
+
status === "aborted"
|
|
114
|
+
? `Task aborted: ${params.error || "No reason provided"}`
|
|
115
|
+
: "Completion recorded.";
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text", text: responseText }],
|
|
119
|
+
details: { data: params.data, status, error: params.error },
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return tool;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Register subprocess tool handler for extraction + termination.
|
|
128
|
+
subprocessToolRegistry.register<CompleteDetails>("complete", {
|
|
129
|
+
extractData: (event) => event.result?.details as CompleteDetails | undefined,
|
|
130
|
+
shouldTerminate: () => true,
|
|
131
|
+
});
|
|
@@ -6,27 +6,34 @@ declare module "@oh-my-pi/pi-agent-core" {
|
|
|
6
6
|
interface AgentToolContext extends CustomToolContext {
|
|
7
7
|
ui?: ExtensionUIContext;
|
|
8
8
|
hasUI?: boolean;
|
|
9
|
+
toolNames?: string[];
|
|
9
10
|
}
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export interface ToolContextStore {
|
|
13
14
|
getContext(): AgentToolContext;
|
|
14
15
|
setUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
|
|
16
|
+
setToolNames(names: string[]): void;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export function createToolContextStore(getBaseContext: () => CustomToolContext): ToolContextStore {
|
|
18
20
|
let uiContext: ExtensionUIContext | undefined;
|
|
19
21
|
let hasUI = false;
|
|
22
|
+
let toolNames: string[] = [];
|
|
20
23
|
|
|
21
24
|
return {
|
|
22
25
|
getContext: () => ({
|
|
23
26
|
...getBaseContext(),
|
|
24
27
|
ui: uiContext,
|
|
25
28
|
hasUI,
|
|
29
|
+
toolNames,
|
|
26
30
|
}),
|
|
27
31
|
setUIContext: (context, uiAvailable) => {
|
|
28
32
|
uiContext = context;
|
|
29
33
|
hasUI = uiAvailable;
|
|
30
34
|
},
|
|
35
|
+
setToolNames: (names) => {
|
|
36
|
+
toolNames = names;
|
|
37
|
+
},
|
|
31
38
|
};
|
|
32
39
|
}
|
package/src/core/tools/edit.ts
CHANGED
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
restoreLineEndings,
|
|
18
18
|
stripBom,
|
|
19
19
|
} from "./edit-diff";
|
|
20
|
-
import
|
|
20
|
+
import type { ToolSession } from "./index";
|
|
21
|
+
import { createLspWritethrough, type FileDiagnosticsResult } from "./lsp/index";
|
|
21
22
|
import { resolveToCwd } from "./path-utils";
|
|
22
23
|
import {
|
|
23
24
|
formatDiagnostics,
|
|
@@ -46,16 +47,11 @@ export interface EditToolDetails {
|
|
|
46
47
|
diagnostics?: FileDiagnosticsResult;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
writethrough
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function createEditTool(cwd: string, options: EditToolOptions = {}): AgentTool<typeof editSchema> {
|
|
57
|
-
const allowFuzzy = options.fuzzyMatch ?? true;
|
|
58
|
-
const writethrough = options.writethrough ?? writethroughNoop;
|
|
50
|
+
export function createEditTool(session: ToolSession): AgentTool<typeof editSchema> {
|
|
51
|
+
const allowFuzzy = session.settings?.getEditFuzzyMatch() ?? true;
|
|
52
|
+
const enableDiagnostics = session.settings?.getLspDiagnosticsOnEdit() ?? false;
|
|
53
|
+
const enableFormat = session.settings?.getLspFormatOnWrite() ?? true;
|
|
54
|
+
const writethrough = createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics });
|
|
59
55
|
return {
|
|
60
56
|
name: "edit",
|
|
61
57
|
label: "Edit",
|
|
@@ -71,7 +67,7 @@ export function createEditTool(cwd: string, options: EditToolOptions = {}): Agen
|
|
|
71
67
|
throw new Error("Cannot edit Jupyter notebooks with the Edit tool. Use the NotebookEdit tool instead.");
|
|
72
68
|
}
|
|
73
69
|
|
|
74
|
-
const absolutePath = resolveToCwd(path, cwd);
|
|
70
|
+
const absolutePath = resolveToCwd(path, session.cwd);
|
|
75
71
|
|
|
76
72
|
const file = Bun.file(absolutePath);
|
|
77
73
|
if (!(await file.exists())) {
|
|
@@ -203,9 +199,6 @@ export function createEditTool(cwd: string, options: EditToolOptions = {}): Agen
|
|
|
203
199
|
};
|
|
204
200
|
}
|
|
205
201
|
|
|
206
|
-
/** Default edit tool using process.cwd() - for backwards compatibility */
|
|
207
|
-
export const editTool = createEditTool(process.cwd());
|
|
208
|
-
|
|
209
202
|
// =============================================================================
|
|
210
203
|
// TUI Renderer
|
|
211
204
|
// =============================================================================
|
|
@@ -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];
|
package/src/core/tools/find.ts
CHANGED
|
@@ -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(
|
|
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);
|
package/src/core/tools/git.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
224
|
+
export const gitTool = createGitTool({
|
|
225
|
+
cwd: process.cwd(),
|
|
226
|
+
hasUI: false,
|
|
227
|
+
rulebookRules: [],
|
|
228
|
+
getSessionFile: () => null,
|
|
229
|
+
getSessionSpawns: () => null,
|
|
230
|
+
})!;
|
package/src/core/tools/grep.ts
CHANGED
|
@@ -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(
|
|
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,188 @@
|
|
|
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("includes complete tool when required", async () => {
|
|
54
|
+
const session = createTestSession({ requireCompleteTool: true });
|
|
55
|
+
const tools = await createTools(session);
|
|
56
|
+
const names = tools.map((t) => t.name);
|
|
57
|
+
|
|
58
|
+
expect(names).toContain("complete");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("excludes ask tool when hasUI is false", async () => {
|
|
62
|
+
const session = createTestSession({ hasUI: false });
|
|
63
|
+
const tools = await createTools(session);
|
|
64
|
+
const names = tools.map((t) => t.name);
|
|
65
|
+
|
|
66
|
+
expect(names).not.toContain("ask");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("includes ask tool when hasUI is true", async () => {
|
|
70
|
+
const session = createTestSession({ hasUI: true });
|
|
71
|
+
const tools = await createTools(session);
|
|
72
|
+
const names = tools.map((t) => t.name);
|
|
73
|
+
|
|
74
|
+
expect(names).toContain("ask");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("excludes rulebook tool when no rules provided", async () => {
|
|
78
|
+
const session = createTestSession({ rulebookRules: [] });
|
|
79
|
+
const tools = await createTools(session);
|
|
80
|
+
const names = tools.map((t) => t.name);
|
|
81
|
+
|
|
82
|
+
expect(names).not.toContain("rulebook");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("includes rulebook tool when rules provided", async () => {
|
|
86
|
+
const session = createTestSession({
|
|
87
|
+
rulebookRules: [
|
|
88
|
+
{
|
|
89
|
+
path: "/test/rule.md",
|
|
90
|
+
name: "Test Rule",
|
|
91
|
+
content: "Test content",
|
|
92
|
+
description: "A test rule",
|
|
93
|
+
_source: { provider: "test", providerName: "Test", path: "/test", level: "project" },
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
const tools = await createTools(session);
|
|
98
|
+
const names = tools.map((t) => t.name);
|
|
99
|
+
|
|
100
|
+
expect(names).toContain("rulebook");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("excludes git tool when disabled in settings", async () => {
|
|
104
|
+
const session = createTestSession({
|
|
105
|
+
settings: {
|
|
106
|
+
getImageAutoResize: () => true,
|
|
107
|
+
getLspFormatOnWrite: () => true,
|
|
108
|
+
getLspDiagnosticsOnWrite: () => true,
|
|
109
|
+
getLspDiagnosticsOnEdit: () => false,
|
|
110
|
+
getEditFuzzyMatch: () => true,
|
|
111
|
+
getGitToolEnabled: () => false,
|
|
112
|
+
getBashInterceptorEnabled: () => true,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const tools = await createTools(session);
|
|
116
|
+
const names = tools.map((t) => t.name);
|
|
117
|
+
|
|
118
|
+
expect(names).not.toContain("git");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("includes git tool when enabled in settings", async () => {
|
|
122
|
+
const session = createTestSession({
|
|
123
|
+
settings: {
|
|
124
|
+
getImageAutoResize: () => true,
|
|
125
|
+
getLspFormatOnWrite: () => true,
|
|
126
|
+
getLspDiagnosticsOnWrite: () => true,
|
|
127
|
+
getLspDiagnosticsOnEdit: () => false,
|
|
128
|
+
getEditFuzzyMatch: () => true,
|
|
129
|
+
getGitToolEnabled: () => true,
|
|
130
|
+
getBashInterceptorEnabled: () => true,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const tools = await createTools(session);
|
|
134
|
+
const names = tools.map((t) => t.name);
|
|
135
|
+
|
|
136
|
+
expect(names).toContain("git");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("includes git tool when no settings provided (default enabled)", async () => {
|
|
140
|
+
const session = createTestSession({ settings: undefined });
|
|
141
|
+
const tools = await createTools(session);
|
|
142
|
+
const names = tools.map((t) => t.name);
|
|
143
|
+
|
|
144
|
+
expect(names).toContain("git");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("always includes output tool when task tool is present", async () => {
|
|
148
|
+
const session = createTestSession();
|
|
149
|
+
const tools = await createTools(session);
|
|
150
|
+
const names = tools.map((t) => t.name);
|
|
151
|
+
|
|
152
|
+
// Both should be present together
|
|
153
|
+
expect(names).toContain("task");
|
|
154
|
+
expect(names).toContain("output");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("BUILTIN_TOOLS contains all expected tools", () => {
|
|
158
|
+
const expectedTools = [
|
|
159
|
+
"ask",
|
|
160
|
+
"bash",
|
|
161
|
+
"edit",
|
|
162
|
+
"find",
|
|
163
|
+
"git",
|
|
164
|
+
"grep",
|
|
165
|
+
"ls",
|
|
166
|
+
"lsp",
|
|
167
|
+
"notebook",
|
|
168
|
+
"output",
|
|
169
|
+
"read",
|
|
170
|
+
"rulebook",
|
|
171
|
+
"task",
|
|
172
|
+
"web_fetch",
|
|
173
|
+
"web_search",
|
|
174
|
+
"write",
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
for (const tool of expectedTools) {
|
|
178
|
+
expect(BUILTIN_TOOLS).toHaveProperty(tool);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Ensure we haven't missed any
|
|
182
|
+
expect(Object.keys(BUILTIN_TOOLS).sort()).toEqual(expectedTools.sort());
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("HIDDEN_TOOLS contains review tools", () => {
|
|
186
|
+
expect(Object.keys(HIDDEN_TOOLS).sort()).toEqual(["complete", "report_finding", "submit_review"]);
|
|
187
|
+
});
|
|
188
|
+
});
|