@oh-my-pi/pi-coding-agent 15.10.5 → 15.10.6
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 +22 -0
- package/dist/types/exa/index.d.ts +1 -19
- package/dist/types/exa/mcp-client.d.ts +10 -3
- package/dist/types/exa/types.d.ts +0 -83
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +8 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -0
- package/dist/types/tools/index.d.ts +0 -2
- package/dist/types/utils/git.d.ts +6 -0
- package/package.json +9 -9
- package/src/cli/auth-gateway-cli.ts +3 -2
- package/src/commit/agentic/tools/split-commit.ts +8 -1
- package/src/config/model-provider-priority.ts +1 -0
- package/src/exa/index.ts +1 -26
- package/src/exa/mcp-client.ts +10 -10
- package/src/exa/types.ts +0 -97
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +6 -4
- package/src/modes/controllers/event-controller.ts +8 -0
- package/src/modes/controllers/input-controller.ts +24 -1
- package/src/modes/controllers/mcp-command-controller.ts +24 -5
- package/src/modes/interactive-mode.ts +33 -1
- package/src/modes/types.ts +1 -0
- package/src/session/agent-session.ts +77 -41
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/task/index.ts +9 -1
- package/src/task/render.ts +22 -12
- package/src/tools/index.ts +0 -4
- package/src/utils/git.ts +41 -0
- package/dist/types/exa/factory.d.ts +0 -13
- package/dist/types/exa/render.d.ts +0 -19
- package/dist/types/exa/researcher.d.ts +0 -9
- package/dist/types/exa/search.d.ts +0 -9
- package/dist/types/exa/websets.d.ts +0 -9
- package/src/exa/factory.ts +0 -60
- package/src/exa/render.ts +0 -244
- package/src/exa/researcher.ts +0 -36
- package/src/exa/search.ts +0 -47
- package/src/exa/websets.ts +0 -248
|
@@ -65,7 +65,12 @@ import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slas
|
|
|
65
65
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
66
66
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
67
67
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
|
|
68
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
humanizePlanTitle,
|
|
70
|
+
type PlanApprovalDetails,
|
|
71
|
+
resolveApprovedPlan,
|
|
72
|
+
resolvePlanTitle,
|
|
73
|
+
} from "../plan-mode/approved-plan";
|
|
69
74
|
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
|
70
75
|
import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compact-instructions.md" with {
|
|
71
76
|
type: "text",
|
|
@@ -2265,6 +2270,33 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2265
2270
|
await this.#startGoalFromObjective(objective);
|
|
2266
2271
|
}
|
|
2267
2272
|
|
|
2273
|
+
/** Manually (re-)open the plan-review overlay — bound to `/plan-review`. Lets
|
|
2274
|
+
* the operator pull the review back up after dismissing it, or review a plan
|
|
2275
|
+
* the agent wrote without calling `resolve`. There is no fixed plan filename:
|
|
2276
|
+
* `getPlanReferencePath()` is empty until a plan is actually approved (and does
|
|
2277
|
+
* not survive a restart), so this drives off the newest `local://<slug>-plan.md`
|
|
2278
|
+
* the agent wrote — the files persist in the session artifacts dir, so the scan
|
|
2279
|
+
* works before any review and across restarts. */
|
|
2280
|
+
async openPlanReview(): Promise<void> {
|
|
2281
|
+
if (!this.planModeEnabled) {
|
|
2282
|
+
this.showWarning("Plan mode is not active.");
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
const noPlan = "No plan to review yet — write one to a local://<slug>-plan.md file first.";
|
|
2286
|
+
const [planFilePath] = await this.#listLocalPlanFiles();
|
|
2287
|
+
if (!planFilePath) {
|
|
2288
|
+
this.showWarning(noPlan);
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
const planContent = await this.#readPlanFile(planFilePath);
|
|
2292
|
+
if (planContent === null) {
|
|
2293
|
+
this.showWarning(noPlan);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
const { title } = resolvePlanTitle({ planContent, planFilePath });
|
|
2297
|
+
await this.handlePlanApproval({ planFilePath, title, planExists: true });
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2268
2300
|
async handlePlanApproval(details: PlanApprovalDetails): Promise<void> {
|
|
2269
2301
|
if (!this.planModeEnabled) {
|
|
2270
2302
|
this.showWarning("Plan mode is not active.");
|
package/src/modes/types.ts
CHANGED
|
@@ -318,6 +318,7 @@ export interface InteractiveModeContext {
|
|
|
318
318
|
disableLoopMode(): void;
|
|
319
319
|
pauseLoop(): void;
|
|
320
320
|
handlePlanApproval(details: PlanApprovalDetails): Promise<void>;
|
|
321
|
+
openPlanReview(): Promise<void>;
|
|
321
322
|
|
|
322
323
|
// Hook UI methods
|
|
323
324
|
initHooksAndCustomTools(): Promise<void>;
|
|
@@ -229,6 +229,7 @@ import {
|
|
|
229
229
|
type PythonExecutionMessage,
|
|
230
230
|
readPendingDisplayTag,
|
|
231
231
|
SILENT_ABORT_MARKER,
|
|
232
|
+
SKILL_PROMPT_MESSAGE_TYPE,
|
|
232
233
|
stripImagesFromMessage,
|
|
233
234
|
} from "./messages";
|
|
234
235
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
@@ -4335,6 +4336,44 @@ export class AgentSession {
|
|
|
4335
4336
|
return { ...message, content: normalized } as T;
|
|
4336
4337
|
}
|
|
4337
4338
|
|
|
4339
|
+
#createMagicKeywordNotices(text: string): CustomMessage[] {
|
|
4340
|
+
const timestamp = Date.now();
|
|
4341
|
+
const turnBudget = parseTurnBudget(text);
|
|
4342
|
+
this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
|
|
4343
|
+
const keywordNotices: CustomMessage[] = [];
|
|
4344
|
+
if (containsUltrathink(text)) {
|
|
4345
|
+
keywordNotices.push({
|
|
4346
|
+
role: "custom",
|
|
4347
|
+
customType: "ultrathink-notice",
|
|
4348
|
+
content: ULTRATHINK_NOTICE,
|
|
4349
|
+
display: false,
|
|
4350
|
+
attribution: "user",
|
|
4351
|
+
timestamp,
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
if (containsOrchestrate(text)) {
|
|
4355
|
+
keywordNotices.push({
|
|
4356
|
+
role: "custom",
|
|
4357
|
+
customType: "orchestrate-notice",
|
|
4358
|
+
content: ORCHESTRATE_NOTICE,
|
|
4359
|
+
display: false,
|
|
4360
|
+
attribution: "user",
|
|
4361
|
+
timestamp,
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
if (containsWorkflow(text)) {
|
|
4365
|
+
keywordNotices.push({
|
|
4366
|
+
role: "custom",
|
|
4367
|
+
customType: "workflow-notice",
|
|
4368
|
+
content: WORKFLOW_NOTICE,
|
|
4369
|
+
display: false,
|
|
4370
|
+
attribution: "user",
|
|
4371
|
+
timestamp,
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4374
|
+
return keywordNotices;
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4338
4377
|
/**
|
|
4339
4378
|
* Send a prompt to the agent.
|
|
4340
4379
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -4376,42 +4415,7 @@ export class AgentSession {
|
|
|
4376
4415
|
// Magic keywords ("ultrathink", "orchestrate"): append hidden system notices after the
|
|
4377
4416
|
// user's message that steer this turn. User-authored prompts only — synthetic /
|
|
4378
4417
|
// agent-initiated turns never trigger them.
|
|
4379
|
-
const keywordNotices
|
|
4380
|
-
if (!options?.synthetic) {
|
|
4381
|
-
const timestamp = Date.now();
|
|
4382
|
-
const turnBudget = parseTurnBudget(expandedText);
|
|
4383
|
-
this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
|
|
4384
|
-
if (containsUltrathink(expandedText)) {
|
|
4385
|
-
keywordNotices.push({
|
|
4386
|
-
role: "custom",
|
|
4387
|
-
customType: "ultrathink-notice",
|
|
4388
|
-
content: ULTRATHINK_NOTICE,
|
|
4389
|
-
display: false,
|
|
4390
|
-
attribution: "user",
|
|
4391
|
-
timestamp,
|
|
4392
|
-
});
|
|
4393
|
-
}
|
|
4394
|
-
if (containsOrchestrate(expandedText)) {
|
|
4395
|
-
keywordNotices.push({
|
|
4396
|
-
role: "custom",
|
|
4397
|
-
customType: "orchestrate-notice",
|
|
4398
|
-
content: ORCHESTRATE_NOTICE,
|
|
4399
|
-
display: false,
|
|
4400
|
-
attribution: "user",
|
|
4401
|
-
timestamp,
|
|
4402
|
-
});
|
|
4403
|
-
}
|
|
4404
|
-
if (containsWorkflow(expandedText)) {
|
|
4405
|
-
keywordNotices.push({
|
|
4406
|
-
role: "custom",
|
|
4407
|
-
customType: "workflow-notice",
|
|
4408
|
-
content: WORKFLOW_NOTICE,
|
|
4409
|
-
display: false,
|
|
4410
|
-
attribution: "user",
|
|
4411
|
-
timestamp,
|
|
4412
|
-
});
|
|
4413
|
-
}
|
|
4414
|
-
}
|
|
4418
|
+
const keywordNotices = options?.synthetic ? [] : this.#createMagicKeywordNotices(expandedText);
|
|
4415
4419
|
|
|
4416
4420
|
// If streaming, queue via steer() or followUp() based on option
|
|
4417
4421
|
if (this.isStreaming) {
|
|
@@ -4481,11 +4485,24 @@ export class AgentSession {
|
|
|
4481
4485
|
.map(content => content.text)
|
|
4482
4486
|
.join("");
|
|
4483
4487
|
|
|
4488
|
+
let keywordNotices: CustomMessage[] = [];
|
|
4489
|
+
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE && message.attribution === "user") {
|
|
4490
|
+
const details = message.details;
|
|
4491
|
+
let skillArgs = "";
|
|
4492
|
+
if (details && typeof details === "object" && "args" in details && typeof details.args === "string") {
|
|
4493
|
+
skillArgs = details.args;
|
|
4494
|
+
}
|
|
4495
|
+
keywordNotices = this.#createMagicKeywordNotices(skillArgs);
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4484
4498
|
if (this.isStreaming) {
|
|
4485
4499
|
if (!options?.streamingBehavior) {
|
|
4486
4500
|
throw new AgentBusyError();
|
|
4487
4501
|
}
|
|
4488
4502
|
await this.sendCustomMessage(message, { deliverAs: options.streamingBehavior });
|
|
4503
|
+
for (const notice of keywordNotices) {
|
|
4504
|
+
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
4505
|
+
}
|
|
4489
4506
|
return;
|
|
4490
4507
|
}
|
|
4491
4508
|
|
|
@@ -4499,7 +4516,10 @@ export class AgentSession {
|
|
|
4499
4516
|
timestamp: Date.now(),
|
|
4500
4517
|
};
|
|
4501
4518
|
|
|
4502
|
-
await this.#promptWithMessage(customMessage, textContent,
|
|
4519
|
+
await this.#promptWithMessage(customMessage, textContent, {
|
|
4520
|
+
...options,
|
|
4521
|
+
appendMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
|
|
4522
|
+
});
|
|
4503
4523
|
}
|
|
4504
4524
|
|
|
4505
4525
|
async #promptWithMessage(
|
|
@@ -7837,16 +7857,32 @@ export class AgentSession {
|
|
|
7837
7857
|
return "handled";
|
|
7838
7858
|
}
|
|
7839
7859
|
const reclaimed = result.toolResultsDropped + result.blocksDropped > 0;
|
|
7840
|
-
//
|
|
7841
|
-
//
|
|
7842
|
-
|
|
7860
|
+
// Detect the dead-loop reported in issue #2119: the threshold check fires,
|
|
7861
|
+
// shake runs, but the resulting context is still above the configured
|
|
7862
|
+
// threshold. The next agent_end would re-trigger shake, which has nothing
|
|
7863
|
+
// new to drop on the second pass, so the loop spins until the user kills it.
|
|
7864
|
+
// Same hazard for "incomplete" (the retry would re-hit the length cap) and
|
|
7865
|
+
// for the existing "overflow + nothing reclaimed" case. In every recovery
|
|
7866
|
+
// reason we hand off to the summarization-driven context-full path so the
|
|
7867
|
+
// situation actually resolves; "idle" is exempt because its 60s+ timer
|
|
7868
|
+
// re-checks usage before re-firing and cannot dead-loop on its own.
|
|
7869
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
7870
|
+
const compactionSettings = this.settings.getGroup("compaction");
|
|
7871
|
+
const postShakeTokens = contextWindow > 0 ? this.#estimatePendingPromptTokens([]) : 0;
|
|
7872
|
+
const stillOverThreshold = shouldCompact(postShakeTokens, contextWindow, compactionSettings);
|
|
7873
|
+
const shouldFallBack = reason !== "idle" && ((reason === "overflow" && !reclaimed) || stillOverThreshold);
|
|
7874
|
+
if (shouldFallBack) {
|
|
7875
|
+
const errorMessage = reclaimed
|
|
7876
|
+
? `Auto-shake reclaimed ~${result.tokensFreed} tokens but context is still above the threshold; falling back to context-full compaction.`
|
|
7877
|
+
: "Auto-shake found nothing eligible to drop; falling back to context-full compaction.";
|
|
7843
7878
|
await this.#emitSessionEvent({
|
|
7844
7879
|
type: "auto_compaction_end",
|
|
7845
7880
|
action,
|
|
7846
7881
|
result: undefined,
|
|
7847
7882
|
aborted: false,
|
|
7848
7883
|
willRetry: false,
|
|
7849
|
-
skipped:
|
|
7884
|
+
skipped: !reclaimed,
|
|
7885
|
+
errorMessage,
|
|
7850
7886
|
});
|
|
7851
7887
|
return "fallback";
|
|
7852
7888
|
}
|
|
@@ -100,6 +100,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
100
100
|
runtime.ctx.editor.setText("");
|
|
101
101
|
},
|
|
102
102
|
},
|
|
103
|
+
{
|
|
104
|
+
name: "plan-review",
|
|
105
|
+
description: "Re-open the plan review for the latest plan (plan mode only)",
|
|
106
|
+
handleTui: async (_command, runtime) => {
|
|
107
|
+
await runtime.ctx.openPlanReview();
|
|
108
|
+
runtime.ctx.editor.setText("");
|
|
109
|
+
},
|
|
110
|
+
},
|
|
103
111
|
{
|
|
104
112
|
name: "goal",
|
|
105
113
|
description: "Toggle goal mode (persistent autonomous objective for this session)",
|
package/src/task/index.ts
CHANGED
|
@@ -158,6 +158,8 @@ export const READ_ONLY_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
|
158
158
|
"search_tool_bm25",
|
|
159
159
|
]);
|
|
160
160
|
|
|
161
|
+
const PLAN_MODE_AGENT_TOOL_ALLOWLIST: ReadonlySet<string> = new Set(["ast_grep", "report_finding"]);
|
|
162
|
+
|
|
161
163
|
export function isReadOnlyAgent(agent: AgentDefinition): boolean {
|
|
162
164
|
return !!agent.tools?.length && agent.tools.every(tool => READ_ONLY_TOOL_NAMES.has(tool));
|
|
163
165
|
}
|
|
@@ -677,7 +679,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
677
679
|
}
|
|
678
680
|
|
|
679
681
|
const planModeState = this.session.getPlanModeState?.();
|
|
680
|
-
const
|
|
682
|
+
const planModeBaseTools = ["read", "search", "find", "lsp", "web_search"];
|
|
683
|
+
const planModeTools = [
|
|
684
|
+
...planModeBaseTools,
|
|
685
|
+
...(agent.tools ?? []).filter(
|
|
686
|
+
tool => PLAN_MODE_AGENT_TOOL_ALLOWLIST.has(tool) && !planModeBaseTools.includes(tool),
|
|
687
|
+
),
|
|
688
|
+
];
|
|
681
689
|
const effectiveAgent: typeof agent = planModeState?.enabled
|
|
682
690
|
? {
|
|
683
691
|
...agent,
|
package/src/task/render.ts
CHANGED
|
@@ -632,7 +632,7 @@ function renderAgentProgress(
|
|
|
632
632
|
const indent = prefix ? `${prefix} ` : "";
|
|
633
633
|
let statusLine: string;
|
|
634
634
|
if (progress.status === "running") {
|
|
635
|
-
const bullet = theme.
|
|
635
|
+
const bullet = theme.styledSymbol("status.done", "text");
|
|
636
636
|
const name = theme.fg("accent", description ? theme.bold(displayId) : displayId);
|
|
637
637
|
statusLine = `${indent}${bullet} ${name}`;
|
|
638
638
|
if (description) {
|
|
@@ -640,7 +640,9 @@ function renderAgentProgress(
|
|
|
640
640
|
statusLine += `${theme.fg("accent", ":")} ${desc}`;
|
|
641
641
|
}
|
|
642
642
|
} else {
|
|
643
|
-
|
|
643
|
+
const glyph =
|
|
644
|
+
progress.status === "completed" ? theme.styledSymbol("status.done", "accent") : theme.fg(iconColor, icon);
|
|
645
|
+
statusLine = `${indent}${glyph} ${theme.fg("accent", titlePart)}`;
|
|
644
646
|
}
|
|
645
647
|
|
|
646
648
|
// Show retry-blocked badge so the parent immediately sees that a child
|
|
@@ -809,7 +811,7 @@ function renderReviewResult(
|
|
|
809
811
|
const verdictColor = summary.overall_correctness === "correct" ? "success" : "error";
|
|
810
812
|
const isCorrect = summary.overall_correctness === "correct";
|
|
811
813
|
const verdictIcon = isCorrect
|
|
812
|
-
? theme.styledSymbol("
|
|
814
|
+
? theme.styledSymbol("status.done", "accent")
|
|
813
815
|
: theme.fg(verdictColor, theme.status.error);
|
|
814
816
|
lines.push(
|
|
815
817
|
`${continuePrefix} Patch is ${theme.fg(verdictColor, summary.overall_correctness)} ${verdictIcon} ${theme.fg(
|
|
@@ -916,7 +918,7 @@ function renderAgentResult(
|
|
|
916
918
|
: needsWarning
|
|
917
919
|
? theme.status.warning
|
|
918
920
|
: success
|
|
919
|
-
? theme.styledSymbol("
|
|
921
|
+
? theme.styledSymbol("status.done", "accent")
|
|
920
922
|
: theme.status.error;
|
|
921
923
|
const iconColor = needsWarning ? "warning" : success ? "success" : mergeFailed ? "warning" : "error";
|
|
922
924
|
const statusText = aborted
|
|
@@ -1074,7 +1076,7 @@ function renderAgentResult(
|
|
|
1074
1076
|
* Render the tool result.
|
|
1075
1077
|
*/
|
|
1076
1078
|
export function renderResult(
|
|
1077
|
-
result: { content: Array<{ type: string; text?: string }>; details?: TaskToolDetails },
|
|
1079
|
+
result: { content: Array<{ type: string; text?: string }>; details?: TaskToolDetails; isError?: boolean },
|
|
1078
1080
|
options: RenderResultOptions,
|
|
1079
1081
|
theme: Theme,
|
|
1080
1082
|
args?: TaskParams,
|
|
@@ -1085,18 +1087,25 @@ export function renderResult(
|
|
|
1085
1087
|
|
|
1086
1088
|
if (!details) {
|
|
1087
1089
|
const text = result.content.find(c => c.type === "text")?.text || "";
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
theme
|
|
1091
|
-
|
|
1090
|
+
const errored = result.isError === true;
|
|
1091
|
+
const header = errored
|
|
1092
|
+
? renderStatusLine({ icon: "error", title: "Task", description: args?.agent }, theme)
|
|
1093
|
+
: renderStatusLine(
|
|
1094
|
+
{
|
|
1095
|
+
iconOverride: theme.styledSymbol("status.done", "accent"),
|
|
1096
|
+
title: "Task",
|
|
1097
|
+
description: args?.agent,
|
|
1098
|
+
},
|
|
1099
|
+
theme,
|
|
1100
|
+
);
|
|
1092
1101
|
return framedBlock(theme, width => ({
|
|
1093
1102
|
header,
|
|
1094
1103
|
sections: [
|
|
1095
1104
|
...(contextSectionRenderer ? [contextSectionRenderer(width)] : []),
|
|
1096
1105
|
...(text ? [{ separator: true, lines: [theme.fg("dim", truncateToWidth(text, width))] }] : []),
|
|
1097
1106
|
],
|
|
1098
|
-
state: "success",
|
|
1099
|
-
borderColor: "borderMuted",
|
|
1107
|
+
state: errored ? "error" : "success",
|
|
1108
|
+
borderColor: errored ? "error" : "borderMuted",
|
|
1100
1109
|
width,
|
|
1101
1110
|
}));
|
|
1102
1111
|
}
|
|
@@ -1116,7 +1125,8 @@ export function renderResult(
|
|
|
1116
1125
|
const metaLabel = countLabel ? (agentName ? `${countLabel}: ${agentName}` : countLabel) : agentName;
|
|
1117
1126
|
const header = renderStatusLine(
|
|
1118
1127
|
{
|
|
1119
|
-
icon,
|
|
1128
|
+
icon: icon === "success" ? undefined : icon,
|
|
1129
|
+
iconOverride: icon === "success" ? theme.styledSymbol("status.done", "accent") : undefined,
|
|
1120
1130
|
title: "Task",
|
|
1121
1131
|
meta: metaLabel ? [metaLabel] : undefined,
|
|
1122
1132
|
},
|
package/src/tools/index.ts
CHANGED
|
@@ -59,11 +59,7 @@ import { type TodoPhase, TodoTool } from "./todo";
|
|
|
59
59
|
import { WriteTool } from "./write";
|
|
60
60
|
import { YieldTool } from "./yield";
|
|
61
61
|
|
|
62
|
-
// Exa MCP tools (22 tools)
|
|
63
|
-
|
|
64
62
|
export * from "../edit";
|
|
65
|
-
export * from "../exa";
|
|
66
|
-
export type * from "../exa/types";
|
|
67
63
|
export * from "../goals";
|
|
68
64
|
export * from "../lsp";
|
|
69
65
|
export * from "../session/streaming-output";
|
package/src/utils/git.ts
CHANGED
|
@@ -45,6 +45,10 @@ export interface StageHunksOptions {
|
|
|
45
45
|
readonly rawDiff?: string;
|
|
46
46
|
readonly signal?: AbortSignal;
|
|
47
47
|
}
|
|
48
|
+
export interface HunkSelectionValidationError {
|
|
49
|
+
readonly path: string;
|
|
50
|
+
readonly message: string;
|
|
51
|
+
}
|
|
48
52
|
|
|
49
53
|
export interface DiffOptions {
|
|
50
54
|
readonly allowFailure?: boolean;
|
|
@@ -678,6 +682,43 @@ function selectHunks(file: FileHunks, selector: HunkSelection["hunks"]): FileHun
|
|
|
678
682
|
return file.hunks;
|
|
679
683
|
}
|
|
680
684
|
|
|
685
|
+
export function createHunkSelectionValidator(
|
|
686
|
+
rawDiff: string,
|
|
687
|
+
): (selections: readonly HunkSelection[]) => HunkSelectionValidationError[] {
|
|
688
|
+
const fileDiffMap = new Map(parseFileDiffs(rawDiff).map(entry => [entry.filename, entry]));
|
|
689
|
+
return selections => validateHunkSelectionsFromMap(fileDiffMap, selections);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function validateHunkSelectionsFromMap(
|
|
693
|
+
fileDiffMap: ReadonlyMap<string, FileDiff>,
|
|
694
|
+
selections: readonly HunkSelection[],
|
|
695
|
+
): HunkSelectionValidationError[] {
|
|
696
|
+
const errors: HunkSelectionValidationError[] = [];
|
|
697
|
+
|
|
698
|
+
for (const selection of selections) {
|
|
699
|
+
const fileDiff = fileDiffMap.get(selection.path);
|
|
700
|
+
if (!fileDiff) continue;
|
|
701
|
+
if (selection.hunks.type === "all") continue;
|
|
702
|
+
if (fileDiff.isBinary) {
|
|
703
|
+
errors.push({ path: selection.path, message: `Cannot select hunks for binary file ${selection.path}` });
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const selected = selectHunks(parseFileHunks(fileDiff), selection.hunks);
|
|
707
|
+
if (selected.length === 0) {
|
|
708
|
+
errors.push({ path: selection.path, message: `No hunks selected for ${selection.path}` });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return errors;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export function validateHunkSelections(
|
|
716
|
+
rawDiff: string,
|
|
717
|
+
selections: readonly HunkSelection[],
|
|
718
|
+
): HunkSelectionValidationError[] {
|
|
719
|
+
return createHunkSelectionValidator(rawDiff)(selections);
|
|
720
|
+
}
|
|
721
|
+
|
|
681
722
|
function parseStatusPorcelain(text: string): GitStatusSummary {
|
|
682
723
|
let staged = 0;
|
|
683
724
|
let unstaged = 0;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared factory for creating Exa tools with consistent error handling and response formatting.
|
|
3
|
-
*/
|
|
4
|
-
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
5
|
-
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
6
|
-
import type { ExaRenderDetails } from "./types";
|
|
7
|
-
/** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
|
|
8
|
-
export declare function createExaTool(name: string, label: string, description: string, parameters: TSchema, mcpToolName: string, options?: {
|
|
9
|
-
/** When true, checks isSearchResponse and formats with formatSearchResults. Default: true */
|
|
10
|
-
formatResponse?: boolean;
|
|
11
|
-
/** Transform params before passing to callExaTool */
|
|
12
|
-
transformParams?: (params: Record<string, unknown>) => Record<string, unknown>;
|
|
13
|
-
}): CustomTool<TSchema, ExaRenderDetails>;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Exa TUI Rendering
|
|
3
|
-
*
|
|
4
|
-
* Tree-based rendering with collapsed/expanded states for Exa search results.
|
|
5
|
-
*/
|
|
6
|
-
import type { Component } from "@oh-my-pi/pi-tui";
|
|
7
|
-
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
|
-
import type { Theme } from "../modes/theme/theme";
|
|
9
|
-
import type { ExaRenderDetails } from "./types";
|
|
10
|
-
/** Render Exa result with tree-based layout */
|
|
11
|
-
export declare function renderExaResult(result: {
|
|
12
|
-
content: Array<{
|
|
13
|
-
type: string;
|
|
14
|
-
text?: string;
|
|
15
|
-
}>;
|
|
16
|
-
details?: ExaRenderDetails;
|
|
17
|
-
}, options: RenderResultOptions, uiTheme: Theme): Component;
|
|
18
|
-
/** Render Exa call (query/args preview) */
|
|
19
|
-
export declare function renderExaCall(args: Record<string, unknown>, toolName: string, uiTheme: Theme): Component;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Exa Researcher Tools
|
|
3
|
-
*
|
|
4
|
-
* Async research tasks with polling for completion.
|
|
5
|
-
*/
|
|
6
|
-
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
7
|
-
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
8
|
-
import type { ExaRenderDetails } from "./types";
|
|
9
|
-
export declare const researcherTools: CustomTool<TSchema, ExaRenderDetails>[];
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Exa Search Tools
|
|
3
|
-
*
|
|
4
|
-
* Basic neural/keyword search, deep research, code search, and URL crawling.
|
|
5
|
-
*/
|
|
6
|
-
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
7
|
-
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
8
|
-
import type { ExaRenderDetails } from "./types";
|
|
9
|
-
export declare const searchTools: CustomTool<TSchema, ExaRenderDetails>[];
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Exa Websets Tools
|
|
3
|
-
*
|
|
4
|
-
* CRUD operations for websets, items, searches, enrichments, and monitoring.
|
|
5
|
-
*/
|
|
6
|
-
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
7
|
-
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
8
|
-
import type { ExaRenderDetails } from "./types";
|
|
9
|
-
export declare const websetsTools: CustomTool<TSchema, ExaRenderDetails>[];
|
package/src/exa/factory.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared factory for creating Exa tools with consistent error handling and response formatting.
|
|
3
|
-
*/
|
|
4
|
-
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
5
|
-
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
6
|
-
import { callExaTool, findApiKey, formatGenericResponse, formatSearchResults, isSearchResponse } from "./mcp-client";
|
|
7
|
-
import type { ExaRenderDetails } from "./types";
|
|
8
|
-
|
|
9
|
-
/** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
|
|
10
|
-
export function createExaTool(
|
|
11
|
-
name: string,
|
|
12
|
-
label: string,
|
|
13
|
-
description: string,
|
|
14
|
-
parameters: TSchema,
|
|
15
|
-
mcpToolName: string,
|
|
16
|
-
options?: {
|
|
17
|
-
/** When true, checks isSearchResponse and formats with formatSearchResults. Default: true */
|
|
18
|
-
formatResponse?: boolean;
|
|
19
|
-
/** Transform params before passing to callExaTool */
|
|
20
|
-
transformParams?: (params: Record<string, unknown>) => Record<string, unknown>;
|
|
21
|
-
},
|
|
22
|
-
): CustomTool<TSchema, ExaRenderDetails> {
|
|
23
|
-
const formatResponse = options?.formatResponse ?? true;
|
|
24
|
-
const transformParams = options?.transformParams;
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
name,
|
|
28
|
-
label,
|
|
29
|
-
description,
|
|
30
|
-
parameters,
|
|
31
|
-
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
32
|
-
try {
|
|
33
|
-
const apiKey = findApiKey();
|
|
34
|
-
// Exa MCP endpoint is publicly accessible; API key is optional
|
|
35
|
-
const rawArgs = params as Record<string, unknown>;
|
|
36
|
-
const args = transformParams ? transformParams(rawArgs) : rawArgs;
|
|
37
|
-
const response = await callExaTool(mcpToolName, args, apiKey);
|
|
38
|
-
|
|
39
|
-
if (formatResponse && isSearchResponse(response)) {
|
|
40
|
-
const formatted = formatSearchResults(response);
|
|
41
|
-
return {
|
|
42
|
-
content: [{ type: "text" as const, text: formatted }],
|
|
43
|
-
details: { response, toolName: name },
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
content: [{ type: "text" as const, text: formatGenericResponse(response) }],
|
|
49
|
-
details: { raw: response, toolName: name },
|
|
50
|
-
};
|
|
51
|
-
} catch (error) {
|
|
52
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
-
return {
|
|
54
|
-
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
55
|
-
details: { error: message, toolName: name },
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|