a21a542afd
- Extracted prompt rendering and formatting utilities from coding-agent to centralized pi-utils package with new API surface (prompt.render, prompt.format, prompt.registerHelper). - Migrated parseFrontmatter utility from coding-agent to pi-utils package; updated 8 files to import from @oh-my-pi/pi-utils. - Removed 170-line prompt-format.ts module and consolidated 192 lines of Handlebars helper registrations into pi-utils prompt module. - Updated 60+ files across coding-agent and typescript-edit-benchmark to use new prompt.render() and prompt.format() API from pi-utils. - Simplified prompt-templates.ts by delegating core functionality to pi-utils while retaining custom helper registrations (jtdToTypeScript, jsonStringify, etc.).
97 lines
3.1 KiB
TypeScript
97 lines
3.1 KiB
TypeScript
import * as fs from "node:fs/promises";
|
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
import { isEnoent, prompt } from "@oh-my-pi/pi-utils";
|
|
import { type Static, Type } from "@sinclair/typebox";
|
|
import exitPlanModeDescription from "../prompts/tools/exit-plan-mode.md" with { type: "text" };
|
|
import type { ToolSession } from ".";
|
|
import { resolvePlanPath } from "./plan-mode-guard";
|
|
import { ToolError } from "./tool-errors";
|
|
|
|
const exitPlanModeSchema = Type.Object({
|
|
title: Type.String({ description: "Final plan title, e.g. WP_MIGRATION_PLAN" }),
|
|
});
|
|
|
|
type ExitPlanModeParams = Static<typeof exitPlanModeSchema>;
|
|
|
|
function normalizePlanTitle(title: string): { title: string; fileName: string } {
|
|
const trimmed = title.trim();
|
|
if (!trimmed) {
|
|
throw new ToolError("Title is required and must not be empty.");
|
|
}
|
|
|
|
if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("..")) {
|
|
throw new ToolError("Title must not contain path separators or '..'.");
|
|
}
|
|
|
|
const withExtension = trimmed.toLowerCase().endsWith(".md") ? trimmed : `${trimmed}.md`;
|
|
if (!/^[A-Za-z0-9_-]+\.md$/.test(withExtension)) {
|
|
throw new ToolError("Title may only contain letters, numbers, underscores, or hyphens.");
|
|
}
|
|
|
|
const normalizedTitle = withExtension.slice(0, -3);
|
|
return { title: normalizedTitle, fileName: withExtension };
|
|
}
|
|
|
|
export interface ExitPlanModeDetails {
|
|
planFilePath: string;
|
|
planExists: boolean;
|
|
title: string;
|
|
finalPlanFilePath: string;
|
|
}
|
|
|
|
export class ExitPlanModeTool implements AgentTool<typeof exitPlanModeSchema, ExitPlanModeDetails> {
|
|
readonly name = "exit_plan_mode";
|
|
readonly label = "ExitPlanMode";
|
|
readonly description: string;
|
|
readonly parameters = exitPlanModeSchema;
|
|
readonly strict = true;
|
|
readonly concurrency = "exclusive";
|
|
|
|
constructor(private readonly session: ToolSession) {
|
|
this.description = prompt.render(exitPlanModeDescription);
|
|
}
|
|
|
|
async execute(
|
|
_toolCallId: string,
|
|
params: ExitPlanModeParams,
|
|
_signal?: AbortSignal,
|
|
_onUpdate?: AgentToolUpdateCallback<ExitPlanModeDetails>,
|
|
_context?: AgentToolContext,
|
|
): Promise<AgentToolResult<ExitPlanModeDetails>> {
|
|
const state = this.session.getPlanModeState?.();
|
|
if (!state?.enabled) {
|
|
throw new ToolError("Plan mode is not active.");
|
|
}
|
|
|
|
const normalized = normalizePlanTitle(params.title);
|
|
const finalPlanFilePath = `local://${normalized.fileName}`;
|
|
const resolvedPlanPath = resolvePlanPath(this.session, state.planFilePath);
|
|
resolvePlanPath(this.session, finalPlanFilePath);
|
|
let planExists = false;
|
|
try {
|
|
const stat = await fs.stat(resolvedPlanPath);
|
|
planExists = stat.isFile();
|
|
} catch (error) {
|
|
if (!isEnoent(error)) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (!planExists) {
|
|
throw new ToolError(
|
|
`Plan file not found at ${state.planFilePath}. Write the finalized plan to ${state.planFilePath} before calling exit_plan_mode.`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
content: [{ type: "text", text: "Plan ready for approval." }],
|
|
details: {
|
|
planFilePath: state.planFilePath,
|
|
planExists,
|
|
title: normalized.title,
|
|
finalPlanFilePath,
|
|
},
|
|
};
|
|
}
|
|
}
|