@oh-my-pi/pi-coding-agent 8.10.11 → 8.10.12
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/package.json +12 -12
- package/src/cli/stats-cli.ts +9 -0
- package/src/config/model-resolver.ts +1 -0
- package/src/config/prompt-templates.ts +1 -1
- package/src/config/settings-manager.ts +53 -0
- package/src/modes/components/hook-selector.ts +9 -1
- package/src/modes/controllers/extension-ui-controller.ts +8 -3
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/types.ts +6 -2
- package/src/patch/index.ts +37 -20
- package/src/prompts/tools/ask.md +4 -3
- package/src/sdk.ts +4 -1
- package/src/session/auth-storage.ts +6 -0
- package/src/task/executor.ts +16 -16
- package/src/tools/ask.ts +46 -6
- package/src/tools/fetch.ts +30 -11
- package/src/tools/index.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.10.
|
|
3
|
+
"version": "8.10.12",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -83,18 +83,18 @@
|
|
|
83
83
|
"test": "bun test"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@oh-my-pi/omp-stats": "8.10.
|
|
87
|
-
"@oh-my-pi/pi-agent-core": "8.10.
|
|
88
|
-
"@oh-my-pi/pi-ai": "8.10.
|
|
89
|
-
"@oh-my-pi/pi-tui": "8.10.
|
|
90
|
-
"@oh-my-pi/pi-utils": "8.10.
|
|
91
|
-
"@openai/agents": "^0.4.
|
|
92
|
-
"@sinclair/typebox": "^0.34.
|
|
86
|
+
"@oh-my-pi/omp-stats": "8.10.12",
|
|
87
|
+
"@oh-my-pi/pi-agent-core": "8.10.12",
|
|
88
|
+
"@oh-my-pi/pi-ai": "8.10.12",
|
|
89
|
+
"@oh-my-pi/pi-tui": "8.10.12",
|
|
90
|
+
"@oh-my-pi/pi-utils": "8.10.12",
|
|
91
|
+
"@openai/agents": "^0.4.4",
|
|
92
|
+
"@sinclair/typebox": "^0.34.48",
|
|
93
93
|
"ajv": "^8.17.1",
|
|
94
|
-
"chalk": "^5.
|
|
94
|
+
"chalk": "^5.6.2",
|
|
95
95
|
"cli-highlight": "^2.1.11",
|
|
96
|
-
"diff": "^8.0.
|
|
97
|
-
"file-type": "^21.
|
|
96
|
+
"diff": "^8.0.3",
|
|
97
|
+
"file-type": "^21.3.0",
|
|
98
98
|
"glob": "^13.0.0",
|
|
99
99
|
"handlebars": "^4.7.8",
|
|
100
100
|
"highlight.js": "^11.11.1",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"node-html-parser": "^7.0.2",
|
|
104
104
|
"smol-toml": "^1.6.0",
|
|
105
105
|
"strip-ansi": "^7.1.2",
|
|
106
|
-
"zod": "^4.3.
|
|
106
|
+
"zod": "^4.3.6"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
109
|
"@types/diff": "^8.0.0",
|
package/src/cli/stats-cli.ts
CHANGED
|
@@ -107,6 +107,15 @@ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
|
|
|
107
107
|
// Start the dashboard server
|
|
108
108
|
const { port } = await startServer(cmd.port);
|
|
109
109
|
console.log(chalk.green(`Dashboard available at: http://localhost:${port}`));
|
|
110
|
+
|
|
111
|
+
// Open browser
|
|
112
|
+
const url = `http://localhost:${port}`;
|
|
113
|
+
const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
114
|
+
Bun.spawn(openCommand === "cmd" ? ["cmd", "/c", "start", url] : [openCommand, url], {
|
|
115
|
+
stdout: "ignore",
|
|
116
|
+
stderr: "ignore",
|
|
117
|
+
}).unref();
|
|
118
|
+
|
|
110
119
|
console.log("Press Ctrl+C to stop\n");
|
|
111
120
|
|
|
112
121
|
// Keep process running
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import { jtdToTypeScript } from "@oh-my-pi/pi-coding-agent/tools/jtd-to-typescript";
|
|
3
2
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
3
|
import Handlebars from "handlebars";
|
|
5
4
|
import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
|
|
5
|
+
import { jtdToTypeScript } from "../tools/jtd-to-typescript";
|
|
6
6
|
import { parseFrontmatter } from "../utils/frontmatter";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -135,6 +135,8 @@ export interface EditSettings {
|
|
|
135
135
|
fuzzyThreshold?: number; // default: 0.95 (similarity threshold for fuzzy matching)
|
|
136
136
|
patchMode?: boolean; // default: true (use codex-style apply-patch format instead of old_text/new_text)
|
|
137
137
|
streamingAbort?: boolean; // default: false (abort streaming edit tool calls when patch preview fails)
|
|
138
|
+
/** Model-specific variant overrides. Keys are model pattern substrings (e.g., "kimi", "deepseek"). */
|
|
139
|
+
modelVariants?: Record<string, "patch" | "replace">;
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
export type { SymbolPreset };
|
|
@@ -1387,6 +1389,57 @@ export class SettingsManager {
|
|
|
1387
1389
|
await this.save();
|
|
1388
1390
|
}
|
|
1389
1391
|
|
|
1392
|
+
/**
|
|
1393
|
+
* Default model patterns that should use replace mode instead of patch mode.
|
|
1394
|
+
* These are models known to struggle with unified diff format.
|
|
1395
|
+
*/
|
|
1396
|
+
static readonly DEFAULT_REPLACE_MODE_PATTERNS = ["kimi"];
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Get the edit variant for a specific model.
|
|
1400
|
+
* Returns "patch", "replace", or null (use global default).
|
|
1401
|
+
*/
|
|
1402
|
+
getEditVariantForModel(model: string | undefined): "patch" | "replace" | null {
|
|
1403
|
+
if (!model) return null;
|
|
1404
|
+
const modelLower = model.toLowerCase();
|
|
1405
|
+
|
|
1406
|
+
const userVariants = this.settings.edit?.modelVariants;
|
|
1407
|
+
if (userVariants) {
|
|
1408
|
+
for (const [pattern, variant] of Object.entries(userVariants)) {
|
|
1409
|
+
if (modelLower.includes(pattern.toLowerCase())) {
|
|
1410
|
+
return variant;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
for (const pattern of SettingsManager.DEFAULT_REPLACE_MODE_PATTERNS) {
|
|
1416
|
+
if (modelLower.includes(pattern)) {
|
|
1417
|
+
return "replace";
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
getEditModelVariants(): Record<string, "patch" | "replace"> {
|
|
1425
|
+
return this.settings.edit?.modelVariants ?? {};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
async setEditModelVariant(pattern: string, variant: "patch" | "replace" | null): Promise<void> {
|
|
1429
|
+
if (!this.globalSettings.edit) {
|
|
1430
|
+
this.globalSettings.edit = {};
|
|
1431
|
+
}
|
|
1432
|
+
if (!this.globalSettings.edit.modelVariants) {
|
|
1433
|
+
this.globalSettings.edit.modelVariants = {};
|
|
1434
|
+
}
|
|
1435
|
+
if (variant === null) {
|
|
1436
|
+
delete this.globalSettings.edit.modelVariants[pattern];
|
|
1437
|
+
} else {
|
|
1438
|
+
this.globalSettings.edit.modelVariants[pattern] = variant;
|
|
1439
|
+
}
|
|
1440
|
+
await this.save();
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1390
1443
|
getNormativeRewrite(): boolean {
|
|
1391
1444
|
return this.settings.normativeRewrite ?? false;
|
|
1392
1445
|
}
|
|
@@ -50,7 +50,15 @@ export class HookSelectorComponent extends Container {
|
|
|
50
50
|
opts.timeout,
|
|
51
51
|
opts.tui,
|
|
52
52
|
s => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)),
|
|
53
|
-
() =>
|
|
53
|
+
() => {
|
|
54
|
+
// Auto-select current option on timeout (typically the first/recommended option)
|
|
55
|
+
const selected = this.options[this.selectedIndex];
|
|
56
|
+
if (selected) {
|
|
57
|
+
this.onSelectCallback(selected);
|
|
58
|
+
} else {
|
|
59
|
+
this.onCancelCallback();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
54
62
|
);
|
|
55
63
|
}
|
|
56
64
|
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ExtensionContextActions,
|
|
9
9
|
ExtensionError,
|
|
10
10
|
ExtensionUIContext,
|
|
11
|
+
ExtensionUIDialogOptions,
|
|
11
12
|
} from "../../extensibility/extensions";
|
|
12
13
|
import { HookEditorComponent } from "../../modes/components/hook-editor";
|
|
13
14
|
import { HookInputComponent } from "../../modes/components/hook-input";
|
|
@@ -25,7 +26,7 @@ export class ExtensionUiController {
|
|
|
25
26
|
async initHooksAndCustomTools(): Promise<void> {
|
|
26
27
|
// Create and set hook & tool UI context
|
|
27
28
|
const uiContext: ExtensionUIContext = {
|
|
28
|
-
select: (title, options, dialogOptions) => this.showHookSelector(title, options, dialogOptions
|
|
29
|
+
select: (title, options, dialogOptions) => this.showHookSelector(title, options, dialogOptions),
|
|
29
30
|
confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
|
|
30
31
|
input: (title, placeholder, _dialogOptions) => this.showHookInput(title, placeholder),
|
|
31
32
|
notify: (message, type) => this.showHookNotify(message, type),
|
|
@@ -484,7 +485,11 @@ export class ExtensionUiController {
|
|
|
484
485
|
/**
|
|
485
486
|
* Show a selector for hooks.
|
|
486
487
|
*/
|
|
487
|
-
showHookSelector(
|
|
488
|
+
showHookSelector(
|
|
489
|
+
title: string,
|
|
490
|
+
options: string[],
|
|
491
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
492
|
+
): Promise<string | undefined> {
|
|
488
493
|
const { promise, resolve } = Promise.withResolvers<string | undefined>();
|
|
489
494
|
this.ctx.hookSelector = new HookSelectorComponent(
|
|
490
495
|
title,
|
|
@@ -497,7 +502,7 @@ export class ExtensionUiController {
|
|
|
497
502
|
this.hideHookSelector();
|
|
498
503
|
resolve(undefined);
|
|
499
504
|
},
|
|
500
|
-
{ initialIndex },
|
|
505
|
+
{ initialIndex: dialogOptions?.initialIndex, timeout: dialogOptions?.timeout, tui: this.ctx.ui },
|
|
501
506
|
);
|
|
502
507
|
|
|
503
508
|
this.ctx.editorContainer.clear();
|
|
@@ -32,12 +32,12 @@ export class InputController {
|
|
|
32
32
|
this.ctx.editor.setText("");
|
|
33
33
|
this.ctx.isBashMode = false;
|
|
34
34
|
this.ctx.updateEditorBorderColor();
|
|
35
|
+
} else if (this.ctx.session.isPythonRunning) {
|
|
36
|
+
this.ctx.session.abortPython();
|
|
35
37
|
} else if (this.ctx.isPythonMode) {
|
|
36
38
|
this.ctx.editor.setText("");
|
|
37
39
|
this.ctx.isPythonMode = false;
|
|
38
40
|
this.ctx.updateEditorBorderColor();
|
|
39
|
-
} else if (this.ctx.session.isPythonRunning) {
|
|
40
|
-
this.ctx.session.abortPython();
|
|
41
41
|
} else if (!this.ctx.editor.getText().trim()) {
|
|
42
42
|
// Double-escape with empty editor triggers /tree or /branch based on setting
|
|
43
43
|
const now = Date.now();
|
|
@@ -20,7 +20,7 @@ import chalk from "chalk";
|
|
|
20
20
|
import { KeybindingsManager } from "../config/keybindings";
|
|
21
21
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
22
22
|
import type { SettingsManager } from "../config/settings-manager";
|
|
23
|
-
import type { ExtensionUIContext } from "../extensibility/extensions";
|
|
23
|
+
import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
|
|
24
24
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
25
25
|
import { loadSlashCommands } from "../extensibility/slash-commands";
|
|
26
26
|
import { resolvePlanUrlToPath } from "../internal-urls";
|
|
@@ -1020,8 +1020,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1020
1020
|
this.extensionUiController.setHookStatus(key, text);
|
|
1021
1021
|
}
|
|
1022
1022
|
|
|
1023
|
-
showHookSelector(
|
|
1024
|
-
|
|
1023
|
+
showHookSelector(
|
|
1024
|
+
title: string,
|
|
1025
|
+
options: string[],
|
|
1026
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
1027
|
+
): Promise<string | undefined> {
|
|
1028
|
+
return this.extensionUiController.showHookSelector(title, options, dialogOptions);
|
|
1025
1029
|
}
|
|
1026
1030
|
|
|
1027
1031
|
hideHookSelector(): void {
|
package/src/modes/types.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-m
|
|
|
3
3
|
import type { Component, Container, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
5
5
|
import type { SettingsManager } from "../config/settings-manager";
|
|
6
|
-
import type { ExtensionUIContext } from "../extensibility/extensions";
|
|
6
|
+
import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
|
|
7
7
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
8
8
|
import type { MCPManager } from "../mcp";
|
|
9
9
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
@@ -189,7 +189,11 @@ export interface InteractiveModeContext {
|
|
|
189
189
|
): Promise<void>;
|
|
190
190
|
setHookWidget(key: string, content: unknown): void;
|
|
191
191
|
setHookStatus(key: string, text: string | undefined): void;
|
|
192
|
-
showHookSelector(
|
|
192
|
+
showHookSelector(
|
|
193
|
+
title: string,
|
|
194
|
+
options: string[],
|
|
195
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
196
|
+
): Promise<string | undefined>;
|
|
193
197
|
hideHookSelector(): void;
|
|
194
198
|
showHookInput(title: string, placeholder?: string): Promise<string | undefined>;
|
|
195
199
|
hideHookInput(): void;
|
package/src/patch/index.ts
CHANGED
|
@@ -202,14 +202,12 @@ type TInput = typeof replaceEditSchema | typeof patchEditSchema;
|
|
|
202
202
|
export class EditTool implements AgentTool<TInput> {
|
|
203
203
|
public readonly name = "edit";
|
|
204
204
|
public readonly label = "Edit";
|
|
205
|
-
public readonly description: string;
|
|
206
|
-
public readonly parameters: TInput;
|
|
207
205
|
|
|
208
206
|
private readonly session: ToolSession;
|
|
209
|
-
private readonly patchMode: boolean;
|
|
210
207
|
private readonly allowFuzzy: boolean;
|
|
211
208
|
private readonly fuzzyThreshold: number;
|
|
212
209
|
private readonly writethrough: WritethroughCallback;
|
|
210
|
+
private readonly envEditVariant: string;
|
|
213
211
|
|
|
214
212
|
constructor(session: ToolSession) {
|
|
215
213
|
this.session = session;
|
|
@@ -217,22 +215,14 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
217
215
|
const {
|
|
218
216
|
OMP_EDIT_FUZZY: editFuzzy = "auto",
|
|
219
217
|
OMP_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto",
|
|
220
|
-
OMP_EDIT_VARIANT:
|
|
218
|
+
OMP_EDIT_VARIANT: envEditVariant = "auto",
|
|
221
219
|
} = process.env;
|
|
220
|
+
this.envEditVariant = envEditVariant;
|
|
222
221
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.patchMode = false;
|
|
226
|
-
break;
|
|
227
|
-
case "patch":
|
|
228
|
-
this.patchMode = true;
|
|
229
|
-
break;
|
|
230
|
-
case "auto":
|
|
231
|
-
this.patchMode = session.settings?.getEditPatchMode?.() ?? true;
|
|
232
|
-
break;
|
|
233
|
-
default:
|
|
234
|
-
throw new Error(`Invalid OMP_EDIT_VARIANT: ${process.env.OMP_EDIT_VARIANT}`);
|
|
222
|
+
if (envEditVariant !== "replace" && envEditVariant !== "patch" && envEditVariant !== "auto") {
|
|
223
|
+
throw new Error(`Invalid OMP_EDIT_VARIANT: ${envEditVariant}`);
|
|
235
224
|
}
|
|
225
|
+
|
|
236
226
|
switch (editFuzzy) {
|
|
237
227
|
case "true":
|
|
238
228
|
case "1":
|
|
@@ -266,10 +256,37 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
266
256
|
this.writethrough = enableLsp
|
|
267
257
|
? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
|
|
268
258
|
: writethroughNoop;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Determine patch mode dynamically based on current model.
|
|
263
|
+
* This is re-evaluated on each access so tool definitions stay current when model changes.
|
|
264
|
+
*/
|
|
265
|
+
private get patchMode(): boolean {
|
|
266
|
+
if (this.envEditVariant === "replace") return false;
|
|
267
|
+
if (this.envEditVariant === "patch") return true;
|
|
268
|
+
|
|
269
|
+
// Auto mode: check model-specific settings
|
|
270
|
+
const activeModel = this.session.getActiveModelString?.();
|
|
271
|
+
const modelVariant = this.session.settings?.getEditVariantForModel?.(activeModel);
|
|
272
|
+
if (modelVariant === "replace") return false;
|
|
273
|
+
if (modelVariant === "patch") return true;
|
|
274
|
+
|
|
275
|
+
return this.session.settings?.getEditPatchMode?.() ?? true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Dynamic description based on current patch mode (which depends on current model).
|
|
280
|
+
*/
|
|
281
|
+
public get description(): string {
|
|
282
|
+
return this.patchMode ? renderPromptTemplate(patchDescription) : renderPromptTemplate(replaceDescription);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Dynamic parameters schema based on current patch mode (which depends on current model).
|
|
287
|
+
*/
|
|
288
|
+
public get parameters(): TInput {
|
|
289
|
+
return this.patchMode ? patchEditSchema : replaceEditSchema;
|
|
273
290
|
}
|
|
274
291
|
|
|
275
292
|
public async execute(
|
package/src/prompts/tools/ask.md
CHANGED
|
@@ -10,7 +10,7 @@ Ask the user a question when you need clarification or input during task executi
|
|
|
10
10
|
</conditions>
|
|
11
11
|
|
|
12
12
|
<instruction>
|
|
13
|
-
-
|
|
13
|
+
- Use `recommended: <index>` to mark the default option (0-indexed); " (Recommended)" suffix is added automatically
|
|
14
14
|
- Use `questions` array for multiple related questions instead of asking one at a time
|
|
15
15
|
- Set `multi: true` on a question to allow multiple selections
|
|
16
16
|
</instruction>
|
|
@@ -37,12 +37,13 @@ If you can make a reasonable inference from the user's request, **do it**. Users
|
|
|
37
37
|
|
|
38
38
|
<example name="single">
|
|
39
39
|
question: "Which authentication method should this API use?"
|
|
40
|
-
options: [{"label": "JWT
|
|
40
|
+
options: [{"label": "JWT"}, {"label": "OAuth2"}, {"label": "Session cookies"}]
|
|
41
|
+
recommended: 0
|
|
41
42
|
</example>
|
|
42
43
|
|
|
43
44
|
<example name="multi-part">
|
|
44
45
|
questions: [
|
|
45
|
-
{"id": "auth", "question": "Which auth method?", "options": [{"label": "JWT"}, {"label": "OAuth2"}]},
|
|
46
|
+
{"id": "auth", "question": "Which auth method?", "options": [{"label": "JWT"}, {"label": "OAuth2"}], "recommended": 0},
|
|
46
47
|
{"id": "cache", "question": "Enable caching?", "options": [{"label": "Yes"}, {"label": "No"}]},
|
|
47
48
|
{"id": "features", "question": "Which features to include?", "options": [{"label": "Logging"}, {"label": "Metrics"}, {"label": "Tracing"}], "multi": true}
|
|
48
49
|
]
|
package/src/sdk.ts
CHANGED
|
@@ -753,7 +753,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
753
753
|
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
|
754
754
|
getActiveModelString: () => {
|
|
755
755
|
const activeModel = agent?.state.model;
|
|
756
|
-
|
|
756
|
+
if (activeModel) return formatModelString(activeModel);
|
|
757
|
+
// Fall back to initial model during tool creation (before agent exists)
|
|
758
|
+
if (model) return formatModelString(model);
|
|
759
|
+
return undefined;
|
|
757
760
|
},
|
|
758
761
|
getPlanModeState: () => session.getPlanModeState(),
|
|
759
762
|
settings: settingsManager,
|
|
@@ -11,11 +11,13 @@ import {
|
|
|
11
11
|
getOAuthApiKey,
|
|
12
12
|
githubCopilotUsageProvider,
|
|
13
13
|
googleGeminiCliUsageProvider,
|
|
14
|
+
kimiUsageProvider,
|
|
14
15
|
loginAnthropic,
|
|
15
16
|
loginAntigravity,
|
|
16
17
|
loginCursor,
|
|
17
18
|
loginGeminiCli,
|
|
18
19
|
loginGitHubCopilot,
|
|
20
|
+
loginKimi,
|
|
19
21
|
loginOpenAICodex,
|
|
20
22
|
type OAuthController,
|
|
21
23
|
type OAuthCredentials,
|
|
@@ -85,6 +87,7 @@ export type AuthStorageOptions = {
|
|
|
85
87
|
|
|
86
88
|
const DEFAULT_USAGE_PROVIDERS: UsageProvider[] = [
|
|
87
89
|
openaiCodexUsageProvider,
|
|
90
|
+
kimiUsageProvider,
|
|
88
91
|
antigravityUsageProvider,
|
|
89
92
|
googleGeminiCliUsageProvider,
|
|
90
93
|
claudeUsageProvider,
|
|
@@ -770,6 +773,9 @@ export class AuthStorage {
|
|
|
770
773
|
case "openai-codex":
|
|
771
774
|
credentials = await loginOpenAICodex(ctrl);
|
|
772
775
|
break;
|
|
776
|
+
case "kimi-code":
|
|
777
|
+
credentials = await loginKimi(ctrl);
|
|
778
|
+
break;
|
|
773
779
|
case "cursor":
|
|
774
780
|
credentials = await loginCursor(
|
|
775
781
|
url => ctrl.onAuth({ url }),
|
package/src/task/executor.ts
CHANGED
|
@@ -6,26 +6,26 @@
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
8
|
import type { Api, Model, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
9
|
-
import type { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
|
10
|
-
import { parseModelPattern } from "@oh-my-pi/pi-coding-agent/config/model-resolver";
|
|
11
|
-
import { type PromptTemplate, renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
|
|
12
|
-
import { SettingsManager } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
|
|
13
|
-
import type { CustomTool } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
|
|
14
|
-
import type { Skill } from "@oh-my-pi/pi-coding-agent/extensibility/skills";
|
|
15
|
-
import { callTool } from "@oh-my-pi/pi-coding-agent/mcp/client";
|
|
16
|
-
import type { MCPManager } from "@oh-my-pi/pi-coding-agent/mcp/manager";
|
|
17
|
-
import { createAgentSession, discoverAuthStorage, discoverModels } from "@oh-my-pi/pi-coding-agent/sdk";
|
|
18
|
-
import type { AgentSession, AgentSessionEvent } from "@oh-my-pi/pi-coding-agent/session/agent-session";
|
|
19
|
-
import type { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
|
20
|
-
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
|
|
21
|
-
import type { ContextFileEntry } from "@oh-my-pi/pi-coding-agent/tools";
|
|
22
|
-
import { jtdToJsonSchema } from "@oh-my-pi/pi-coding-agent/tools/jtd-to-json-schema";
|
|
23
|
-
import { ToolAbortError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
|
|
24
|
-
import type { EventBus } from "@oh-my-pi/pi-coding-agent/utils/event-bus";
|
|
25
9
|
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
26
10
|
import type { TSchema } from "@sinclair/typebox";
|
|
27
11
|
import Ajv, { type ValidateFunction } from "ajv";
|
|
12
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
13
|
+
import { parseModelPattern } from "../config/model-resolver";
|
|
14
|
+
import { type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
|
|
15
|
+
import { SettingsManager } from "../config/settings-manager";
|
|
16
|
+
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
17
|
+
import type { Skill } from "../extensibility/skills";
|
|
18
|
+
import { callTool } from "../mcp/client";
|
|
19
|
+
import type { MCPManager } from "../mcp/manager";
|
|
28
20
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
21
|
+
import { createAgentSession, discoverAuthStorage, discoverModels } from "../sdk";
|
|
22
|
+
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
23
|
+
import type { AuthStorage } from "../session/auth-storage";
|
|
24
|
+
import { SessionManager } from "../session/session-manager";
|
|
25
|
+
import type { ContextFileEntry } from "../tools";
|
|
26
|
+
import { jtdToJsonSchema } from "../tools/jtd-to-json-schema";
|
|
27
|
+
import { ToolAbortError } from "../tools/tool-errors";
|
|
28
|
+
import type { EventBus } from "../utils/event-bus";
|
|
29
29
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
30
30
|
import {
|
|
31
31
|
type AgentDefinition,
|
package/src/tools/ask.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* Usage notes:
|
|
12
12
|
* - Users will always be able to select "Other" to provide custom text input
|
|
13
13
|
* - Use multi: true to allow multiple answers to be selected for a question
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
14
|
+
* - Use recommended: <index> to mark the default option; "(Recommended)" suffix is added automatically
|
|
15
|
+
* - Questions time out after 30 seconds and auto-select the recommended option
|
|
16
16
|
*/
|
|
17
17
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
18
18
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -39,12 +39,14 @@ const QuestionItem = Type.Object({
|
|
|
39
39
|
question: Type.String({ description: "Question text" }),
|
|
40
40
|
options: Type.Array(OptionItem, { description: "Available options" }),
|
|
41
41
|
multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections" })),
|
|
42
|
+
recommended: Type.Optional(Type.Number({ description: "Index of recommended option (0-indexed)" })),
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
const askSchema = Type.Object({
|
|
45
46
|
question: Type.Optional(Type.String({ description: "Question to ask" })),
|
|
46
47
|
options: Type.Optional(Type.Array(OptionItem, { description: "Available options" })),
|
|
47
48
|
multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections (default: false)" })),
|
|
49
|
+
recommended: Type.Optional(Type.Number({ description: "Index of recommended option (0-indexed, default: 0)" })),
|
|
48
50
|
questions: Type.Optional(Type.Array(QuestionItem, { description: "Multiple questions in sequence" })),
|
|
49
51
|
});
|
|
50
52
|
|
|
@@ -74,10 +76,30 @@ export interface AskToolDetails {
|
|
|
74
76
|
// =============================================================================
|
|
75
77
|
|
|
76
78
|
const OTHER_OPTION = "Other (type your own)";
|
|
79
|
+
const RECOMMENDED_SUFFIX = " (Recommended)";
|
|
80
|
+
|
|
77
81
|
function getDoneOptionLabel(): string {
|
|
78
82
|
return `${theme.status.success} Done selecting`;
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
/** Add "(Recommended)" suffix to the option at the given index if not already present */
|
|
86
|
+
function addRecommendedSuffix(labels: string[], recommendedIndex?: number): string[] {
|
|
87
|
+
if (recommendedIndex === undefined || recommendedIndex < 0 || recommendedIndex >= labels.length) {
|
|
88
|
+
return labels;
|
|
89
|
+
}
|
|
90
|
+
return labels.map((label, i) => {
|
|
91
|
+
if (i === recommendedIndex && !label.endsWith(RECOMMENDED_SUFFIX)) {
|
|
92
|
+
return label + RECOMMENDED_SUFFIX;
|
|
93
|
+
}
|
|
94
|
+
return label;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Strip "(Recommended)" suffix from a label */
|
|
99
|
+
function stripRecommendedSuffix(label: string): string {
|
|
100
|
+
return label.endsWith(RECOMMENDED_SUFFIX) ? label.slice(0, -RECOMMENDED_SUFFIX.length) : label;
|
|
101
|
+
}
|
|
102
|
+
|
|
81
103
|
// =============================================================================
|
|
82
104
|
// Question Selection Logic
|
|
83
105
|
// =============================================================================
|
|
@@ -88,7 +110,11 @@ interface SelectionResult {
|
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
interface UIContext {
|
|
91
|
-
select(
|
|
113
|
+
select(
|
|
114
|
+
prompt: string,
|
|
115
|
+
options: string[],
|
|
116
|
+
options_?: { initialIndex?: number; timeout?: number },
|
|
117
|
+
): Promise<string | undefined>;
|
|
92
118
|
input(prompt: string): Promise<string | undefined>;
|
|
93
119
|
}
|
|
94
120
|
|
|
@@ -97,6 +123,7 @@ async function askSingleQuestion(
|
|
|
97
123
|
question: string,
|
|
98
124
|
optionLabels: string[],
|
|
99
125
|
multi: boolean,
|
|
126
|
+
recommended?: number,
|
|
100
127
|
): Promise<SelectionResult> {
|
|
101
128
|
const doneLabel = getDoneOptionLabel();
|
|
102
129
|
let selectedOptions: string[] = [];
|
|
@@ -155,12 +182,16 @@ async function askSingleQuestion(
|
|
|
155
182
|
}
|
|
156
183
|
selectedOptions = Array.from(selected);
|
|
157
184
|
} else {
|
|
158
|
-
const
|
|
185
|
+
const displayLabels = addRecommendedSuffix(optionLabels, recommended);
|
|
186
|
+
const choice = await ui.select(question, [...displayLabels, OTHER_OPTION], {
|
|
187
|
+
timeout: 30000,
|
|
188
|
+
initialIndex: recommended,
|
|
189
|
+
});
|
|
159
190
|
if (choice === OTHER_OPTION) {
|
|
160
191
|
const input = await ui.input("Enter your response:");
|
|
161
192
|
if (input) customInput = input;
|
|
162
193
|
} else if (choice) {
|
|
163
|
-
selectedOptions = [choice];
|
|
194
|
+
selectedOptions = [stripRecommendedSuffix(choice)];
|
|
164
195
|
}
|
|
165
196
|
}
|
|
166
197
|
|
|
@@ -187,11 +218,13 @@ interface AskParams {
|
|
|
187
218
|
question?: string;
|
|
188
219
|
options?: Array<{ label: string }>;
|
|
189
220
|
multi?: boolean;
|
|
221
|
+
recommended?: number;
|
|
190
222
|
questions?: Array<{
|
|
191
223
|
id: string;
|
|
192
224
|
question: string;
|
|
193
225
|
options: Array<{ label: string }>;
|
|
194
226
|
multi?: boolean;
|
|
227
|
+
recommended?: number;
|
|
195
228
|
}>;
|
|
196
229
|
}
|
|
197
230
|
|
|
@@ -243,6 +276,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
243
276
|
q.question,
|
|
244
277
|
optionLabels,
|
|
245
278
|
q.multi ?? false,
|
|
279
|
+
q.recommended,
|
|
246
280
|
);
|
|
247
281
|
|
|
248
282
|
results.push({
|
|
@@ -275,7 +309,13 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
275
309
|
};
|
|
276
310
|
}
|
|
277
311
|
|
|
278
|
-
const { selectedOptions, customInput } = await askSingleQuestion(
|
|
312
|
+
const { selectedOptions, customInput } = await askSingleQuestion(
|
|
313
|
+
ui,
|
|
314
|
+
question,
|
|
315
|
+
optionLabels,
|
|
316
|
+
multi,
|
|
317
|
+
params.recommended,
|
|
318
|
+
);
|
|
279
319
|
|
|
280
320
|
const details: AskToolDetails = {
|
|
281
321
|
question,
|
package/src/tools/fetch.ts
CHANGED
|
@@ -20,9 +20,11 @@ import { convertWithMarkitdown, fetchBinary } from "../web/scrapers/utils";
|
|
|
20
20
|
import type { ToolSession } from ".";
|
|
21
21
|
import { applyListLimit } from "./list-limit";
|
|
22
22
|
import type { OutputMeta } from "./output-meta";
|
|
23
|
+
import { allocateOutputArtifact } from "./output-utils";
|
|
23
24
|
import { formatExpandHint } from "./render-utils";
|
|
24
25
|
import { ToolAbortError } from "./tool-errors";
|
|
25
26
|
import { toolResult } from "./tool-result";
|
|
27
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateHead } from "./truncate";
|
|
26
28
|
|
|
27
29
|
// =============================================================================
|
|
28
30
|
// Types and Constants
|
|
@@ -921,29 +923,46 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
|
|
|
921
923
|
|
|
922
924
|
const scratchDir = this.session.getArtifactsDir?.() ?? this.session.cwd;
|
|
923
925
|
const result = await renderUrl(url, effectiveTimeout, raw, scratchDir, signal);
|
|
926
|
+
const truncation = truncateHead(result.content, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
|
|
927
|
+
const needsArtifact = truncation.truncated;
|
|
928
|
+
let artifactId: string | undefined;
|
|
929
|
+
|
|
930
|
+
const buildOutput = (content: string): string => {
|
|
931
|
+
let output = "";
|
|
932
|
+
output += `URL: ${result.finalUrl}\n`;
|
|
933
|
+
output += `Content-Type: ${result.contentType}\n`;
|
|
934
|
+
output += `Method: ${result.method}\n`;
|
|
935
|
+
if (result.notes.length > 0) {
|
|
936
|
+
output += `Notes: ${result.notes.join("; ")}\n`;
|
|
937
|
+
}
|
|
938
|
+
output += `\n---\n\n`;
|
|
939
|
+
output += content;
|
|
940
|
+
return output;
|
|
941
|
+
};
|
|
924
942
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
output += `Notes: ${result.notes.join("; ")}\n`;
|
|
943
|
+
if (needsArtifact) {
|
|
944
|
+
const { artifactPath, artifactId: allocatedId } = await allocateOutputArtifact(this.session, "fetch");
|
|
945
|
+
if (artifactPath) {
|
|
946
|
+
await Bun.write(artifactPath, buildOutput(result.content));
|
|
947
|
+
artifactId = allocatedId;
|
|
948
|
+
}
|
|
932
949
|
}
|
|
933
|
-
|
|
934
|
-
output
|
|
950
|
+
|
|
951
|
+
const output = buildOutput(needsArtifact ? truncation.content : result.content);
|
|
935
952
|
|
|
936
953
|
const details: FetchToolDetails = {
|
|
937
954
|
url: result.url,
|
|
938
955
|
finalUrl: result.finalUrl,
|
|
939
956
|
contentType: result.contentType,
|
|
940
957
|
method: result.method,
|
|
941
|
-
truncated: result.truncated,
|
|
958
|
+
truncated: result.truncated || needsArtifact,
|
|
942
959
|
notes: result.notes,
|
|
943
960
|
};
|
|
944
961
|
|
|
945
962
|
const resultBuilder = toolResult(details).text(output).sourceUrl(result.finalUrl);
|
|
946
|
-
if (
|
|
963
|
+
if (needsArtifact) {
|
|
964
|
+
resultBuilder.truncation(truncation, { direction: "head", artifactId });
|
|
965
|
+
} else if (result.truncated) {
|
|
947
966
|
const outputLines = result.content.split("\n").length;
|
|
948
967
|
const outputBytes = Buffer.byteLength(result.content, "utf-8");
|
|
949
968
|
const totalBytes = Math.max(outputBytes + 1, MAX_OUTPUT_CHARS + 1);
|
package/src/tools/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type { SettingsManager } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
|
|
3
2
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
3
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
5
|
-
import type { BashInterceptorRule } from "../config/settings-manager";
|
|
4
|
+
import type { BashInterceptorRule, SettingsManager } from "../config/settings-manager";
|
|
6
5
|
import type { Skill } from "../extensibility/skills";
|
|
7
6
|
import type { InternalUrlRouter } from "../internal-urls";
|
|
8
7
|
import { getPreludeDocs, warmPythonEnvironment } from "../ipy/executor";
|
|
@@ -164,6 +163,7 @@ export interface ToolSession {
|
|
|
164
163
|
getEditFuzzyMatch(): boolean;
|
|
165
164
|
getEditFuzzyThreshold?(): number;
|
|
166
165
|
getEditPatchMode?(): boolean;
|
|
166
|
+
getEditVariantForModel?(model: string | undefined): "patch" | "replace" | null;
|
|
167
167
|
getBashInterceptorEnabled(): boolean;
|
|
168
168
|
getBashInterceptorSimpleLsEnabled(): boolean;
|
|
169
169
|
getBashInterceptorRules(): BashInterceptorRule[];
|