import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { StringEnum } from "@earendil-works/pi-ai"; import { Type } from "typebox"; import { formatContext, setupGuidance } from "../context.js"; import { runTod } from "../tod.js"; import type { OneDevSourceWatchManager } from "../source-watch.js"; import { DIGEST_FETCH_BYTES, digestProjects, toolResult, withFooter, type ToolDeps, } from "./common.js"; import { registerBuildTool } from "./build.js"; import { registerIssueTool } from "./issue.js"; import { registerPullTool } from "./pull.js"; import { registerWatchTool } from "./watch.js"; const ONEDEV_TOOL_DOMAINS = ["issue", "pull", "build", "watch"] as const; type OneDevToolDomain = (typeof ONEDEV_TOOL_DOMAINS)[number]; const ALWAYS_ACTIVE_TOOLS = ["onedev_context", "onedev_tools"] as const; const LAZY_TOOL_NAMES = [ "onedev_issue", "onedev_pull", "onedev_build", "onedev_watch", ] as const; const LAZY_TOOLS = new Set(LAZY_TOOL_NAMES); const DOMAIN_TOOL_NAMES = { issue: ["onedev_issue"], pull: ["onedev_pull"], build: ["onedev_build"], watch: ["onedev_watch"], } as const satisfies Record; export interface OnedevToolController { /** Restore only the small context/loader surface at session boundaries. */ reset(): void; } function supportsDynamicTools(pi: ExtensionAPI): boolean { return ( typeof pi.getActiveTools === "function" && typeof pi.setActiveTools === "function" ); } function registerContextTool(pi: ExtensionAPI, deps: ToolDeps): void { pi.registerTool({ name: "onedev_context", label: "OneDev Context", description: "Inspect OneDev readiness, health, login, or setup.", parameters: Type.Object( { action: StringEnum([ "current", "health", "login_name", "setup", "list_projects", ]), count: Type.Optional( Type.Integer({ minimum: 1, maximum: 100, description: "Default: 25" }), ), offset: Type.Optional(Type.Integer({ minimum: 0 })), }, { additionalProperties: false }, ), async execute(_toolCallId, params, signal) { if (params.action === "current") { return toolResult(formatContext(deps.context())); } if (params.action === "setup") { return toolResult(setupGuidance(deps.context())); } if (params.action === "list_projects") { const context = deps.context(); if ( context.status === "not_configured" || context.status === "unavailable" ) { return toolResult(setupGuidance(context)); } const args = ["project", "list", "--count", String(params.count ?? 25)]; if (params.offset !== undefined) args.push("--offset", String(params.offset)); const output = await runTod(deps.exec, args, { cwd: context.cwd, signal, maxOutputBytes: DIGEST_FETCH_BYTES, }); const digested = digestProjects(output.text); return toolResult( withFooter( digested ?? output.text, context, digested === undefined && output.truncated, ), ); } const context = await deps.refreshContext({ verifyAuth: true, force: true, signal, }); if (params.action === "login_name" && context.status === "ready") { return toolResult(context.loginName ?? "(no login name)"); } return toolResult(formatContext(context)); }, }); } export function registerOnedevTools( pi: ExtensionAPI, deps: ToolDeps, watches: OneDevSourceWatchManager, ): OnedevToolController { registerContextTool(pi, deps); registerIssueTool(pi, deps); registerPullTool(pi, deps); registerBuildTool(pi, deps); registerWatchTool(pi, watches); pi.registerTool({ name: "onedev_tools", label: "OneDev Tools", description: "Activate needed OneDev domains and return authenticated context.", promptGuidelines: [ "Use onedev_tools before an unavailable OneDev domain operation.", ], parameters: Type.Object( { action: StringEnum(["activate"]), domains: Type.Array(StringEnum(ONEDEV_TOOL_DOMAINS), { minItems: 1, maxItems: 4, uniqueItems: true, }), }, { additionalProperties: false }, ), async execute(_toolCallId, params, signal) { const context = await deps.refreshContext({ verifyAuth: true, signal }); if (context.status !== "ready") { return toolResult( `${formatContext(context)}\nNo domain tools were activated. Use onedev_context setup for the next step.`, { requested: params.domains, added: [] }, ); } if (!supportsDynamicTools(pi)) { return toolResult( `Dynamic OneDev tool activation is unavailable in this Pi version.\n${formatContext(context)}`, { requested: params.domains, added: [] }, ); } const selected = [ ...new Set( params.domains.flatMap((domain) => DOMAIN_TOOL_NAMES[domain]), ), ]; const active = pi.getActiveTools(); const added = selected.filter((name) => !active.includes(name)); if (added.length > 0) pi.setActiveTools([...active, ...added]); const activation = added.length > 0 ? `Enabled OneDev tools: ${added.join(", ")}` : `Requested OneDev tools already active: ${selected.join(", ")}`; return toolResult(`${activation}\n${formatContext(context)}`, { requested: params.domains, added, }); }, }); return { reset() { if (!supportsDynamicTools(pi)) return; const current = pi.getActiveTools(); const next = current.filter((name) => !LAZY_TOOLS.has(name)); for (const name of ALWAYS_ACTIVE_TOOLS) { if (!next.includes(name)) next.push(name); } if ( next.length !== current.length || next.some((name, index) => name !== current[index]) ) { pi.setActiveTools(next); } }, }; }