/** * tap.ts — thalix-auto Pi extension. * * Most thalix-auto behavior lives in markdown skills under skills/ and prompt * templates under prompts/, which Pi loads natively. This extension exists to: * * 1. Register a /tap umbrella command that lists available /tap-* prompts so * users have a single discoverable entry point. * 2. Expose a `tap_status` tool the LLM can call to report on the active * thalix-auto state directory (~/.tap/) without shelling out. * 3. Subscribe to tool_call events for optional HUD-style telemetry. The * subscriber is a no-op unless TAP_HUD=1 is set. * * Keep this file thin. Discovery and command bodies belong in markdown, not here. * * Test: pi -e ./extensions/tap.ts */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { promises as fs } from "node:fs"; import path from "node:path"; import os from "node:os"; const HUD_ENABLED = process.env.TAP_HUD === "1"; const STATE_DIR_GLOBAL = path.join(os.homedir(), ".tap"); const STATE_DIR_PROJECT = ".tap"; async function readJsonIfExists(p: string): Promise { try { const raw = await fs.readFile(p, "utf8"); return JSON.parse(raw); } catch { return null; } } async function listTapPrompts(piAgentRoot: string): Promise { const dir = path.join(piAgentRoot, "prompts"); try { const entries = await fs.readdir(dir); return entries .filter((f) => f.startsWith("tap-") && f.endsWith(".md")) .map((f) => "/" + f.replace(/\.md$/, "")) .sort(); } catch { return []; } } export default function tap(pi: ExtensionAPI): void { const piAgentRoot = path.join(os.homedir(), ".pi", "agent"); pi.registerCommand("tap", { description: "thalix-auto — list available /tap-* commands and runtime state", handler: async (_args, ctx) => { const prompts = await listTapPrompts(piAgentRoot); const projectState = await readJsonIfExists(path.join(STATE_DIR_PROJECT, "session.json")); const lines: string[] = [ "**thalix-auto** — multi-agent orchestration skill", "", prompts.length ? `Available commands (${prompts.length}):` : "No /tap-* commands installed. Run `tap-skill add --target=pi`.", ...prompts.map((p) => ` ${p}`), "", `State: global=${STATE_DIR_GLOBAL} project=${projectState ? STATE_DIR_PROJECT : "(none)"}`, ]; ctx.ui.notify(lines.join("\n"), "info"); }, }); pi.registerTool({ name: "tap_status", description: "Report thalix-auto runtime state (active project state dir and global config).", parameters: { type: "object", properties: {}, additionalProperties: false } as any, async execute() { const project = await readJsonIfExists(path.join(STATE_DIR_PROJECT, "session.json")); const global = await readJsonIfExists(path.join(STATE_DIR_GLOBAL, "config.json")); return { content: [ { type: "text", text: JSON.stringify( { project_state: project, global_config: global, state_dir_global: STATE_DIR_GLOBAL, state_dir_project: STATE_DIR_PROJECT, }, null, 2, ), }, ], details: {}, }; }, }); if (HUD_ENABLED) { pi.on("tool_call", (event, _ctx) => { // Non-blocking telemetry sink. Hand off to whatever HUD writer is wired in; // here we just stamp it to ~/.tap/hud.log so the shell HUD can tail it. const line = `${new Date().toISOString()} ${event?.name ?? "?"}\n`; fs.mkdir(STATE_DIR_GLOBAL, { recursive: true }) .then(() => fs.appendFile(path.join(STATE_DIR_GLOBAL, "hud.log"), line)) .catch(() => {}); }); } }