@oh-my-pi/pi-coding-agent 3.20.1 → 3.21.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 +69 -9
- package/docs/custom-tools.md +3 -3
- package/docs/extensions.md +226 -220
- package/docs/hooks.md +2 -2
- package/docs/sdk.md +3 -3
- package/examples/custom-tools/README.md +2 -2
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/examples/extensions/README.md +76 -74
- package/examples/extensions/todo.ts +2 -5
- package/examples/hooks/custom-compaction.ts +1 -1
- 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/12-full-control.ts +1 -1
- package/examples/sdk/README.md +1 -1
- package/package.json +5 -5
- package/src/cli/file-processor.ts +1 -1
- package/src/cli/list-models.ts +1 -1
- package/src/core/agent-session.ts +13 -2
- 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/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +1 -1
- package/src/core/extensions/wrapper.ts +1 -1
- 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 +33 -4
- package/src/core/session-manager.ts +11 -22
- package/src/core/settings-manager.ts +66 -1
- package/src/core/slash-commands.ts +12 -5
- package/src/core/system-prompt.ts +27 -3
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/ask.ts +88 -1
- package/src/core/tools/bash-interceptor.ts +7 -0
- package/src/core/tools/bash.ts +106 -0
- package/src/core/tools/edit-diff.ts +73 -24
- package/src/core/tools/edit.ts +214 -20
- package/src/core/tools/find.ts +155 -0
- package/src/core/tools/gemini-image.ts +279 -56
- package/src/core/tools/git.ts +4 -0
- package/src/core/tools/grep.ts +191 -0
- package/src/core/tools/index.ts +3 -6
- package/src/core/tools/ls.ts +134 -1
- package/src/core/tools/lsp/render.ts +34 -14
- package/src/core/tools/notebook.ts +110 -0
- package/src/core/tools/output.ts +179 -7
- package/src/core/tools/read.ts +122 -9
- package/src/core/tools/render-utils.ts +241 -0
- package/src/core/tools/renderers.ts +40 -828
- package/src/core/tools/review.ts +26 -7
- package/src/core/tools/rulebook.ts +3 -1
- package/src/core/tools/task/index.ts +18 -3
- package/src/core/tools/task/render.ts +5 -0
- package/src/core/tools/task/types.ts +1 -1
- package/src/core/tools/truncate.ts +27 -1
- package/src/core/tools/web-fetch.ts +23 -15
- package/src/core/tools/web-search/index.ts +130 -45
- 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 +5 -0
- package/src/core/tools/web-search/types.ts +13 -0
- package/src/core/tools/write.ts +90 -0
- package/src/core/voice.ts +1 -1
- package/src/main.ts +1 -1
- 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/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/tools/gemini-image.md +5 -1
- package/src/prompts/tools/output.md +4 -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/src/core/sdk.ts
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { join } from "node:path";
|
|
30
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
30
31
|
import { Agent, type AgentTool, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
31
|
-
import type { Model } from "@oh-my-pi/pi-ai";
|
|
32
32
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
33
33
|
import chalk from "chalk";
|
|
34
34
|
// Import discovery to register all providers on startup
|
|
@@ -66,6 +66,7 @@ import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate
|
|
|
66
66
|
import { SessionManager } from "./session-manager";
|
|
67
67
|
import { type Settings, SettingsManager, type SkillsSettings } from "./settings-manager";
|
|
68
68
|
import { loadSkills as loadSkillsInternal, type Skill } from "./skills";
|
|
69
|
+
import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./slash-commands";
|
|
69
70
|
import {
|
|
70
71
|
buildSystemPrompt as buildSystemPromptInternal,
|
|
71
72
|
loadProjectContextFiles as loadContextFilesInternal,
|
|
@@ -100,6 +101,8 @@ import {
|
|
|
100
101
|
lsTool,
|
|
101
102
|
readOnlyTools,
|
|
102
103
|
readTool,
|
|
104
|
+
setPreferredImageProvider,
|
|
105
|
+
setPreferredWebSearchProvider,
|
|
103
106
|
type Tool,
|
|
104
107
|
type ToolName,
|
|
105
108
|
warmupLspServers,
|
|
@@ -153,6 +156,8 @@ export interface CreateAgentSessionOptions {
|
|
|
153
156
|
contextFiles?: Array<{ path: string; content: string }>;
|
|
154
157
|
/** Prompt templates. Default: discovered from cwd/.omp/prompts/ + agentDir/prompts/ */
|
|
155
158
|
promptTemplates?: PromptTemplate[];
|
|
159
|
+
/** File-based slash commands. Default: discovered from commands/ directories */
|
|
160
|
+
slashCommands?: FileSlashCommand[];
|
|
156
161
|
|
|
157
162
|
/** Enable MCP server discovery from .mcp.json files. Default: true */
|
|
158
163
|
enableMCP?: boolean;
|
|
@@ -199,6 +204,7 @@ export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResu
|
|
|
199
204
|
export type { PromptTemplate } from "./prompt-templates";
|
|
200
205
|
export type { Settings, SkillsSettings } from "./settings-manager";
|
|
201
206
|
export type { Skill } from "./skills";
|
|
207
|
+
export type { FileSlashCommand } from "./slash-commands";
|
|
202
208
|
export type { Tool } from "./tools/index";
|
|
203
209
|
|
|
204
210
|
export {
|
|
@@ -315,6 +321,13 @@ export async function discoverPromptTemplates(cwd?: string, agentDir?: string):
|
|
|
315
321
|
});
|
|
316
322
|
}
|
|
317
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Discover file-based slash commands from commands/ directories.
|
|
326
|
+
*/
|
|
327
|
+
export function discoverSlashCommands(cwd?: string): FileSlashCommand[] {
|
|
328
|
+
return loadSlashCommandsInternal({ cwd: cwd ?? process.cwd() });
|
|
329
|
+
}
|
|
330
|
+
|
|
318
331
|
/**
|
|
319
332
|
* Discover custom commands (TypeScript slash commands) from cwd and agentDir.
|
|
320
333
|
*/
|
|
@@ -480,7 +493,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
|
|
|
480
493
|
* const { session } = await createAgentSession();
|
|
481
494
|
*
|
|
482
495
|
* // With explicit model
|
|
483
|
-
* import { getModel } from '@
|
|
496
|
+
* import { getModel } from '@mariozechner/pi-ai';
|
|
484
497
|
* const { session } = await createAgentSession({
|
|
485
498
|
* model: getModel('anthropic', 'claude-opus-4-5'),
|
|
486
499
|
* thinkingLevel: 'high',
|
|
@@ -517,6 +530,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
517
530
|
initializeWithSettings(settingsManager);
|
|
518
531
|
time("initializeWithSettings");
|
|
519
532
|
|
|
533
|
+
// Initialize provider preferences from settings
|
|
534
|
+
setPreferredWebSearchProvider(settingsManager.getWebSearchProvider());
|
|
535
|
+
setPreferredImageProvider(settingsManager.getImageProvider());
|
|
536
|
+
|
|
520
537
|
const sessionManager = options.sessionManager ?? SessionManager.create(cwd);
|
|
521
538
|
time("sessionManager");
|
|
522
539
|
|
|
@@ -625,9 +642,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
625
642
|
});
|
|
626
643
|
time("createAllTools");
|
|
627
644
|
|
|
628
|
-
|
|
645
|
+
// Determine which tools to include based on settings
|
|
646
|
+
let baseToolNames = options.tools
|
|
629
647
|
? options.tools.map((t) => t.name).filter((n): n is ToolName => n in allBuiltInToolsMap)
|
|
630
|
-
: baseCodingToolNames;
|
|
648
|
+
: [...baseCodingToolNames];
|
|
649
|
+
|
|
650
|
+
// Filter out git tool if disabled in settings
|
|
651
|
+
if (!settingsManager.getGitToolEnabled()) {
|
|
652
|
+
baseToolNames = baseToolNames.filter((name) => name !== "git");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const initialActiveToolNames: ToolName[] = baseToolNames;
|
|
631
656
|
const initialActiveBuiltInTools = initialActiveToolNames.map((name) => allBuiltInToolsMap[name]);
|
|
632
657
|
|
|
633
658
|
// Discover MCP tools from .mcp.json files
|
|
@@ -897,6 +922,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
897
922
|
const promptTemplates = options.promptTemplates ?? (await discoverPromptTemplates(cwd, agentDir));
|
|
898
923
|
time("discoverPromptTemplates");
|
|
899
924
|
|
|
925
|
+
const slashCommands = options.slashCommands ?? discoverSlashCommands(cwd);
|
|
926
|
+
time("discoverSlashCommands");
|
|
927
|
+
|
|
900
928
|
const baseSetUIContext = extensionsResult.setUIContext;
|
|
901
929
|
extensionsResult.setUIContext = (uiContext, hasUI) => {
|
|
902
930
|
baseSetUIContext(uiContext, hasUI);
|
|
@@ -951,6 +979,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
951
979
|
settingsManager,
|
|
952
980
|
scopedModels: options.scopedModels,
|
|
953
981
|
promptTemplates,
|
|
982
|
+
slashCommands,
|
|
954
983
|
extensionRunner,
|
|
955
984
|
customCommands: customCommandsResult.commands,
|
|
956
985
|
skillsSettings: settingsManager.getSkillsSettings(),
|
|
@@ -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
|
}
|
|
@@ -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");
|
|
@@ -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,13 @@
|
|
|
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 { formatErrorMessage, formatMeta } from "./render-utils";
|
|
22
26
|
|
|
23
27
|
// =============================================================================
|
|
24
28
|
// Types
|
|
@@ -191,3 +195,86 @@ export function createAskTool(_cwd: string): AgentTool<typeof askSchema, AskTool
|
|
|
191
195
|
|
|
192
196
|
/** Default ask tool using process.cwd() - for backwards compatibility (no UI) */
|
|
193
197
|
export const askTool = createAskTool(process.cwd());
|
|
198
|
+
|
|
199
|
+
// =============================================================================
|
|
200
|
+
// TUI Renderer
|
|
201
|
+
// =============================================================================
|
|
202
|
+
|
|
203
|
+
interface AskRenderArgs {
|
|
204
|
+
question: string;
|
|
205
|
+
options?: Array<{ label: string }>;
|
|
206
|
+
multi?: boolean;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const askToolRenderer = {
|
|
210
|
+
renderCall(args: AskRenderArgs, uiTheme: Theme): Component {
|
|
211
|
+
if (!args.question) {
|
|
212
|
+
return new Text(formatErrorMessage("No question provided", uiTheme), 0, 0);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const label = uiTheme.fg("toolTitle", uiTheme.bold("Ask"));
|
|
216
|
+
let text = `${label} ${uiTheme.fg("accent", args.question)}`;
|
|
217
|
+
|
|
218
|
+
const meta: string[] = [];
|
|
219
|
+
if (args.multi) meta.push("multi");
|
|
220
|
+
if (args.options?.length) meta.push(`options:${args.options.length}`);
|
|
221
|
+
text += formatMeta(meta, uiTheme);
|
|
222
|
+
|
|
223
|
+
if (args.options?.length) {
|
|
224
|
+
for (let i = 0; i < args.options.length; i++) {
|
|
225
|
+
const opt = args.options[i];
|
|
226
|
+
const isLast = i === args.options.length - 1;
|
|
227
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
228
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg(
|
|
229
|
+
"dim",
|
|
230
|
+
uiTheme.checkbox.unchecked,
|
|
231
|
+
)} ${uiTheme.fg("muted", opt.label)}`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return new Text(text, 0, 0);
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
renderResult(
|
|
239
|
+
result: { content: Array<{ type: string; text?: string }>; details?: AskToolDetails },
|
|
240
|
+
_opts: RenderResultOptions,
|
|
241
|
+
uiTheme: Theme,
|
|
242
|
+
): Component {
|
|
243
|
+
const { details } = result;
|
|
244
|
+
if (!details) {
|
|
245
|
+
const txt = result.content[0];
|
|
246
|
+
return new Text(txt?.type === "text" && txt.text ? txt.text : "", 0, 0);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const hasSelection = details.customInput || details.selectedOptions.length > 0;
|
|
250
|
+
const statusIcon = hasSelection
|
|
251
|
+
? uiTheme.styledSymbol("status.success", "success")
|
|
252
|
+
: uiTheme.styledSymbol("status.warning", "warning");
|
|
253
|
+
|
|
254
|
+
let text = `${statusIcon} ${uiTheme.fg("accent", details.question)}`;
|
|
255
|
+
|
|
256
|
+
if (details.customInput) {
|
|
257
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol(
|
|
258
|
+
"status.success",
|
|
259
|
+
"success",
|
|
260
|
+
)} ${uiTheme.fg("toolOutput", details.customInput)}`;
|
|
261
|
+
} else if (details.selectedOptions.length > 0) {
|
|
262
|
+
const selected = details.selectedOptions;
|
|
263
|
+
for (let i = 0; i < selected.length; i++) {
|
|
264
|
+
const isLast = i === selected.length - 1;
|
|
265
|
+
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
266
|
+
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg(
|
|
267
|
+
"success",
|
|
268
|
+
uiTheme.checkbox.checked,
|
|
269
|
+
)} ${uiTheme.fg("toolOutput", selected[i])}`;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol(
|
|
273
|
+
"status.warning",
|
|
274
|
+
"warning",
|
|
275
|
+
)} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return new Text(text, 0, 0);
|
|
279
|
+
},
|
|
280
|
+
};
|
|
@@ -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)/,
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { Text } from "@oh-my-pi/pi-tui";
|
|
2
4
|
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
3
6
|
import bashDescription from "../../prompts/tools/bash.md" with { type: "text" };
|
|
4
7
|
import { executeBash } from "../bash-executor";
|
|
8
|
+
import type { RenderResultOptions } from "../custom-tools/types";
|
|
9
|
+
import { formatBytes, wrapBrackets } from "./render-utils";
|
|
5
10
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
|
|
6
11
|
|
|
7
12
|
const bashSchema = Type.Object({
|
|
@@ -89,3 +94,104 @@ export function createBashTool(cwd: string): AgentTool<typeof bashSchema> {
|
|
|
89
94
|
|
|
90
95
|
/** Default bash tool using process.cwd() - for backwards compatibility */
|
|
91
96
|
export const bashTool = createBashTool(process.cwd());
|
|
97
|
+
|
|
98
|
+
// =============================================================================
|
|
99
|
+
// TUI Renderer
|
|
100
|
+
// =============================================================================
|
|
101
|
+
|
|
102
|
+
interface BashRenderArgs {
|
|
103
|
+
command?: string;
|
|
104
|
+
timeout?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface BashRenderContext {
|
|
108
|
+
/** Visual lines for truncated output (pre-computed by tool-execution) */
|
|
109
|
+
visualLines?: string[];
|
|
110
|
+
/** Number of lines skipped */
|
|
111
|
+
skippedCount?: number;
|
|
112
|
+
/** Total visual lines */
|
|
113
|
+
totalVisualLines?: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const bashToolRenderer = {
|
|
117
|
+
renderCall(args: BashRenderArgs, uiTheme: Theme): Component {
|
|
118
|
+
const command = args.command || uiTheme.format.ellipsis;
|
|
119
|
+
const text = uiTheme.fg("toolTitle", uiTheme.bold(`$ ${command}`));
|
|
120
|
+
return new Text(text, 0, 0);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
renderResult(
|
|
124
|
+
result: {
|
|
125
|
+
content: Array<{ type: string; text?: string }>;
|
|
126
|
+
details?: BashToolDetails;
|
|
127
|
+
},
|
|
128
|
+
options: RenderResultOptions & { renderContext?: BashRenderContext },
|
|
129
|
+
uiTheme: Theme,
|
|
130
|
+
): Component {
|
|
131
|
+
const { expanded, renderContext } = options;
|
|
132
|
+
const details = result.details;
|
|
133
|
+
const lines: string[] = [];
|
|
134
|
+
|
|
135
|
+
// Get output text
|
|
136
|
+
const textContent = result.content?.find((c) => c.type === "text")?.text ?? "";
|
|
137
|
+
const output = textContent.trim();
|
|
138
|
+
|
|
139
|
+
if (output) {
|
|
140
|
+
if (expanded) {
|
|
141
|
+
// Show all lines when expanded
|
|
142
|
+
const styledOutput = output
|
|
143
|
+
.split("\n")
|
|
144
|
+
.map((line) => uiTheme.fg("toolOutput", line))
|
|
145
|
+
.join("\n");
|
|
146
|
+
lines.push(styledOutput);
|
|
147
|
+
} else if (renderContext?.visualLines) {
|
|
148
|
+
// Use pre-computed visual lines from tool-execution
|
|
149
|
+
const { visualLines, skippedCount = 0, totalVisualLines = visualLines.length } = renderContext;
|
|
150
|
+
if (skippedCount > 0) {
|
|
151
|
+
lines.push(
|
|
152
|
+
uiTheme.fg(
|
|
153
|
+
"dim",
|
|
154
|
+
`${uiTheme.format.ellipsis} (${skippedCount} earlier lines, showing ${visualLines.length} of ${totalVisualLines}) (ctrl+o to expand)`,
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
lines.push(...visualLines);
|
|
159
|
+
} else {
|
|
160
|
+
// Fallback: show first few lines
|
|
161
|
+
const outputLines = output.split("\n");
|
|
162
|
+
const maxLines = 5;
|
|
163
|
+
const displayLines = outputLines.slice(0, maxLines);
|
|
164
|
+
const remaining = outputLines.length - maxLines;
|
|
165
|
+
|
|
166
|
+
lines.push(...displayLines.map((line) => uiTheme.fg("toolOutput", line)));
|
|
167
|
+
if (remaining > 0) {
|
|
168
|
+
lines.push(uiTheme.fg("dim", `${uiTheme.format.ellipsis} (${remaining} more lines) (ctrl+o to expand)`));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Truncation warnings
|
|
174
|
+
const truncation = details?.truncation;
|
|
175
|
+
const fullOutputPath = details?.fullOutputPath;
|
|
176
|
+
if (truncation?.truncated || fullOutputPath) {
|
|
177
|
+
const warnings: string[] = [];
|
|
178
|
+
if (fullOutputPath) {
|
|
179
|
+
warnings.push(`Full output: ${fullOutputPath}`);
|
|
180
|
+
}
|
|
181
|
+
if (truncation?.truncated) {
|
|
182
|
+
if (truncation.truncatedBy === "lines") {
|
|
183
|
+
warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
184
|
+
} else {
|
|
185
|
+
warnings.push(
|
|
186
|
+
`Truncated: ${truncation.outputLines} lines shown (${formatBytes(
|
|
187
|
+
truncation.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
188
|
+
)} limit)`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
lines.push(uiTheme.fg("warning", wrapBrackets(warnings.join(". "), uiTheme)));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
196
|
+
},
|
|
197
|
+
};
|