import { Text } from "@earendil-works/pi-tui"; import { Type, type TSchema } from "typebox"; import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent"; import { decide, type Catalog } from "../catalog.ts"; import { paramsToArgs } from "../runner.ts"; import type { ObsidianCliConfig } from "../config.ts"; import type { CliDetails } from "./types.ts"; export function registerIndividualTools(deps: { pi: ExtensionAPI; cat: Catalog; config: ObsidianCliConfig; effectiveConfig: () => ObsidianCliConfig; activeFile: () => { path: string; name: string } | null; individualTools: Set; toolNameFor: (command: string) => string; injectContextDefaults: (cmd: string, params: Record, config: ObsidianCliConfig, active: { path: string; name: string } | null) => { params: Record; defaulted: boolean }; resolveWikilink: (ctx: ExtensionContext, link: string, source?: string) => Promise; executeCommand: (ctx: ExtensionContext, command: string, args: string[], vault?: string, signal?: AbortSignal, options?: Record) => Promise; toToolResult: (details: CliDetails) => any; makeRenderResult: () => any; guidelines: Record; }): void { if (!deps.config.exposeReadOnlyTools) return; const cfg = deps.effectiveConfig(); for (const cmd of deps.cat.commands.values()) { if (cmd.risk !== "read") continue; if (!decide(cfg, cmd, cmd.name).allowed) continue; const toolName = deps.toolNameFor(cmd.name); if (deps.individualTools.has(toolName)) continue; const props: Record = {}; for (const [flag, info] of Object.entries(cmd.flags)) { if (flag.startsWith("<")) { props._arg = Type.Optional(Type.String({ description: info.description })); continue; } if (info.value) { const schema = Type.String({ description: info.description }); props[flag] = info.required ? schema : Type.Optional(schema); } else { props[flag] = Type.Optional(Type.Boolean({ description: info.description })); } } props.vault = Type.Optional(Type.String({ description: "Vault name or id (defaults to the configured/active vault)" })); const capturedCmd = cmd.name; const dedicatedRenderCall = (args: Record, theme: Theme, context: any) => { const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); const tokens = paramsToArgs(args, new Set(["vault"])); let line = theme.fg("toolTitle", theme.bold(`◆ obsidian ${capturedCmd}`)); if (tokens.length > 0) line += theme.fg("dim", ` ${tokens.join(" ")}`); text.setText(line); return text; }; deps.pi.registerTool({ name: toolName, label: `Obsidian ${cmd.name}`, description: `Obsidian vault (read-only): ${cmd.description}. Usage: obsidian ${cmd.name} ${cmd.usage}`.trim(), ...(cfg.promptSnippets ? { promptSnippet: `Obsidian vault: ${cmd.description}` } : {}), ...(deps.guidelines[cmd.name] ? { promptGuidelines: deps.guidelines[cmd.name] } : {}), parameters: Type.Object(props), async execute(_id, params, signal, _onUpdate, ctx) { const { vault, ...rest } = params as Record; const active = deps.activeFile(); const withContext = deps.injectContextDefaults(cmd.name, rest, deps.effectiveConfig(), active); const resolvedParams = { ...withContext.params }; for (const key of ["file", "path"]) { const value = resolvedParams[key]; if (typeof value === "string") { const resolved = await deps.resolveWikilink(ctx, value, active?.path); if (resolved) resolvedParams[key] = resolved; } } const args = paramsToArgs(resolvedParams); const details = await deps.executeCommand(ctx, cmd.name, args, vault as string | undefined, signal, { defaultedFromActiveFile: withContext.defaulted }); return deps.toToolResult(details); }, renderCall: dedicatedRenderCall, renderResult: deps.makeRenderResult(), }); deps.individualTools.add(toolName); } }