@oh-my-pi/pi-coding-agent 3.20.1 → 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.
- package/CHANGELOG.md +107 -8
- package/docs/custom-tools.md +3 -3
- package/docs/extensions.md +226 -220
- package/docs/hooks.md +2 -2
- package/docs/sdk.md +50 -53
- package/examples/custom-tools/README.md +2 -17
- package/examples/extensions/README.md +76 -74
- package/examples/extensions/todo.ts +2 -5
- package/examples/hooks/custom-compaction.ts +2 -4
- package/examples/hooks/handoff.ts +1 -1
- package/examples/hooks/qna.ts +1 -1
- package/examples/sdk/02-custom-model.ts +1 -1
- package/examples/sdk/README.md +7 -11
- package/package.json +6 -6
- package/src/cli/args.ts +9 -6
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/list-models.ts +1 -1
- package/src/core/agent-session.ts +16 -5
- package/src/core/auth-storage.ts +1 -1
- package/src/core/compaction/branch-summarization.ts +2 -2
- package/src/core/compaction/compaction.ts +2 -2
- package/src/core/compaction/utils.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -1
- package/src/core/custom-tools/wrapper.ts +0 -1
- package/src/core/extensions/index.ts +1 -6
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +1 -1
- package/src/core/extensions/wrapper.ts +1 -8
- package/src/core/file-mentions.ts +5 -8
- package/src/core/hooks/runner.ts +2 -2
- package/src/core/hooks/types.ts +1 -1
- package/src/core/messages.ts +1 -1
- package/src/core/model-registry.ts +1 -1
- package/src/core/model-resolver.ts +1 -1
- package/src/core/sdk.ts +64 -105
- package/src/core/session-manager.ts +18 -22
- package/src/core/settings-manager.ts +66 -1
- package/src/core/slash-commands.ts +12 -5
- package/src/core/system-prompt.ts +49 -36
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/ask.ts +98 -4
- package/src/core/tools/bash-interceptor.ts +11 -4
- package/src/core/tools/bash.ts +121 -5
- package/src/core/tools/context.ts +7 -0
- package/src/core/tools/edit-diff.ts +73 -24
- package/src/core/tools/edit.ts +221 -34
- package/src/core/tools/exa/render.ts +4 -16
- package/src/core/tools/find.ts +149 -5
- package/src/core/tools/gemini-image.ts +279 -56
- package/src/core/tools/git.ts +17 -3
- package/src/core/tools/grep.ts +185 -5
- package/src/core/tools/index.test.ts +180 -0
- package/src/core/tools/index.ts +96 -242
- package/src/core/tools/ls.ts +133 -5
- package/src/core/tools/lsp/index.ts +32 -29
- package/src/core/tools/lsp/render.ts +21 -22
- package/src/core/tools/notebook.ts +112 -4
- package/src/core/tools/output.ts +175 -15
- package/src/core/tools/read.ts +127 -25
- package/src/core/tools/render-utils.ts +241 -0
- package/src/core/tools/renderers.ts +40 -828
- package/src/core/tools/review.ts +26 -25
- package/src/core/tools/rulebook.ts +11 -3
- package/src/core/tools/task/agents.ts +28 -7
- package/src/core/tools/task/discovery.ts +0 -6
- package/src/core/tools/task/executor.ts +264 -254
- package/src/core/tools/task/index.ts +48 -208
- package/src/core/tools/task/render.ts +26 -11
- package/src/core/tools/task/types.ts +7 -12
- package/src/core/tools/task/worker-protocol.ts +17 -0
- package/src/core/tools/task/worker.ts +238 -0
- package/src/core/tools/truncate.ts +27 -1
- package/src/core/tools/web-fetch.ts +25 -49
- package/src/core/tools/web-search/index.ts +132 -46
- package/src/core/tools/web-search/providers/anthropic.ts +7 -2
- package/src/core/tools/web-search/providers/exa.ts +2 -1
- package/src/core/tools/web-search/providers/perplexity.ts +6 -1
- package/src/core/tools/web-search/render.ts +6 -4
- package/src/core/tools/web-search/types.ts +13 -0
- package/src/core/tools/write.ts +96 -14
- package/src/core/voice.ts +1 -1
- package/src/discovery/helpers.test.ts +1 -1
- package/src/index.ts +5 -16
- package/src/main.ts +5 -5
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/custom-message.ts +1 -1
- package/src/modes/interactive/components/extensions/inspector-panel.ts +25 -22
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/footer.ts +1 -1
- package/src/modes/interactive/components/hook-message.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +1 -1
- package/src/modes/interactive/components/oauth-selector.ts +1 -1
- package/src/modes/interactive/components/settings-defs.ts +49 -0
- package/src/modes/interactive/components/status-line.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +93 -538
- package/src/modes/interactive/interactive-mode.ts +19 -7
- package/src/modes/interactive/theme/theme.ts +4 -4
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/prompts/system-prompt.md +4 -0
- package/src/prompts/task.md +0 -7
- package/src/prompts/tools/gemini-image.md +5 -1
- package/src/prompts/tools/output.md +6 -2
- package/src/prompts/tools/task.md +68 -0
- package/src/prompts/tools/web-fetch.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/utils/image-convert.ts +8 -2
- package/src/utils/image-magick.ts +247 -0
- package/src/utils/image-resize.ts +53 -13
- 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
|
@@ -13,11 +13,11 @@ import {
|
|
|
13
13
|
type WriteStream,
|
|
14
14
|
} from "node:fs";
|
|
15
15
|
import { basename, join, resolve } from "node:path";
|
|
16
|
+
import type { ImageContent, Message, TextContent, Usage } from "@mariozechner/pi-ai";
|
|
16
17
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
17
|
-
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
18
18
|
import { nanoid } from "nanoid";
|
|
19
|
-
import sharp from "sharp";
|
|
20
19
|
import { getAgentDir as getDefaultAgentDir } from "../config";
|
|
20
|
+
import { resizeImage } from "../utils/image-resize";
|
|
21
21
|
import {
|
|
22
22
|
type BashExecutionMessage,
|
|
23
23
|
type CustomMessage,
|
|
@@ -644,28 +644,17 @@ function isImageBlock(value: unknown): value is { type: "image"; data: string; m
|
|
|
644
644
|
|
|
645
645
|
async function compressImageForPersistence(image: ImageContent): Promise<ImageContent> {
|
|
646
646
|
try {
|
|
647
|
-
const
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const resized = await pipeline
|
|
656
|
-
.resize({
|
|
657
|
-
width: hasDims ? targetWidth : 512,
|
|
658
|
-
height: hasDims ? targetHeight : 512,
|
|
659
|
-
fit: "inside",
|
|
660
|
-
withoutEnlargement: true,
|
|
661
|
-
})
|
|
662
|
-
.jpeg({ quality: 70 })
|
|
663
|
-
.toBuffer();
|
|
664
|
-
const base64 = resized.toString("base64");
|
|
665
|
-
if (base64.length > MAX_PERSIST_CHARS) {
|
|
647
|
+
const maxBytes = Math.floor((MAX_PERSIST_CHARS * 3) / 4);
|
|
648
|
+
const resized = await resizeImage(image, {
|
|
649
|
+
maxWidth: 512,
|
|
650
|
+
maxHeight: 512,
|
|
651
|
+
maxBytes,
|
|
652
|
+
jpegQuality: 70,
|
|
653
|
+
});
|
|
654
|
+
if (resized.data.length > MAX_PERSIST_CHARS) {
|
|
666
655
|
return { type: "image", data: PLACEHOLDER_IMAGE_DATA, mimeType: "image/jpeg" };
|
|
667
656
|
}
|
|
668
|
-
return { type: "image", data:
|
|
657
|
+
return { type: "image", data: resized.data, mimeType: resized.mimeType };
|
|
669
658
|
} catch {
|
|
670
659
|
return { type: "image", data: PLACEHOLDER_IMAGE_DATA, mimeType: "image/jpeg" };
|
|
671
660
|
}
|
|
@@ -709,6 +698,13 @@ async function truncateForPersistence<T>(obj: T, key?: string): Promise<T> {
|
|
|
709
698
|
let changed = false;
|
|
710
699
|
const result: Record<string, unknown> = {};
|
|
711
700
|
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
701
|
+
// Strip transient/redundant properties that shouldn't be persisted
|
|
702
|
+
// - partialJson: streaming accumulator for tool call JSON parsing
|
|
703
|
+
// - jsonlEvents: raw subprocess streaming events (already saved to artifact files)
|
|
704
|
+
if (k === "partialJson" || k === "jsonlEvents") {
|
|
705
|
+
changed = true;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
712
708
|
const newV = await truncateForPersistence(v, k);
|
|
713
709
|
result[k] = newV;
|
|
714
710
|
if (newV !== v) changed = true;
|
|
@@ -12,6 +12,7 @@ export interface CompactionSettings {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface BranchSummarySettings {
|
|
15
|
+
enabled?: boolean; // default: false (prompt user to summarize when leaving branch)
|
|
15
16
|
reserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -61,10 +62,22 @@ export interface ExaSettings {
|
|
|
61
62
|
enableWebsets?: boolean; // default: false
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
export type WebSearchProviderOption = "auto" | "exa" | "perplexity" | "anthropic";
|
|
66
|
+
export type ImageProviderOption = "auto" | "gemini" | "openrouter";
|
|
67
|
+
|
|
68
|
+
export interface ProviderSettings {
|
|
69
|
+
webSearch?: WebSearchProviderOption; // default: "auto" (exa > perplexity > anthropic)
|
|
70
|
+
image?: ImageProviderOption; // default: "auto" (openrouter > gemini)
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
export interface BashInterceptorSettings {
|
|
65
74
|
enabled?: boolean; // default: false (blocks shell commands that have dedicated tools)
|
|
66
75
|
}
|
|
67
76
|
|
|
77
|
+
export interface GitSettings {
|
|
78
|
+
enabled?: boolean; // default: false (structured git tool; use bash for git commands when disabled)
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
export interface MCPSettings {
|
|
69
82
|
enableProjectConfig?: boolean; // default: true (load .mcp.json from project root)
|
|
70
83
|
}
|
|
@@ -166,11 +179,13 @@ export interface Settings {
|
|
|
166
179
|
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
|
|
167
180
|
exa?: ExaSettings;
|
|
168
181
|
bashInterceptor?: BashInterceptorSettings;
|
|
182
|
+
git?: GitSettings;
|
|
169
183
|
mcp?: MCPSettings;
|
|
170
184
|
lsp?: LspSettings;
|
|
171
185
|
edit?: EditSettings;
|
|
172
186
|
ttsr?: TtsrSettings;
|
|
173
187
|
voice?: VoiceSettings;
|
|
188
|
+
providers?: ProviderSettings;
|
|
174
189
|
disabledProviders?: string[]; // Discovery provider IDs that are disabled
|
|
175
190
|
disabledExtensions?: string[]; // Individual extension IDs that are disabled (e.g., "skill:commit")
|
|
176
191
|
statusLine?: StatusLineSettings; // Status line configuration
|
|
@@ -432,8 +447,21 @@ export class SettingsManager {
|
|
|
432
447
|
};
|
|
433
448
|
}
|
|
434
449
|
|
|
435
|
-
|
|
450
|
+
getBranchSummaryEnabled(): boolean {
|
|
451
|
+
return this.settings.branchSummary?.enabled ?? false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
setBranchSummaryEnabled(enabled: boolean): void {
|
|
455
|
+
if (!this.globalSettings.branchSummary) {
|
|
456
|
+
this.globalSettings.branchSummary = {};
|
|
457
|
+
}
|
|
458
|
+
this.globalSettings.branchSummary.enabled = enabled;
|
|
459
|
+
this.save();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
getBranchSummarySettings(): { enabled: boolean; reserveTokens: number } {
|
|
436
463
|
return {
|
|
464
|
+
enabled: this.getBranchSummaryEnabled(),
|
|
437
465
|
reserveTokens: this.settings.branchSummary?.reserveTokens ?? 16384,
|
|
438
466
|
};
|
|
439
467
|
}
|
|
@@ -626,6 +654,31 @@ export class SettingsManager {
|
|
|
626
654
|
this.save();
|
|
627
655
|
}
|
|
628
656
|
|
|
657
|
+
// Provider settings
|
|
658
|
+
getWebSearchProvider(): WebSearchProviderOption {
|
|
659
|
+
return this.settings.providers?.webSearch ?? "auto";
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
setWebSearchProvider(provider: WebSearchProviderOption): void {
|
|
663
|
+
if (!this.globalSettings.providers) {
|
|
664
|
+
this.globalSettings.providers = {};
|
|
665
|
+
}
|
|
666
|
+
this.globalSettings.providers.webSearch = provider;
|
|
667
|
+
this.save();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
getImageProvider(): ImageProviderOption {
|
|
671
|
+
return this.settings.providers?.image ?? "auto";
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
setImageProvider(provider: ImageProviderOption): void {
|
|
675
|
+
if (!this.globalSettings.providers) {
|
|
676
|
+
this.globalSettings.providers = {};
|
|
677
|
+
}
|
|
678
|
+
this.globalSettings.providers.image = provider;
|
|
679
|
+
this.save();
|
|
680
|
+
}
|
|
681
|
+
|
|
629
682
|
getBashInterceptorEnabled(): boolean {
|
|
630
683
|
return this.settings.bashInterceptor?.enabled ?? false;
|
|
631
684
|
}
|
|
@@ -638,6 +691,18 @@ export class SettingsManager {
|
|
|
638
691
|
this.save();
|
|
639
692
|
}
|
|
640
693
|
|
|
694
|
+
getGitToolEnabled(): boolean {
|
|
695
|
+
return this.settings.git?.enabled ?? false;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
setGitToolEnabled(enabled: boolean): void {
|
|
699
|
+
if (!this.globalSettings.git) {
|
|
700
|
+
this.globalSettings.git = {};
|
|
701
|
+
}
|
|
702
|
+
this.globalSettings.git.enabled = enabled;
|
|
703
|
+
this.save();
|
|
704
|
+
}
|
|
705
|
+
|
|
641
706
|
getMCPProjectConfigEnabled(): boolean {
|
|
642
707
|
return this.settings.mcp?.enableProjectConfig ?? true;
|
|
643
708
|
}
|
|
@@ -54,20 +54,27 @@ export function parseCommandArgs(argsString: string): string[] {
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Substitute argument placeholders in command content
|
|
57
|
-
* Supports $1, $2, ... for positional args and
|
|
57
|
+
* Supports $1, $2, ... for positional args, $@ and $ARGUMENTS for all args
|
|
58
58
|
*/
|
|
59
59
|
export function substituteArgs(content: string, args: string[]): string {
|
|
60
60
|
let result = content;
|
|
61
61
|
|
|
62
|
-
// Replace
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Replace $1, $2, etc. with positional args
|
|
62
|
+
// Replace $1, $2, etc. with positional args FIRST (before wildcards)
|
|
63
|
+
// This prevents wildcard replacement values containing $<digit> patterns from being re-substituted
|
|
66
64
|
result = result.replace(/\$(\d+)/g, (_, num) => {
|
|
67
65
|
const index = parseInt(num, 10) - 1;
|
|
68
66
|
return args[index] ?? "";
|
|
69
67
|
});
|
|
70
68
|
|
|
69
|
+
// Pre-compute all args joined
|
|
70
|
+
const allArgs = args.join(" ");
|
|
71
|
+
|
|
72
|
+
// Replace $ARGUMENTS with all args joined (aligns with Claude, Codex)
|
|
73
|
+
result = result.replace(/\$ARGUMENTS/g, allArgs);
|
|
74
|
+
|
|
75
|
+
// Replace $@ with all args joined
|
|
76
|
+
result = result.replace(/\$@/g, allArgs);
|
|
77
|
+
|
|
71
78
|
return result;
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -69,11 +69,12 @@ ${commitsText}`;
|
|
|
69
69
|
const toolDescriptions: Record<ToolName, string> = {
|
|
70
70
|
ask: "Ask user for input or clarification",
|
|
71
71
|
read: "Read file contents",
|
|
72
|
-
bash: "Execute bash commands (
|
|
72
|
+
bash: "Execute bash commands (npm, docker, etc.)",
|
|
73
73
|
edit: "Make surgical edits to files (find exact text and replace)",
|
|
74
74
|
write: "Create or overwrite files",
|
|
75
75
|
grep: "Search file contents for patterns (respects .gitignore)",
|
|
76
76
|
find: "Find files by glob pattern (respects .gitignore)",
|
|
77
|
+
git: "Structured Git operations with safety guards (status, diff, log, commit, push, pr, etc.)",
|
|
77
78
|
ls: "List directory contents",
|
|
78
79
|
lsp: "PREFERRED for semantic code queries: go-to-definition, find-all-references, hover (type info), call hierarchy. Returns precise, deterministic results. Use BEFORE grep for symbol lookups.",
|
|
79
80
|
notebook: "Edit Jupyter notebook cells",
|
|
@@ -99,9 +100,10 @@ function generateAntiBashRules(tools: ToolName[]): string | null {
|
|
|
99
100
|
const hasLs = tools.includes("ls");
|
|
100
101
|
const hasEdit = tools.includes("edit");
|
|
101
102
|
const hasLsp = tools.includes("lsp");
|
|
103
|
+
const hasGit = tools.includes("git");
|
|
102
104
|
|
|
103
105
|
// Only show rules if we have specialized tools that should be preferred
|
|
104
|
-
const hasSpecializedTools = hasRead || hasGrep || hasFind || hasLs || hasEdit;
|
|
106
|
+
const hasSpecializedTools = hasRead || hasGrep || hasFind || hasLs || hasEdit || hasGit;
|
|
105
107
|
if (!hasSpecializedTools) return null;
|
|
106
108
|
|
|
107
109
|
const lines: string[] = [];
|
|
@@ -114,6 +116,7 @@ function generateAntiBashRules(tools: ToolName[]): string | null {
|
|
|
114
116
|
if (hasFind) lines.push("- **File finding**: Use `find` instead of find/fd/locate");
|
|
115
117
|
if (hasLs) lines.push("- **Directory listing**: Use `ls` instead of bash ls");
|
|
116
118
|
if (hasEdit) lines.push("- **File editing**: Use `edit` instead of sed/awk/perl -pi/echo >/cat <<EOF");
|
|
119
|
+
if (hasGit) lines.push("- **Git operations**: Use `git` tool instead of bash git commands");
|
|
117
120
|
|
|
118
121
|
lines.push("\n### Tool Preference (highest → lowest priority)");
|
|
119
122
|
const ladder: string[] = [];
|
|
@@ -122,7 +125,8 @@ function generateAntiBashRules(tools: ToolName[]): string | null {
|
|
|
122
125
|
if (hasFind) ladder.push("find (locate files by pattern)");
|
|
123
126
|
if (hasRead) ladder.push("read (view file contents)");
|
|
124
127
|
if (hasEdit) ladder.push("edit (precise text replacement)");
|
|
125
|
-
ladder.push("
|
|
128
|
+
if (hasGit) ladder.push("git (structured git operations with safety guards)");
|
|
129
|
+
ladder.push(`bash (ONLY for ${hasGit ? "" : "git, "}npm, docker, make, cargo, etc.)`);
|
|
126
130
|
lines.push(ladder.map((t, i) => `${i + 1}. ${t}`).join("\n"));
|
|
127
131
|
|
|
128
132
|
// Add LSP guidance if available
|
|
@@ -137,6 +141,26 @@ function generateAntiBashRules(tools: ToolName[]): string | null {
|
|
|
137
141
|
lines.push("- **Find symbol across codebase** → `lsp workspace_symbols`\n");
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
// Add Git guidance if available
|
|
145
|
+
if (hasGit) {
|
|
146
|
+
lines.push("\n### Git Tool — Preferred for Git Operations");
|
|
147
|
+
lines.push("Use `git` instead of bash git when you need:");
|
|
148
|
+
lines.push(
|
|
149
|
+
"- **Status/diff/log**: `git { operation: 'status' }`, `git { operation: 'diff' }`, `git { operation: 'log' }`",
|
|
150
|
+
);
|
|
151
|
+
lines.push(
|
|
152
|
+
"- **Commit workflow**: `git { operation: 'add', paths: [...] }` then `git { operation: 'commit', message: '...' }`",
|
|
153
|
+
);
|
|
154
|
+
lines.push("- **Branching**: `git { operation: 'branch', action: 'create', name: '...' }`");
|
|
155
|
+
lines.push("- **GitHub PRs**: `git { operation: 'pr', action: 'create', title: '...', body: '...' }`");
|
|
156
|
+
lines.push(
|
|
157
|
+
"- **GitHub Issues**: `git { operation: 'issue', action: 'list' }` or `{ operation: 'issue', number: 123 }`",
|
|
158
|
+
);
|
|
159
|
+
lines.push(
|
|
160
|
+
"The git tool provides typed output, safety guards, and a clean API for all git and GitHub operations.\n",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
140
164
|
// Add search-first protocol
|
|
141
165
|
if (hasGrep || hasFind) {
|
|
142
166
|
lines.push("\n### Search-First Protocol");
|
|
@@ -232,10 +256,10 @@ export function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): st
|
|
|
232
256
|
export interface BuildSystemPromptOptions {
|
|
233
257
|
/** Custom system prompt (replaces default). */
|
|
234
258
|
customPrompt?: string;
|
|
235
|
-
/** Tools to include in prompt.
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
|
|
259
|
+
/** Tools to include in prompt. */
|
|
260
|
+
tools?: Map<string, { description: string; label: string }>;
|
|
261
|
+
/** Tool names to include in prompt. */
|
|
262
|
+
toolNames?: string[];
|
|
239
263
|
/** Text to append to system prompt. */
|
|
240
264
|
appendSystemPrompt?: string;
|
|
241
265
|
/** Skills settings for discovery. */
|
|
@@ -247,21 +271,21 @@ export interface BuildSystemPromptOptions {
|
|
|
247
271
|
/** Pre-loaded skills (skips discovery if provided). */
|
|
248
272
|
skills?: Skill[];
|
|
249
273
|
/** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
|
|
250
|
-
|
|
274
|
+
rules?: Rule[];
|
|
251
275
|
}
|
|
252
276
|
|
|
253
277
|
/** Build the system prompt with tools, guidelines, and context */
|
|
254
278
|
export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
|
|
255
279
|
const {
|
|
256
280
|
customPrompt,
|
|
257
|
-
|
|
258
|
-
extraToolDescriptions = [],
|
|
281
|
+
tools,
|
|
259
282
|
appendSystemPrompt,
|
|
260
283
|
skillsSettings,
|
|
284
|
+
toolNames,
|
|
261
285
|
cwd,
|
|
262
286
|
contextFiles: providedContextFiles,
|
|
263
287
|
skills: providedSkills,
|
|
264
|
-
rulebookRules,
|
|
288
|
+
rules: rulebookRules,
|
|
265
289
|
} = options;
|
|
266
290
|
const resolvedCwd = cwd ?? process.cwd();
|
|
267
291
|
const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
|
|
@@ -287,6 +311,9 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
287
311
|
// Resolve context files: use provided or discover
|
|
288
312
|
const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
|
|
289
313
|
|
|
314
|
+
// Build tools list based on selected tools
|
|
315
|
+
const toolsList = toolNames?.map((name) => `- ${name}: ${toolDescriptions[name as ToolName]}`).join("\n") ?? "";
|
|
316
|
+
|
|
290
317
|
// Resolve skills: use provided or discover
|
|
291
318
|
const skills =
|
|
292
319
|
providedSkills ??
|
|
@@ -311,9 +338,11 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
311
338
|
}
|
|
312
339
|
|
|
313
340
|
// Append custom tool descriptions if provided
|
|
314
|
-
if (
|
|
315
|
-
prompt += "\n\n#
|
|
316
|
-
prompt +=
|
|
341
|
+
if (tools && tools.size > 0) {
|
|
342
|
+
prompt += "\n\n# Tools\n\n";
|
|
343
|
+
prompt += Array.from(tools.entries())
|
|
344
|
+
.map(([name, { description }]) => `- ${name}: ${description}`)
|
|
345
|
+
.join("\n");
|
|
317
346
|
}
|
|
318
347
|
|
|
319
348
|
// Append git context if in a git repo
|
|
@@ -323,8 +352,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
323
352
|
}
|
|
324
353
|
|
|
325
354
|
// Append skills section (only if read tool is available)
|
|
326
|
-
|
|
327
|
-
if (customPromptHasRead && skills.length > 0) {
|
|
355
|
+
if (tools?.has("read") && skills.length > 0) {
|
|
328
356
|
prompt += formatSkillsForPrompt(skills);
|
|
329
357
|
}
|
|
330
358
|
|
|
@@ -345,25 +373,16 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
345
373
|
const docsPath = getDocsPath();
|
|
346
374
|
const examplesPath = getExamplesPath();
|
|
347
375
|
|
|
348
|
-
// Build tools list based on selected tools
|
|
349
|
-
const tools = selectedTools || (["read", "bash", "edit", "write"] as ToolName[]);
|
|
350
|
-
const builtInToolsList = tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n");
|
|
351
|
-
const extraToolsList =
|
|
352
|
-
extraToolDescriptions.length > 0
|
|
353
|
-
? extraToolDescriptions.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n")
|
|
354
|
-
: "";
|
|
355
|
-
const toolsList = [builtInToolsList, extraToolsList].filter(Boolean).join("\n");
|
|
356
|
-
|
|
357
376
|
// Generate anti-bash rules (returns null if not applicable)
|
|
358
|
-
const antiBashSection = generateAntiBashRules(tools);
|
|
377
|
+
const antiBashSection = generateAntiBashRules(Array.from(tools?.keys() ?? []));
|
|
359
378
|
|
|
360
379
|
// Build guidelines based on which tools are actually available
|
|
361
380
|
const guidelinesList: string[] = [];
|
|
362
381
|
|
|
363
|
-
const hasBash = tools
|
|
364
|
-
const hasEdit = tools
|
|
365
|
-
const hasWrite = tools
|
|
366
|
-
const hasRead = tools
|
|
382
|
+
const hasBash = tools?.has("bash");
|
|
383
|
+
const hasEdit = tools?.has("edit");
|
|
384
|
+
const hasWrite = tools?.has("write");
|
|
385
|
+
const hasRead = tools?.has("read");
|
|
367
386
|
|
|
368
387
|
// Read-only mode notice (no bash, edit, or write)
|
|
369
388
|
if (!hasBash && !hasEdit && !hasWrite) {
|
|
@@ -430,12 +449,6 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
430
449
|
}
|
|
431
450
|
}
|
|
432
451
|
|
|
433
|
-
// Append custom tool descriptions if provided
|
|
434
|
-
if (extraToolDescriptions.length > 0) {
|
|
435
|
-
prompt += "\n\n# Additional Tools\n\n";
|
|
436
|
-
prompt += extraToolDescriptions.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
437
|
-
}
|
|
438
|
-
|
|
439
452
|
// Append git context if in a git repo
|
|
440
453
|
const gitContext = loadGitContext(resolvedCwd);
|
|
441
454
|
if (gitContext) {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Generate session titles using a smol, fast model.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Model } from "@
|
|
6
|
-
import { completeSimple } from "@
|
|
5
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
6
|
+
import { completeSimple } from "@mariozechner/pi-ai";
|
|
7
7
|
import titleSystemPrompt from "../prompts/title-system.md" with { type: "text" };
|
|
8
8
|
import { logger } from "./logger";
|
|
9
9
|
import type { ModelRegistry } from "./model-registry";
|
package/src/core/tools/ask.ts
CHANGED
|
@@ -16,9 +16,14 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
20
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
19
21
|
import { Type } from "@sinclair/typebox";
|
|
20
|
-
import { theme } from "../../modes/interactive/theme/theme";
|
|
22
|
+
import { type Theme, theme } from "../../modes/interactive/theme/theme";
|
|
21
23
|
import askDescription from "../../prompts/tools/ask.md" with { type: "text" };
|
|
24
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
25
|
+
import type { ToolSession } from "./index";
|
|
26
|
+
import { formatErrorMessage, formatMeta } from "./render-utils";
|
|
22
27
|
|
|
23
28
|
// =============================================================================
|
|
24
29
|
// Types
|
|
@@ -63,7 +68,10 @@ function getDoneOptionLabel(): string {
|
|
|
63
68
|
// Tool Implementation
|
|
64
69
|
// =============================================================================
|
|
65
70
|
|
|
66
|
-
export function createAskTool(
|
|
71
|
+
export function createAskTool(session: ToolSession): null | AgentTool<typeof askSchema, AskToolDetails> {
|
|
72
|
+
if (!session.hasUI) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
67
75
|
return {
|
|
68
76
|
name: "ask",
|
|
69
77
|
label: "Ask",
|
|
@@ -189,5 +197,91 @@ export function createAskTool(_cwd: string): AgentTool<typeof askSchema, AskTool
|
|
|
189
197
|
};
|
|
190
198
|
}
|
|
191
199
|
|
|
192
|
-
/** Default ask tool
|
|
193
|
-
export const askTool = createAskTool(
|
|
200
|
+
/** Default ask tool - returns null when no UI */
|
|
201
|
+
export const askTool = createAskTool({
|
|
202
|
+
cwd: process.cwd(),
|
|
203
|
+
hasUI: false,
|
|
204
|
+
rulebookRules: [],
|
|
205
|
+
getSessionFile: () => null,
|
|
206
|
+
getSessionSpawns: () => "*",
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// =============================================================================
|
|
210
|
+
// TUI Renderer
|
|
211
|
+
// =============================================================================
|
|
212
|
+
|
|
213
|
+
interface AskRenderArgs {
|
|
214
|
+
question: string;
|
|
215
|
+
options?: Array<{ label: string }>;
|
|
216
|
+
multi?: boolean;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export const askToolRenderer = {
|
|
220
|
+
renderCall(args: AskRenderArgs, uiTheme: Theme): Component {
|
|
221
|
+
if (!args.question) {
|
|
222
|
+
return new Text(formatErrorMessage("No question provided", uiTheme), 0, 0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const label = uiTheme.fg("toolTitle", uiTheme.bold("Ask"));
|
|
226
|
+
let text = `${label} ${uiTheme.fg("accent", args.question)}`;
|
|
227
|
+
|
|
228
|
+
const meta: string[] = [];
|
|
229
|
+
if (args.multi) meta.push("multi");
|
|
230
|
+
if (args.options?.length) meta.push(`options:${args.options.length}`);
|
|
231
|
+
text += formatMeta(meta, uiTheme);
|
|
232
|
+
|
|
233
|
+
if (args.options?.length) {
|
|
234
|
+
for (let i = 0; i < args.options.length; i++) {
|
|
235
|
+
const opt = args.options[i];
|
|
236
|
+
const isLast = i === args.options.length - 1;
|
|
237
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
238
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${uiTheme.fg("muted", opt.label)}`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return new Text(text, 0, 0);
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
renderResult(
|
|
246
|
+
result: { content: Array<{ type: string; text?: string }>; details?: AskToolDetails },
|
|
247
|
+
_opts: RenderResultOptions,
|
|
248
|
+
uiTheme: Theme,
|
|
249
|
+
): Component {
|
|
250
|
+
const { details } = result;
|
|
251
|
+
if (!details) {
|
|
252
|
+
const txt = result.content[0];
|
|
253
|
+
return new Text(txt?.type === "text" && txt.text ? txt.text : "", 0, 0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const hasSelection = details.customInput || details.selectedOptions.length > 0;
|
|
257
|
+
const statusIcon = hasSelection
|
|
258
|
+
? uiTheme.styledSymbol("status.success", "success")
|
|
259
|
+
: uiTheme.styledSymbol("status.warning", "warning");
|
|
260
|
+
|
|
261
|
+
let text = `${statusIcon} ${uiTheme.fg("accent", details.question)}`;
|
|
262
|
+
|
|
263
|
+
if (details.customInput) {
|
|
264
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol(
|
|
265
|
+
"status.success",
|
|
266
|
+
"success",
|
|
267
|
+
)} ${uiTheme.fg("toolOutput", details.customInput)}`;
|
|
268
|
+
} else if (details.selectedOptions.length > 0) {
|
|
269
|
+
const selected = details.selectedOptions;
|
|
270
|
+
for (let i = 0; i < selected.length; i++) {
|
|
271
|
+
const isLast = i === selected.length - 1;
|
|
272
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
273
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg(
|
|
274
|
+
"success",
|
|
275
|
+
uiTheme.checkbox.checked,
|
|
276
|
+
)} ${uiTheme.fg("toolOutput", selected[i])}`;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol(
|
|
280
|
+
"status.warning",
|
|
281
|
+
"warning",
|
|
282
|
+
)} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return new Text(text, 0, 0);
|
|
286
|
+
},
|
|
287
|
+
};
|
|
@@ -36,6 +36,13 @@ const forbiddenPatterns: Array<{
|
|
|
36
36
|
tool: "grep",
|
|
37
37
|
message: "Use the `grep` tool instead of grep/rg. It respects .gitignore and provides structured output.",
|
|
38
38
|
},
|
|
39
|
+
// Git operations
|
|
40
|
+
{
|
|
41
|
+
pattern: /^\s*git(\s+|$)/,
|
|
42
|
+
tool: "git",
|
|
43
|
+
message:
|
|
44
|
+
"Use the `git` tool instead of running git in bash. It provides structured output and safety confirmations.",
|
|
45
|
+
},
|
|
39
46
|
// File finding
|
|
40
47
|
{
|
|
41
48
|
pattern: /^\s*(find|fd|locate)\s+.*(-name|-iname|-type|--type|-glob)/,
|
|
@@ -73,13 +80,13 @@ const forbiddenPatterns: Array<{
|
|
|
73
80
|
* @param availableTools Set of tool names that are available
|
|
74
81
|
* @returns InterceptionResult indicating if the command should be blocked
|
|
75
82
|
*/
|
|
76
|
-
export function checkBashInterception(command: string, availableTools:
|
|
83
|
+
export function checkBashInterception(command: string, availableTools: string[]): InterceptionResult {
|
|
77
84
|
// Normalize command for pattern matching
|
|
78
85
|
const normalizedCommand = command.trim();
|
|
79
86
|
|
|
80
87
|
for (const { pattern, tool, message } of forbiddenPatterns) {
|
|
81
88
|
// Only block if the suggested tool is actually available
|
|
82
|
-
if (!availableTools.
|
|
89
|
+
if (!availableTools.includes(tool)) {
|
|
83
90
|
continue;
|
|
84
91
|
}
|
|
85
92
|
|
|
@@ -99,8 +106,8 @@ export function checkBashInterception(command: string, availableTools: Set<strin
|
|
|
99
106
|
* Check if a command is a simple directory listing that should use `ls` tool.
|
|
100
107
|
* Only applies to bare `ls` without complex flags.
|
|
101
108
|
*/
|
|
102
|
-
export function checkSimpleLsInterception(command: string, availableTools:
|
|
103
|
-
if (!availableTools.
|
|
109
|
+
export function checkSimpleLsInterception(command: string, availableTools: string[]): InterceptionResult {
|
|
110
|
+
if (!availableTools.includes("ls")) {
|
|
104
111
|
return { block: false };
|
|
105
112
|
}
|
|
106
113
|
|