From d501ca7d6dfc4666b2c1aaa599e3f45dcf71e3a4 Mon Sep 17 00:00:00 2001 From: ace-pm Date: Wed, 15 Apr 2026 14:34:53 +0200 Subject: [PATCH] fix: clean up git state after directory restoration - Accept deletion of gsd-phase-state.ts (renamed to forge-phase-state.ts earlier) - Accept deletion of create-gsd-extension/ (renamed to create-forge-extension/ earlier) - These renames were part of the rebrand and are preserved in commit history Stabilize git state after restoration operations. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../mcp-server/src/import-candidates.test.ts | 12 +- .../mcp-server/src/workflow-tools.test.ts | 4 +- packages/mcp-server/src/workflow-tools.ts | 46 +- src/cli.ts | 2 +- src/headless-query.ts | 2 +- .../extensions/shared/gsd-phase-state.ts | 42 -- .../skills/create-gsd-extension/SKILL.md | 89 ---- .../references/compaction-session-control.md | 77 --- .../references/custom-commands.md | 139 ----- .../references/custom-rendering.md | 108 ---- .../references/custom-tools.md | 183 ------- .../references/custom-ui.md | 490 ------------------ .../references/events-reference.md | 126 ----- .../references/extension-lifecycle.md | 64 --- .../references/extensionapi-reference.md | 75 --- .../references/extensioncontext-reference.md | 53 -- .../references/key-rules-gotchas.md | 37 -- .../references/mode-behavior.md | 32 -- .../references/model-provider-management.md | 89 ---- .../references/packaging-distribution.md | 55 -- .../references/remote-execution-overrides.md | 90 ---- .../references/state-management.md | 70 --- .../references/system-prompt-modification.md | 52 -- .../templates/extension-skeleton.ts | 51 -- .../templates/stateful-tool-skeleton.ts | 143 ----- .../workflows/add-capability.md | 57 -- .../workflows/create-extension.md | 156 ------ .../workflows/debug-extension.md | 76 --- src/tests/app-smoke.test.ts | 12 +- src/tests/auto-budget.test.ts | 2 +- src/tests/auto-tool-tracking.test.ts | 2 +- .../cross-platform-filesystem-safety.test.ts | 2 +- src/tests/integration/e2e-headless.test.ts | 2 +- src/tests/integration/e2e-smoke.test.ts | 2 +- src/tests/integration/pack-install.test.ts | 2 +- src/tests/integration/web-cli-entry.test.ts | 4 +- .../web-command-parity-contract.test.ts | 2 +- .../web-state-surfaces-contract.test.ts | 2 +- .../web-subprocess-module-resolution.test.ts | 20 +- src/tests/marketplace-discovery.test.ts | 4 +- src/tests/offline-mode.test.ts | 8 +- src/tests/resolve-ts-loader.test.ts | 2 +- src/tests/rtk-execution-seams.test.ts | 2 +- src/tests/token-counter.test.ts | 2 +- src/tests/web-subprocess-runner.test.ts | 4 +- src/web/auto-dashboard-service.ts | 2 +- src/web/bridge-service.ts | 2 +- src/web/captures-service.ts | 4 +- src/web/cleanup-service.ts | 4 +- src/web/doctor-service.ts | 4 +- src/web/export-service.ts | 2 +- src/web/forensics-service.ts | 2 +- src/web/git-summary-service.ts | 2 +- src/web/history-service.ts | 2 +- src/web/hooks-service.ts | 2 +- src/web/notifications-service.ts | 4 +- src/web/recovery-diagnostics-service.ts | 4 +- src/web/settings-service.ts | 10 +- src/web/skill-health-service.ts | 2 +- src/web/subprocess-runner.ts | 2 +- src/web/ts-subprocess-flags.ts | 2 +- src/web/undo-service.ts | 4 +- src/web/visualizer-service.ts | 2 +- src/worktree-cli.ts | 2 +- web/lib/diagnostics-types.ts | 2 +- web/lib/knowledge-captures-types.ts | 2 +- web/lib/remaining-command-types.ts | 2 +- web/lib/settings-types.ts | 2 +- web/lib/visualizer-types.ts | 4 +- 69 files changed, 105 insertions(+), 2459 deletions(-) delete mode 100644 src/resources/extensions/shared/gsd-phase-state.ts delete mode 100644 src/resources/skills/create-gsd-extension/SKILL.md delete mode 100644 src/resources/skills/create-gsd-extension/references/compaction-session-control.md delete mode 100644 src/resources/skills/create-gsd-extension/references/custom-commands.md delete mode 100644 src/resources/skills/create-gsd-extension/references/custom-rendering.md delete mode 100644 src/resources/skills/create-gsd-extension/references/custom-tools.md delete mode 100644 src/resources/skills/create-gsd-extension/references/custom-ui.md delete mode 100644 src/resources/skills/create-gsd-extension/references/events-reference.md delete mode 100644 src/resources/skills/create-gsd-extension/references/extension-lifecycle.md delete mode 100644 src/resources/skills/create-gsd-extension/references/extensionapi-reference.md delete mode 100644 src/resources/skills/create-gsd-extension/references/extensioncontext-reference.md delete mode 100644 src/resources/skills/create-gsd-extension/references/key-rules-gotchas.md delete mode 100644 src/resources/skills/create-gsd-extension/references/mode-behavior.md delete mode 100644 src/resources/skills/create-gsd-extension/references/model-provider-management.md delete mode 100644 src/resources/skills/create-gsd-extension/references/packaging-distribution.md delete mode 100644 src/resources/skills/create-gsd-extension/references/remote-execution-overrides.md delete mode 100644 src/resources/skills/create-gsd-extension/references/state-management.md delete mode 100644 src/resources/skills/create-gsd-extension/references/system-prompt-modification.md delete mode 100644 src/resources/skills/create-gsd-extension/templates/extension-skeleton.ts delete mode 100644 src/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts delete mode 100644 src/resources/skills/create-gsd-extension/workflows/add-capability.md delete mode 100644 src/resources/skills/create-gsd-extension/workflows/create-extension.md delete mode 100644 src/resources/skills/create-gsd-extension/workflows/debug-extension.md diff --git a/packages/mcp-server/src/import-candidates.test.ts b/packages/mcp-server/src/import-candidates.test.ts index 5b0171f3f..5e678b49e 100644 --- a/packages/mcp-server/src/import-candidates.test.ts +++ b/packages/mcp-server/src/import-candidates.test.ts @@ -6,23 +6,23 @@ import { _buildImportCandidates } from "./workflow-tools.js"; describe("_buildImportCandidates", () => { it("includes dist/ fallback for src/ paths", () => { - const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js"); + const candidates = _buildImportCandidates("../../../src/resources/extensions/sf/db-writer.js"); assert.ok( - candidates.some((c) => c.includes("/dist/resources/extensions/gsd/db-writer.js")), + candidates.some((c) => c.includes("/dist/resources/extensions/sf/db-writer.js")), "should include dist/ swapped candidate", ); }); it("includes src/ fallback for dist/ paths", () => { - const candidates = _buildImportCandidates("../../../dist/resources/extensions/gsd/db-writer.js"); + const candidates = _buildImportCandidates("../../../dist/resources/extensions/sf/db-writer.js"); assert.ok( - candidates.some((c) => c.includes("/src/resources/extensions/gsd/db-writer.js")), + candidates.some((c) => c.includes("/src/resources/extensions/sf/db-writer.js")), "should include src/ swapped candidate", ); }); it("includes .ts variants for .js paths", () => { - const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js"); + const candidates = _buildImportCandidates("../../../src/resources/extensions/sf/db-writer.js"); assert.ok( candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/src/")), "should include .ts variant for original src/ path", @@ -34,7 +34,7 @@ describe("_buildImportCandidates", () => { }); it("returns original path first", () => { - const input = "../../../src/resources/extensions/gsd/db-writer.js"; + const input = "../../../src/resources/extensions/sf/db-writer.js"; const candidates = _buildImportCandidates(input); assert.equal(candidates[0], input, "first candidate should be the original path"); }); diff --git a/packages/mcp-server/src/workflow-tools.test.ts b/packages/mcp-server/src/workflow-tools.test.ts index 8435203c6..957ed0a99 100644 --- a/packages/mcp-server/src/workflow-tools.test.ts +++ b/packages/mcp-server/src/workflow-tools.test.ts @@ -5,7 +5,7 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { randomUUID } from "node:crypto"; -import { _getAdapter, closeDatabase } from "../../../src/resources/extensions/gsd/gsd-db.ts"; +import { _getAdapter, closeDatabase } from "../../../src/resources/extensions/sf/gsd-db.ts"; import { registerWorkflowTools, WORKFLOW_TOOL_NAMES } from "./workflow-tools.ts"; function makeTmpBase(): string { @@ -363,7 +363,7 @@ describe("workflow MCP tools", () => { title: "Add planning bridge", description: "Implement the shared executor path.", estimate: "15m", - files: ["src/resources/extensions/gsd/tools/workflow-tool-executors.ts"], + files: ["src/resources/extensions/sf/tools/workflow-tool-executors.ts"], verify: "node --test", inputs: ["ROADMAP.md"], expectedOutput: ["S01-PLAN.md", "T01-PLAN.md"], diff --git a/packages/mcp-server/src/workflow-tools.ts b/packages/mcp-server/src/workflow-tools.ts index ba6986beb..7369490c8 100644 --- a/packages/mcp-server/src/workflow-tools.ts +++ b/packages/mcp-server/src/workflow-tools.ts @@ -329,9 +329,9 @@ function getWriteGateModuleCandidates(): string[] { } candidates.push( - new URL("../../../src/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url).href, - new URL("../../../dist/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url).href, - new URL("../../../src/resources/extensions/gsd/bootstrap/write-gate.ts", import.meta.url).href, + new URL("../../../src/resources/extensions/sf/bootstrap/write-gate.js", import.meta.url).href, + new URL("../../../dist/resources/extensions/sf/bootstrap/write-gate.js", import.meta.url).href, + new URL("../../../src/resources/extensions/sf/bootstrap/write-gate.ts", import.meta.url).href, ); return [...new Set(candidates)]; @@ -387,9 +387,9 @@ function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.en } candidates.push( - new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url).href, - new URL("../../../dist/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url).href, - new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.ts", import.meta.url).href, + new URL("../../../src/resources/extensions/sf/tools/workflow-tool-executors.js", import.meta.url).href, + new URL("../../../dist/resources/extensions/sf/tools/workflow-tool-executors.js", import.meta.url).href, + new URL("../../../src/resources/extensions/sf/tools/workflow-tool-executors.ts", import.meta.url).href, ); return [...new Set(candidates)]; @@ -414,7 +414,7 @@ async function getWorkflowToolExecutors(): Promise { throw new Error( "Unable to load GSD workflow executor bridge for MCP mutation tools. " + "Set GSD_WORKFLOW_EXECUTORS_MODULE to an importable workflow-tool-executors module, " + - "or run the MCP server from a GSD checkout that includes src/resources/extensions/gsd/tools/workflow-tool-executors.(js|ts). " + + "or run the MCP server from a GSD checkout that includes src/resources/extensions/sf/tools/workflow-tool-executors.(js|ts). " + `Attempts: ${attempts.join("; ")}`, ); })(); @@ -516,7 +516,7 @@ async function runSerializedWorkflowDbOperation( ): Promise { return runSerializedWorkflowOperation(async () => { const { ensureDbOpen } = await importLocalModule( - "../../../src/resources/extensions/gsd/bootstrap/dynamic-tools.js", + "../../../src/resources/extensions/sf/bootstrap/dynamic-tools.js", ); const dbAvailable = await ensureDbOpen(projectDir); if (!dbAvailable) { @@ -657,7 +657,7 @@ async function handleSaveGateResult( async function ensureMilestoneDbRow(milestoneId: string): Promise { try { - const { insertMilestone } = await importLocalModule("../../../src/resources/extensions/gsd/gsd-db.js"); + const { insertMilestone } = await importLocalModule("../../../src/resources/extensions/sf/gsd-db.js"); insertMilestone({ id: milestoneId, status: "queued" }); } catch { // Ignore pre-existing rows or transient DB availability issues. @@ -990,7 +990,7 @@ export function registerWorkflowTools(server: McpToolServer): void { const { projectDir, ...params } = parsed; await enforceWorkflowWriteGate("gsd_decision_save", projectDir); const result = await runSerializedWorkflowDbOperation(projectDir, async () => { - const { saveDecisionToDb } = await importLocalModule("../../../src/resources/extensions/gsd/db-writer.js"); + const { saveDecisionToDb } = await importLocalModule("../../../src/resources/extensions/sf/db-writer.js"); return saveDecisionToDb(params, projectDir); }); return { content: [{ type: "text" as const, text: `Saved decision ${result.id}` }] }; @@ -1006,7 +1006,7 @@ export function registerWorkflowTools(server: McpToolServer): void { const { projectDir, ...params } = parsed; await enforceWorkflowWriteGate("gsd_decision_save", projectDir); const result = await runSerializedWorkflowDbOperation(projectDir, async () => { - const { saveDecisionToDb } = await importLocalModule("../../../src/resources/extensions/gsd/db-writer.js"); + const { saveDecisionToDb } = await importLocalModule("../../../src/resources/extensions/sf/db-writer.js"); return saveDecisionToDb(params, projectDir); }); return { content: [{ type: "text" as const, text: `Saved decision ${result.id}` }] }; @@ -1022,7 +1022,7 @@ export function registerWorkflowTools(server: McpToolServer): void { const { projectDir, id, ...updates } = parsed; await enforceWorkflowWriteGate("gsd_requirement_update", projectDir); await runSerializedWorkflowDbOperation(projectDir, async () => { - const { updateRequirementInDb } = await importLocalModule("../../../src/resources/extensions/gsd/db-writer.js"); + const { updateRequirementInDb } = await importLocalModule("../../../src/resources/extensions/sf/db-writer.js"); return updateRequirementInDb(id, updates, projectDir); }); return { content: [{ type: "text" as const, text: `Updated requirement ${id}` }] }; @@ -1038,7 +1038,7 @@ export function registerWorkflowTools(server: McpToolServer): void { const { projectDir, id, ...updates } = parsed; await enforceWorkflowWriteGate("gsd_requirement_update", projectDir); await runSerializedWorkflowDbOperation(projectDir, async () => { - const { updateRequirementInDb } = await importLocalModule("../../../src/resources/extensions/gsd/db-writer.js"); + const { updateRequirementInDb } = await importLocalModule("../../../src/resources/extensions/sf/db-writer.js"); return updateRequirementInDb(id, updates, projectDir); }); return { content: [{ type: "text" as const, text: `Updated requirement ${id}` }] }; @@ -1054,7 +1054,7 @@ export function registerWorkflowTools(server: McpToolServer): void { const { projectDir, ...params } = parsed; await enforceWorkflowWriteGate("gsd_requirement_save", projectDir); const result = await runSerializedWorkflowDbOperation(projectDir, async () => { - const { saveRequirementToDb } = await importLocalModule("../../../src/resources/extensions/gsd/db-writer.js"); + const { saveRequirementToDb } = await importLocalModule("../../../src/resources/extensions/sf/db-writer.js"); return saveRequirementToDb(params, projectDir); }); return { content: [{ type: "text" as const, text: `Saved requirement ${result.id}` }] }; @@ -1070,7 +1070,7 @@ export function registerWorkflowTools(server: McpToolServer): void { const { projectDir, ...params } = parsed; await enforceWorkflowWriteGate("gsd_requirement_save", projectDir); const result = await runSerializedWorkflowDbOperation(projectDir, async () => { - const { saveRequirementToDb } = await importLocalModule("../../../src/resources/extensions/gsd/db-writer.js"); + const { saveRequirementToDb } = await importLocalModule("../../../src/resources/extensions/sf/db-writer.js"); return saveRequirementToDb(params, projectDir); }); return { content: [{ type: "text" as const, text: `Saved requirement ${result.id}` }] }; @@ -1090,7 +1090,7 @@ export function registerWorkflowTools(server: McpToolServer): void { findMilestoneIds, getReservedMilestoneIds, nextMilestoneId, - } = await importLocalModule("../../../src/resources/extensions/gsd/milestone-ids.js"); + } = await importLocalModule("../../../src/resources/extensions/sf/milestone-ids.js"); const reserved = claimReservedId(); if (reserved) { await ensureMilestoneDbRow(reserved); @@ -1118,7 +1118,7 @@ export function registerWorkflowTools(server: McpToolServer): void { findMilestoneIds, getReservedMilestoneIds, nextMilestoneId, - } = await importLocalModule("../../../src/resources/extensions/gsd/milestone-ids.js"); + } = await importLocalModule("../../../src/resources/extensions/sf/milestone-ids.js"); const reserved = claimReservedId(); if (reserved) { await ensureMilestoneDbRow(reserved); @@ -1168,7 +1168,7 @@ export function registerWorkflowTools(server: McpToolServer): void { const { projectDir, ...params } = parsed; await enforceWorkflowWriteGate("gsd_plan_task", projectDir, params.milestoneId); const result = await runSerializedWorkflowDbOperation(projectDir, async () => { - const { handlePlanTask } = await importLocalModule("../../../src/resources/extensions/gsd/tools/plan-task.js"); + const { handlePlanTask } = await importLocalModule("../../../src/resources/extensions/sf/tools/plan-task.js"); return handlePlanTask(params, projectDir); }); if ("error" in result) { @@ -1189,7 +1189,7 @@ export function registerWorkflowTools(server: McpToolServer): void { const { projectDir, ...params } = parsed; await enforceWorkflowWriteGate("gsd_plan_task", projectDir, params.milestoneId); const result = await runSerializedWorkflowDbOperation(projectDir, async () => { - const { handlePlanTask } = await importLocalModule("../../../src/resources/extensions/gsd/tools/plan-task.js"); + const { handlePlanTask } = await importLocalModule("../../../src/resources/extensions/sf/tools/plan-task.js"); return handlePlanTask(params, projectDir); }); if ("error" in result) { @@ -1249,9 +1249,9 @@ export function registerWorkflowTools(server: McpToolServer): void { const { projectDir, milestoneId, sliceId, reason } = parseWorkflowArgs(skipSliceSchema, args); await enforceWorkflowWriteGate("gsd_skip_slice", projectDir, milestoneId); await runSerializedWorkflowDbOperation(projectDir, async () => { - const { getSlice, updateSliceStatus } = await importLocalModule("../../../src/resources/extensions/gsd/gsd-db.js"); - const { invalidateStateCache } = await importLocalModule("../../../src/resources/extensions/gsd/state.js"); - const { rebuildState } = await importLocalModule("../../../src/resources/extensions/gsd/doctor.js"); + const { getSlice, updateSliceStatus } = await importLocalModule("../../../src/resources/extensions/sf/gsd-db.js"); + const { invalidateStateCache } = await importLocalModule("../../../src/resources/extensions/sf/state.js"); + const { rebuildState } = await importLocalModule("../../../src/resources/extensions/sf/doctor.js"); const slice = getSlice(milestoneId, sliceId); if (!slice) { throw new Error(`Slice ${sliceId} not found in milestone ${milestoneId}`); @@ -1402,7 +1402,7 @@ export function registerWorkflowTools(server: McpToolServer): void { journalQueryParams, async (args: Record) => { const { projectDir, limit, ...filters } = parseWorkflowArgs(journalQuerySchema, args); - const { queryJournal } = await importLocalModule("../../../src/resources/extensions/gsd/journal.js"); + const { queryJournal } = await importLocalModule("../../../src/resources/extensions/sf/journal.js"); const entries = queryJournal(projectDir, filters).slice(0, limit ?? 100); if (entries.length === 0) { return { content: [{ type: "text" as const, text: "No matching journal entries found." }] }; diff --git a/src/cli.ts b/src/cli.ts index 4d34274d3..837ff9969 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -32,7 +32,7 @@ import { stopWebMode } from './web-mode.js' import { getProjectSessionsDir } from './project-sessions.js' import { markStartup, printStartupTimings } from './startup-timings.js' import { bootstrapRtk, GSD_RTK_DISABLED_ENV } from './rtk.js' -import { loadEffectiveGSDPreferences } from './resources/extensions/gsd/preferences.js' +import { loadEffectiveGSDPreferences } from './resources/extensions/sf/preferences.js' // --------------------------------------------------------------------------- // V8 compile cache — Node 22+ can cache compiled bytecode across runs, diff --git a/src/headless-query.ts b/src/headless-query.ts index 00816e103..f802e89c9 100644 --- a/src/headless-query.ts +++ b/src/headless-query.ts @@ -18,7 +18,7 @@ import { createJiti } from '@mariozechner/jiti' import { fileURLToPath } from 'node:url' import { join } from 'node:path' import { homedir } from 'node:os' -import type { GSDState } from './resources/extensions/gsd/types.js' +import type { GSDState } from './resources/extensions/sf/types.js' import { resolveBundledSourceResource } from './bundled-resource-path.js' const jiti = createJiti(fileURLToPath(import.meta.url), { interopDefault: true, debug: false }) diff --git a/src/resources/extensions/shared/gsd-phase-state.ts b/src/resources/extensions/shared/gsd-phase-state.ts deleted file mode 100644 index 360410e2a..000000000 --- a/src/resources/extensions/shared/gsd-phase-state.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * GSD Phase State — cross-extension coordination - * Copyright (c) 2026 Jeremy McSpadden - * - * Lightweight module-level state that GSD auto-mode writes to and the - * subagent tool reads from. Both extensions run in the same process so - * a module variable is sufficient — no file I/O needed. - */ - -let _active = false; -let _currentPhase: string | null = null; - -/** Mark GSD auto-mode as active. */ -export function activateGSD(): void { - _active = true; -} - -/** Mark GSD auto-mode as inactive and clear the current phase. */ -export function deactivateGSD(): void { - _active = false; - _currentPhase = null; -} - -/** Set the currently dispatched GSD phase (e.g. "plan-milestone"). */ -export function setCurrentPhase(phase: string): void { - _currentPhase = phase; -} - -/** Clear the current phase (unit completed or aborted). */ -export function clearCurrentPhase(): void { - _currentPhase = null; -} - -/** Returns true if GSD auto-mode is currently active. */ -export function isGSDActive(): boolean { - return _active; -} - -/** Returns the current GSD phase, or null if none is active. */ -export function getCurrentPhase(): string | null { - return _active ? _currentPhase : null; -} diff --git a/src/resources/skills/create-gsd-extension/SKILL.md b/src/resources/skills/create-gsd-extension/SKILL.md deleted file mode 100644 index 28c51efca..000000000 --- a/src/resources/skills/create-gsd-extension/SKILL.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -name: create-gsd-extension -description: Create, debug, and iterate on GSD extensions (TypeScript modules that add tools, commands, event hooks, custom UI, and providers to GSD). Use when asked to build an extension, add a tool the LLM can call, register a slash command, hook into GSD events, create custom TUI components, or modify GSD behavior. Triggers on "create extension", "build extension", "add a tool", "register command", "hook into gsd", "custom tool", "gsd plugin", "gsd extension". ---- - - - -**Extensions are TypeScript modules** that hook into GSD's runtime (built on pi). They export a default function receiving `ExtensionAPI` and use it to subscribe to events, register tools/commands/shortcuts, and interact with the session. - -**GSD extension paths (community/user-installed extensions):** -- Global: `~/.pi/agent/extensions/*.ts` or `~/.pi/agent/extensions/*/index.ts` -- Project-local: `.gsd/extensions/*.ts` or `.gsd/extensions/*/index.ts` - -Note: `~/.gsd/agent/extensions/` is reserved for bundled extensions synced from the gsd-pi package. Community extensions placed there are silently ignored by the loader. - -**The three primitives:** -1. **Events** — Listen and react (`pi.on("event", handler)`). Can block tool calls, modify messages, inject context. -2. **Tools** — Give the LLM new abilities (`pi.registerTool()`). LLM calls them autonomously. -3. **Commands** — Give users slash commands (`pi.registerCommand()`). Users type `/mycommand`. - -**Non-negotiable rules:** -- Use `StringEnum` from `@mariozechner/pi-ai` for string enum params (NOT `Type.Union`/`Type.Literal` — breaks Google's API) -- Truncate tool output to 50KB / 2000 lines max (use `truncateHead`/`truncateTail` from `@mariozechner/pi-coding-agent`) -- Store stateful tool state in `details` for branching support -- Check `signal?.aborted` in long-running tool executions -- Use `pi.exec()` not `child_process` for shell commands -- Check `ctx.hasUI` before dialog methods (non-interactive modes exist) -- Session control methods (`waitForIdle`, `newSession`, `fork`, `navigateTree`, `reload`) are ONLY available in command handlers — they deadlock in event handlers -- Lines from `render()` must not exceed `width` — use `truncateToWidth()` -- Use theme from callback params, never import directly -- Strip leading `@` from path params in custom tools (some models add it) - -**Available imports:** - -| Package | Purpose | -|---------|---------| -| `@mariozechner/pi-coding-agent` | `ExtensionAPI`, `ExtensionContext`, `Theme`, event types, tool utilities, `DynamicBorder`, `BorderedLoader`, `CustomEditor`, `highlightCode` | -| `@sinclair/typebox` | `Type.Object`, `Type.String`, `Type.Number`, `Type.Optional`, `Type.Boolean`, `Type.Array` | -| `@mariozechner/pi-ai` | `StringEnum` (required for string enums), `Type` re-export | -| `@mariozechner/pi-tui` | `Text`, `Box`, `Container`, `Spacer`, `Markdown`, `SelectList`, `Input`, `matchesKey`, `Key`, `truncateToWidth`, `visibleWidth` | -| Node.js built-ins | `node:fs`, `node:path`, `node:child_process`, etc. | - - - - -Based on user intent, route to the appropriate workflow: - -**Building a new extension:** -- "Create an extension", "build a tool", "I want to add a command" → `workflows/create-extension.md` - -**Adding capabilities to an existing extension:** -- "Add a tool to my extension", "add event hook", "add custom rendering" → `workflows/add-capability.md` - -**Debugging an extension:** -- "My extension doesn't work", "tool not showing up", "event not firing" → `workflows/debug-extension.md` - -**If user intent is clear from context, skip the question and go directly to the workflow.** - - - -All domain knowledge in `references/`: - -**Core architecture:** extension-lifecycle.md, events-reference.md -**API surface:** extensionapi-reference.md, extensioncontext-reference.md -**Capabilities:** custom-tools.md, custom-commands.md, custom-ui.md, custom-rendering.md -**Patterns:** state-management.md, system-prompt-modification.md, compaction-session-control.md -**Infrastructure:** model-provider-management.md, remote-execution-overrides.md, packaging-distribution.md, mode-behavior.md -**Gotchas:** key-rules-gotchas.md - - - -| Workflow | Purpose | -|----------|---------| -| create-extension.md | Build a new extension from scratch | -| add-capability.md | Add tools, commands, hooks, UI to an existing extension | -| debug-extension.md | Diagnose and fix extension issues | - - - -Extension is complete when: -- TypeScript compiles without errors (jiti handles this at runtime) -- Extension loads on GSD startup or `/reload` without errors -- Tools appear in the LLM's system prompt and are callable -- Commands respond to `/command` input -- Event hooks fire at the expected lifecycle points -- Custom UI renders correctly within terminal width -- State persists correctly across session restarts (if stateful) -- Output is truncated to safe limits (if tools produce variable output) - diff --git a/src/resources/skills/create-gsd-extension/references/compaction-session-control.md b/src/resources/skills/create-gsd-extension/references/compaction-session-control.md deleted file mode 100644 index 9826955fb..000000000 --- a/src/resources/skills/create-gsd-extension/references/compaction-session-control.md +++ /dev/null @@ -1,77 +0,0 @@ - -Custom compaction hooks, triggering compaction, and session control methods available only in command handlers. - - - -Override default compaction behavior: - -```typescript -pi.on("session_before_compact", async (event, ctx) => { - const { preparation, branchEntries, customInstructions, signal } = event; - - // Option 1: Cancel - return { cancel: true }; - - // Option 2: Custom summary - return { - compaction: { - summary: "Custom summary of conversation so far...", - firstKeptEntryId: preparation.firstKeptEntryId, - tokensBefore: preparation.tokensBefore, - } - }; -}); -``` - - - -Trigger compaction programmatically from any handler: - -```typescript -ctx.compact({ - customInstructions: "Focus on the authentication changes", - onComplete: (result) => ctx.ui.notify("Compacted!", "info"), - onError: (error) => ctx.ui.notify(`Failed: ${error.message}`, "error"), -}); -``` - - - -**Only available in command handlers** (deadlocks in event handlers): - -```typescript -pi.registerCommand("handoff", { - handler: async (args, ctx) => { - await ctx.waitForIdle(); - - // Create new session with initial context - const result = await ctx.newSession({ - parentSession: ctx.sessionManager.getSessionFile(), - setup: async (sm) => { - sm.appendMessage({ - role: "user", - content: [{ type: "text", text: `Context: ${args}` }], - timestamp: Date.now(), - }); - }, - }); - - if (result.cancelled) { /* extension cancelled via session_before_switch */ } - }, -}); -``` - -| Method | Purpose | -|--------|---------| -| `ctx.waitForIdle()` | Wait for agent to finish streaming | -| `ctx.newSession(options?)` | Create a new session | -| `ctx.fork(entryId)` | Fork from a specific entry | -| `ctx.navigateTree(targetId, options?)` | Navigate session tree (with optional summary) | -| `ctx.reload()` | Hot-reload everything (treat as terminal — code after runs pre-reload version) | - -`navigateTree` options: -- `summarize: boolean` — generate summary of abandoned branch -- `customInstructions: string` — instructions for summarizer -- `replaceInstructions: boolean` — replace default prompt entirely -- `label: string` — label to attach to branch summary - diff --git a/src/resources/skills/create-gsd-extension/references/custom-commands.md b/src/resources/skills/create-gsd-extension/references/custom-commands.md deleted file mode 100644 index 43a6c0676..000000000 --- a/src/resources/skills/create-gsd-extension/references/custom-commands.md +++ /dev/null @@ -1,139 +0,0 @@ - -Custom slash commands — registration, argument completions, subcommand patterns, and the extended command context. - - - -```typescript -pi.registerCommand("deploy", { - description: "Deploy to an environment", - handler: async (args, ctx) => { - // args = everything after "/deploy " - // ctx = ExtensionCommandContext (has session control methods) - ctx.ui.notify(`Deploying to ${args || "production"}`, "info"); - }, -}); -``` - - - -Add tab-completion for command arguments: - -```typescript -import type { AutocompleteItem } from "@mariozechner/pi-tui"; - -pi.registerCommand("deploy", { - description: "Deploy to an environment", - getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => { - const envs = ["dev", "staging", "prod"]; - const items = envs.map(e => ({ value: e, label: e })); - const filtered = items.filter(i => i.value.startsWith(prefix)); - return filtered.length > 0 ? filtered : null; - }, - handler: async (args, ctx) => { - ctx.ui.notify(`Deploying to ${args}`, "info"); - }, -}); -``` - - - -Fake nested commands via first-argument parsing. Used by `/wt new|ls|switch|merge|rm`. - -```typescript -pi.registerCommand("foo", { - description: "Manage foo items: /foo new|list|delete [name]", - - getArgumentCompletions: (prefix: string) => { - const parts = prefix.trim().split(/\s+/); - - // First arg: subcommand - if (parts.length <= 1) { - return ["new", "list", "delete"] - .filter(cmd => cmd.startsWith(parts[0] ?? "")) - .map(cmd => ({ value: cmd, label: cmd })); - } - - // Second arg: depends on subcommand - if (parts[0] === "delete") { - const items = getItemsSomehow(); - return items - .filter(name => name.startsWith(parts[1] ?? "")) - .map(name => ({ value: `delete ${name}`, label: name })); - } - - return []; - }, - - handler: async (args, ctx) => { - const parts = args.trim().split(/\s+/); - const sub = parts[0]; - - switch (sub) { - case "new": /* ... */ return; - case "list": /* ... */ return; - case "delete": /* handle parts[1] */ return; - default: - ctx.ui.notify("Usage: /foo [name]", "info"); - } - }, -}); -``` - -**Gotcha:** `"".trim().split(/\s+/)` produces `['']`, not `[]`. That's why `parts.length <= 1` handles both empty and partial first arg. - - - -Command handlers get `ExtensionCommandContext` which extends `ExtensionContext` with session control methods: - -| Method | Purpose | -|--------|---------| -| `ctx.waitForIdle()` | Wait for agent to finish streaming | -| `ctx.newSession(options?)` | Create a new session | -| `ctx.fork(entryId)` | Fork from an entry | -| `ctx.navigateTree(targetId, options?)` | Navigate session tree | -| `ctx.reload()` | Hot-reload everything | - -**⚠️ These methods are ONLY available in command handlers.** Calling them from event handlers causes deadlocks. - -```typescript -pi.registerCommand("handoff", { - handler: async (args, ctx) => { - await ctx.waitForIdle(); - await ctx.newSession({ - setup: async (sm) => { - sm.appendMessage({ - role: "user", - content: [{ type: "text", text: `Context: ${args}` }], - timestamp: Date.now(), - }); - }, - }); - }, -}); -``` - - - -Expose reload as both a command and a tool the LLM can call: - -```typescript -pi.registerCommand("reload-runtime", { - description: "Reload extensions, skills, prompts, and themes", - handler: async (_args, ctx) => { - await ctx.reload(); - return; // Treat reload as terminal - }, -}); - -pi.registerTool({ - name: "reload_runtime", - label: "Reload Runtime", - description: "Reload extensions, skills, prompts, and themes", - parameters: Type.Object({}), - async execute() { - pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" }); - return { content: [{ type: "text", text: "Queued /reload-runtime as follow-up." }] }; - }, -}); -``` - diff --git a/src/resources/skills/create-gsd-extension/references/custom-rendering.md b/src/resources/skills/create-gsd-extension/references/custom-rendering.md deleted file mode 100644 index 8572af6d2..000000000 --- a/src/resources/skills/create-gsd-extension/references/custom-rendering.md +++ /dev/null @@ -1,108 +0,0 @@ - -Custom rendering for tools and messages — control how they appear in the TUI. - - - -Tools can provide `renderCall` (how the call looks) and `renderResult` (how the result looks): - -```typescript -import { Text } from "@mariozechner/pi-tui"; -import { keyHint } from "@mariozechner/pi-coding-agent"; - -pi.registerTool({ - name: "my_tool", - // ... - - renderCall(args, theme) { - let text = theme.fg("toolTitle", theme.bold("my_tool ")); - text += theme.fg("muted", args.action); - if (args.text) text += " " + theme.fg("dim", `"${args.text}"`); - return new Text(text, 0, 0); // 0,0 padding — Box handles it - }, - - renderResult(result, { expanded, isPartial }, theme) { - // isPartial = true during streaming (onUpdate was called) - if (isPartial) { - return new Text(theme.fg("warning", "Processing..."), 0, 0); - } - - // expanded = user toggled expand (Ctrl+O) - if (result.details?.error) { - return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0); - } - - let text = theme.fg("success", "✓ Done"); - if (!expanded) { - text += ` (${keyHint("expandTools", "to expand")})`; - } - if (expanded && result.details?.items) { - for (const item of result.details.items) { - text += "\n " + theme.fg("dim", item); - } - } - return new Text(text, 0, 0); - }, -}); -``` - -If you omit `renderCall`/`renderResult`, the built-in renderer is used. Useful for tool overrides where you just wrap logic without reimplementing UI. - -**Fallback:** If render methods throw, `renderCall` shows tool name, `renderResult` shows raw `content` text. - - - -Key hint helpers for showing keybinding info in render output: - -```typescript -import { keyHint, appKeyHint, editorKey, rawKeyHint } from "@mariozechner/pi-coding-agent"; - -// Editor action hint (respects user keybinding config) -keyHint("expandTools", "to expand") // e.g., "Ctrl+O to expand" -keyHint("selectConfirm", "to select") - -// Raw key hint (always shows literal key) -rawKeyHint("Ctrl+O", "to expand") -``` - - - -Register a renderer for custom message types: - -```typescript -import { Text } from "@mariozechner/pi-tui"; - -pi.registerMessageRenderer("my-extension", (message, options, theme) => { - const { expanded } = options; - let text = theme.fg("accent", `[${message.customType}] `) + message.content; - if (expanded && message.details) { - text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2)); - } - return new Text(text, 0, 0); -}); - -// Send messages that use this renderer: -pi.sendMessage({ - customType: "my-extension", // Matches renderer name - content: "Status update", - display: true, - details: { foo: "bar" }, -}); -``` - - - -```typescript -import { highlightCode, getLanguageFromPath } from "@mariozechner/pi-coding-agent"; - -const lang = getLanguageFromPath("/path/to/file.rs"); // "rust" -const highlighted = highlightCode(code, lang, theme); -``` - - - -- Return `Text` with padding `(0, 0)` — the wrapping `Box` handles padding -- Support `expanded` for detail on demand -- Handle `isPartial` for streaming progress -- Keep collapsed view compact -- Use `\n` for multi-line content within a single `Text` - diff --git a/src/resources/skills/create-gsd-extension/references/custom-tools.md b/src/resources/skills/create-gsd-extension/references/custom-tools.md deleted file mode 100644 index acc1b4361..000000000 --- a/src/resources/skills/create-gsd-extension/references/custom-tools.md +++ /dev/null @@ -1,183 +0,0 @@ - -Complete custom tools reference — registration, parameters, execution, output truncation, overrides, rendering, and dynamic registration. - - - -```typescript -import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@mariozechner/pi-ai"; - -pi.registerTool({ - name: "my_tool", // Unique identifier (snake_case) - label: "My Tool", // Display name in TUI - description: "What this does", // Full description shown to LLM - - // Optional: one-liner for system prompt "Available tools" section - promptSnippet: "Manage project todo items", - - // Optional: bullets added to system prompt "Guidelines" when tool is active - promptGuidelines: [ - "Use my_tool for task management instead of file edits." - ], - - // Parameter schema (MUST use TypeBox) - parameters: Type.Object({ - action: StringEnum(["list", "add", "remove"] as const), - text: Type.Optional(Type.String({ description: "Item text" })), - id: Type.Optional(Type.Number({ description: "Item ID" })), - }), - - async execute(toolCallId, params, signal, onUpdate, ctx) { - // 1. Check cancellation - if (signal?.aborted) { - return { content: [{ type: "text", text: "Cancelled" }] }; - } - - // 2. Stream progress (optional) - onUpdate?.({ - content: [{ type: "text", text: "Working..." }], - details: { progress: 50 }, - }); - - // 3. Do the work - const result = await doWork(params); - - // 4. Return result - return { - content: [{ type: "text", text: "Result text for LLM" }], // Sent to LLM context - details: { data: result }, // For rendering & state - }; - }, - - // Optional: custom TUI rendering - renderCall(args, theme) { ... }, - renderResult(result, { expanded, isPartial }, theme) { ... }, -}); -``` - - - -**⚠️ MUST use `StringEnum` for string enum parameters:** - -```typescript -import { StringEnum } from "@mariozechner/pi-ai"; - -// ✅ Correct — works with all providers including Google -action: StringEnum(["list", "add", "remove"] as const) - -// ❌ BROKEN with Google's API -action: Type.Union([Type.Literal("list"), Type.Literal("add")]) -``` - - - -Tools MUST truncate output to avoid context overflow. Built-in limit: 50KB / 2000 lines. - -```typescript -import { - truncateHead, truncateTail, formatSize, - DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, -} from "@mariozechner/pi-coding-agent"; - -async execute(toolCallId, params, signal, onUpdate, ctx) { - const output = await runCommand(); - const truncation = truncateHead(output, { - maxLines: DEFAULT_MAX_LINES, - maxBytes: DEFAULT_MAX_BYTES, - }); - - let result = truncation.content; - if (truncation.truncated) { - const tempFile = writeTempFile(output); - result += `\n\n[Output truncated: ${truncation.outputLines}/${truncation.totalLines} lines`; - result += ` (${formatSize(truncation.outputBytes)}/${formatSize(truncation.totalBytes)}).`; - result += ` Full output: ${tempFile}]`; - } - return { content: [{ type: "text", text: result }] }; -} -``` - -Use `truncateHead` when beginning matters (search results, file reads). Use `truncateTail` when end matters (logs, command output). - - - -Throw to signal an error (sets `isError: true`). Returning a value never sets error flag. - -```typescript -async execute(toolCallId, params) { - if (!isValid(params.input)) { - throw new Error(`Invalid input: ${params.input}`); - } - return { content: [{ type: "text", text: "OK" }], details: {} }; -} -``` - - - -Tools can be registered at any time — during load, in `session_start`, in command handlers. Available immediately without `/reload`. - -```typescript -pi.on("session_start", async (_event, ctx) => { - pi.registerTool({ name: "dynamic_tool", ... }); -}); -``` - -Use `pi.setActiveTools(names)` to enable/disable tools at runtime. - - - -Register a tool with the same name as a built-in (`read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`) to override it. **Must match exact result shape including `details` type.** - -```typescript -import { createReadTool } from "@mariozechner/pi-coding-agent"; - -pi.registerTool({ - name: "read", - label: "Read (Logged)", - description: "Read file contents with logging", - parameters: Type.Object({ - path: Type.String(), - offset: Type.Optional(Type.Number()), - limit: Type.Optional(Type.Number()), - }), - async execute(toolCallId, params, signal, onUpdate, ctx) { - console.log(`[AUDIT] Reading: ${params.path}`); - const builtIn = createReadTool(ctx.cwd); - return builtIn.execute(toolCallId, params, signal, onUpdate); - }, - // Omit renderCall/renderResult to use built-in renderer -}); -``` - -Start with no built-in tools: `gsd --no-tools -e ./my-extension.ts` - - - -One extension can register multiple tools with shared state: - -```typescript -export default function (pi: ExtensionAPI) { - let connection = null; - - pi.registerTool({ name: "db_connect", ... }); - pi.registerTool({ name: "db_query", ... }); - pi.registerTool({ name: "db_close", ... }); - - pi.on("session_shutdown", async () => { - connection?.close(); - }); -} -``` - - - -Some models add `@` prefix to path arguments. Strip it: - -```typescript -async execute(toolCallId, params, signal, onUpdate, ctx) { - let path = params.path; - if (path.startsWith("@")) path = path.slice(1); - // ... -} -``` - diff --git a/src/resources/skills/create-gsd-extension/references/custom-ui.md b/src/resources/skills/create-gsd-extension/references/custom-ui.md deleted file mode 100644 index 7eeaadc2a..000000000 --- a/src/resources/skills/create-gsd-extension/references/custom-ui.md +++ /dev/null @@ -1,490 +0,0 @@ - -Complete custom UI reference — dialogs, persistent elements, custom components, overlays, custom editors, built-in components, keyboard input, performance, theming, and common mistakes. - - - -``` -┌─────────────────────────────────────────────────┐ -│ Custom Header (ctx.ui.setHeader) │ -├─────────────────────────────────────────────────┤ -│ Message Area │ -│ - User/assistant messages │ -│ - Tool calls ◄── renderCall/renderResult │ -│ - Custom messages ◄── registerMessageRenderer │ -├─────────────────────────────────────────────────┤ -│ Widgets (above editor) ◄── ctx.ui.setWidget │ -├─────────────────────────────────────────────────┤ -│ Editor ◄── ctx.ui.custom() / setEditorComponent│ -├─────────────────────────────────────────────────┤ -│ Widgets (below editor) ◄── ctx.ui.setWidget │ -├─────────────────────────────────────────────────┤ -│ Footer ◄── ctx.ui.setFooter / setStatus │ -└─────────────────────────────────────────────────┘ - ┌─────────────────────┐ - │ Overlay (floating) │ ◄── ctx.ui.custom({ overlay }) - └─────────────────────┘ -``` - -**11 ways to get UI on screen:** - -| Method | Blocks? | Replaces editor? | -|--------|---------|-------------------| -| `ctx.ui.select/confirm/input/editor` | Yes | Temporarily | -| `ctx.ui.notify` | No | No | -| `ctx.ui.setStatus` | No | No (footer) | -| `ctx.ui.setWidget` | No | No | -| `ctx.ui.setFooter` | No | No (replaces footer) | -| `ctx.ui.setHeader` | No | No (replaces header) | -| `ctx.ui.custom()` | Yes | Temporarily | -| `ctx.ui.custom({overlay})` | Yes | No (renders on top) | -| `ctx.ui.setEditorComponent` | No | Yes (permanently) | -| `renderCall/renderResult` | No | No (inline in messages) | -| `registerMessageRenderer` | No | No (inline in messages) | - - - -Every visual element implements: - -```typescript -interface Component { - render(width: number): string[]; // Required — each line ≤ width visible chars - handleInput?(data: string): void; // Optional — receive keyboard input - wantsKeyRelease?: boolean; // Optional — receive key release events (Kitty protocol) - invalidate(): void; // Required — clear cached render state -} -``` - -**Render contract:** -- Return array of strings, one per line -- Each string MUST NOT exceed `width` in visible characters -- ANSI escape codes don't count toward visible width -- **Styles are reset at end of each line** — reapply per line -- Return `[]` for zero-height component - -**Invalidation contract:** -- Clear ALL cached render output -- Clear any pre-baked themed strings -- Call `super.invalidate()` if extending a built-in component - - - -Blocking dialog methods on `ctx.ui`: - -```typescript -const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]); // string | undefined -const ok = await ctx.ui.confirm("Delete?", "This cannot be undone"); // boolean -const name = await ctx.ui.input("Name:", "placeholder"); // string | undefined -const text = await ctx.ui.editor("Edit:", "prefilled text"); // string | undefined - -// Timed auto-dismiss with countdown -const ok = await ctx.ui.confirm("Proceed?", "Auto-continues in 5s", { timeout: 5000 }); -// Returns false on timeout, undefined for select/input - -// Manual dismissal with AbortSignal (distinguish timeout from cancel) -const controller = new AbortController(); -const timeoutId = setTimeout(() => controller.abort(), 5000); -const ok = await ctx.ui.confirm("Timed", "Auto-cancels in 5s", { signal: controller.signal }); -clearTimeout(timeoutId); -if (controller.signal.aborted) { /* timed out */ } -``` - - - -```typescript -// Footer status (multiple extensions can set independent entries) -ctx.ui.setStatus("my-ext", "● Active"); -ctx.ui.setStatus("my-ext", undefined); // Clear - -// Widgets -ctx.ui.setWidget("my-id", ["Line 1", "Line 2"]); // Above editor -ctx.ui.setWidget("my-id", ["Below"], { placement: "belowEditor" }); // Below editor -ctx.ui.setWidget("my-id", (_tui, theme) => ({ // Themed - render: () => [theme.fg("accent", "Styled")], - invalidate: () => {}, -})); -ctx.ui.setWidget("my-id", undefined); // Clear - -// Working message during streaming -ctx.ui.setWorkingMessage("Analyzing code..."); -ctx.ui.setWorkingMessage(); // Restore default - -// Custom footer (full replacement) -ctx.ui.setFooter((tui, theme, footerData) => ({ - render(width) { - const branch = footerData.getGitBranch(); // Only available here - const statuses = footerData.getExtensionStatuses(); // All setStatus values - return [truncateToWidth(`${branch} | model`, width)]; - }, - invalidate() {}, - dispose: footerData.onBranchChange(() => tui.requestRender()), // Reactive -})); -ctx.ui.setFooter(undefined); // Restore default - -// Custom header -ctx.ui.setHeader((tui, theme) => ({ - render(width) { return [theme.fg("accent", theme.bold("My Header"))]; }, - invalidate() {}, -})); - -// Editor control -ctx.ui.setEditorText("Prefill"); -const current = ctx.ui.getEditorText(); -ctx.ui.pasteToEditor("pasted content"); // Triggers paste handling - -// Tool expansion -ctx.ui.setToolsExpanded(true); -const expanded = ctx.ui.getToolsExpanded(); - -// Theme management -const themes = ctx.ui.getAllThemes(); -ctx.ui.setTheme("light"); -ctx.ui.theme.fg("accent", "text"); // Access current theme -``` - - - -`ctx.ui.custom()` temporarily replaces the editor. Returns a value when `done()` is called. - -**Factory callback args:** - -| Argument | Type | Purpose | -|----------|------|---------| -| `tui` | `TUI` | `tui.requestRender()` triggers re-render after state changes | -| `theme` | `Theme` | Current theme for styling | -| `keybindings` | `KeybindingsManager` | App keybinding config | -| `done` | `(value: T) => void` | Close component and return value | - -**Inline pattern:** -```typescript -const result = await ctx.ui.custom((tui, theme, keybindings, done) => ({ - render(width: number): string[] { - return [truncateToWidth("Press Enter to confirm, Escape to cancel", width)]; - }, - handleInput(data: string) { - if (matchesKey(data, Key.enter)) done("confirmed"); - if (matchesKey(data, Key.escape)) done(null); - }, - invalidate() {}, -})); -``` - -**Class-based pattern (recommended for complex UI):** -```typescript -class MyComponent { - private selected = 0; - private cachedWidth?: number; - private cachedLines?: string[]; - - constructor( - private tui: { requestRender: () => void }, - private theme: Theme, - private items: string[], - private done: (value: string | null) => void, - ) {} - - handleInput(data: string) { - if (matchesKey(data, Key.up) && this.selected > 0) this.selected--; - else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) this.selected++; - else if (matchesKey(data, Key.enter)) { this.done(this.items[this.selected]); return; } - else if (matchesKey(data, Key.escape)) { this.done(null); return; } - else return; - this.invalidate(); - this.tui.requestRender(); - } - - render(width: number): string[] { - if (this.cachedLines && this.cachedWidth === width) return this.cachedLines; - this.cachedLines = this.items.map((item, i) => - truncateToWidth((i === this.selected ? "> " : " ") + item, width) - ); - this.cachedWidth = width; - return this.cachedLines; - } - - invalidate() { this.cachedWidth = undefined; this.cachedLines = undefined; } -} - -const result = await ctx.ui.custom((tui, theme, _kb, done) => - new MyComponent(tui, theme, ["A", "B", "C"], done) -); -``` - -**Composing with built-in components:** -```typescript -const result = await ctx.ui.custom((tui, theme, _kb, done) => { - const container = new Container(); - container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s))); - container.addChild(new Text(theme.fg("accent", theme.bold("Title")), 1, 0)); - - const selectList = new SelectList(items, 10, { - selectedPrefix: (t) => theme.fg("accent", t), - selectedText: (t) => theme.fg("accent", t), - description: (t) => theme.fg("muted", t), - scrollInfo: (t) => theme.fg("dim", t), - noMatch: (t) => theme.fg("warning", t), - }); - selectList.onSelect = (item) => done(item.value); - selectList.onCancel = () => done(null); - container.addChild(selectList); - - return { - render: (w) => container.render(w), - invalidate: () => container.invalidate(), - handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); }, - }; -}); -``` - - - -Floating modals rendered on top of everything: - -```typescript -const result = await ctx.ui.custom( - (tui, theme, _kb, done) => new MyDialog({ onClose: done }), - { - overlay: true, - overlayOptions: { - anchor: "center", // 9 positions (see below) - width: "50%", // number = columns, string = percentage - minWidth: 40, - maxHeight: "80%", - margin: 2, // All sides, or { top, right, bottom, left } - offsetX: 0, offsetY: 0, // Fine-tune position - visible: (w, h) => w >= 80, // Hide on narrow terminals - }, - onHandle: (handle) => { - // handle.setHidden(true/false) — temporarily hide - // handle.hide() — permanently remove - }, - } -); -``` - -**Anchor positions:** -``` -top-left top-center top-right -left-center center right-center -bottom-left bottom-center bottom-right -``` - -**Stacked overlays:** Multiple overlays stack (newest on top). Closing one gives focus to the one below. - -**⚠️ Overlay lifecycle:** Components are disposed when closed. Never reuse references — create fresh instances each time. - - - -Replace the main input editor permanently: - -```typescript -import { CustomEditor } from "@mariozechner/pi-coding-agent"; - -class VimEditor extends CustomEditor { - private mode: "normal" | "insert" = "insert"; - - handleInput(data: string): void { - if (matchesKey(data, "escape") && this.mode === "insert") { - this.mode = "normal"; return; - } - if (this.mode === "insert") { super.handleInput(data); return; } - switch (data) { - case "i": this.mode = "insert"; return; - case "h": super.handleInput("\x1b[D"); return; // Left - case "j": super.handleInput("\x1b[B"); return; // Down - case "k": super.handleInput("\x1b[A"); return; // Up - case "l": super.handleInput("\x1b[C"); return; // Right - } - if (data.length === 1 && data.charCodeAt(0) >= 32) return; // Block printable in normal - super.handleInput(data); - } -} - -ctx.ui.setEditorComponent((_tui, theme, keybindings) => new VimEditor(theme, keybindings)); -ctx.ui.setEditorComponent(undefined); // Restore default -``` - -**Critical:** Extend `CustomEditor` (NOT `Editor`) to get app keybindings (escape to abort, ctrl+d, model switching). - - - -**From `@mariozechner/pi-tui`:** - -| Component | Constructor | Purpose | -|-----------|-------------|---------| -| `Text` | `new Text(content, paddingX, paddingY, bgFn?)` | Multi-line text with word wrap | -| `Box` | `new Box(paddingX, paddingY, bgFn)` | Container with padding+background, `.addChild()` | -| `Container` | `new Container()` | Vertical stack, `.addChild()`, `.removeChild()`, `.clear()` | -| `Spacer` | `new Spacer(lines)` | Empty vertical space | -| `Markdown` | `new Markdown(content, padX, padY, getMarkdownTheme())` | Rendered markdown with syntax highlighting | -| `Image` | `new Image(base64, mimeType, theme, opts?)` | Image rendering (Kitty, iTerm2) | -| `SelectList` | `new SelectList(items, maxVisible, themeOpts)` | Interactive selection with search and scrolling | -| `SettingsList` | `new SettingsList(items, maxVisible, theme, onChange, onClose, opts?)` | Toggle settings with left/right arrows | -| `Input` | `new Input()` | Text input field | -| `Editor` | `new Editor(tui, editorTheme)` | Multi-line editor with undo | - -**SelectList usage:** -```typescript -const items: SelectItem[] = [ - { value: "opt1", label: "Option 1", description: "First option" }, - { value: "opt2", label: "Option 2" }, -]; -const selectList = new SelectList(items, 10, { - selectedPrefix: (t) => theme.fg("accent", t), - selectedText: (t) => theme.fg("accent", t), - description: (t) => theme.fg("muted", t), - scrollInfo: (t) => theme.fg("dim", t), - noMatch: (t) => theme.fg("warning", t), -}); -selectList.onSelect = (item) => { /* item.value */ }; -selectList.onCancel = () => { /* escape pressed */ }; -``` - -**SettingsList usage:** -```typescript -const items: SettingItem[] = [ - { id: "verbose", label: "Verbose mode", currentValue: "off", values: ["on", "off"] }, - { id: "theme", label: "Theme", currentValue: "dark", values: ["dark", "light", "auto"] }, -]; -const settings = new SettingsList(items, 15, getSettingsListTheme(), - (id, newValue) => { /* setting changed */ }, - () => { /* close requested */ }, - { enableSearch: true }, -); -``` - -**From `@mariozechner/pi-coding-agent`:** - -| Component | Constructor | Purpose | -|-----------|-------------|---------| -| `DynamicBorder` | `new DynamicBorder((s: string) => theme.fg("accent", s))` | Border line | -| `BorderedLoader` | — | Spinner with cancel support | -| `CustomEditor` | `new CustomEditor(theme, keybindings)` | Base class for custom editors | - - - -```typescript -import { matchesKey, Key } from "@mariozechner/pi-tui"; - -handleInput(data: string) { - // Basic keys - if (matchesKey(data, Key.up)) {} - if (matchesKey(data, Key.down)) {} - if (matchesKey(data, Key.enter)) {} - if (matchesKey(data, Key.escape)) {} - if (matchesKey(data, Key.tab)) {} - if (matchesKey(data, Key.space)) {} - if (matchesKey(data, Key.backspace)) {} - if (matchesKey(data, Key.home)) {} - if (matchesKey(data, Key.end)) {} - - // With modifiers - if (matchesKey(data, Key.ctrl("c"))) {} - if (matchesKey(data, Key.shift("tab"))) {} - if (matchesKey(data, Key.alt("left"))) {} - if (matchesKey(data, Key.ctrlShift("p"))) {} - - // String format also works: "enter", "ctrl+c", "shift+tab" - - // Printable character detection - if (data.length === 1 && data.charCodeAt(0) >= 32) { - // Letter, number, symbol - } -} -``` - -**handleInput contract:** -1. Check for your keys -2. Update state -3. Call `this.invalidate()` if render output changes -4. Call `tui.requestRender()` to trigger re-render - - - -**Cardinal rule: each line from render() must not exceed `width` visible characters.** - -```typescript -import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui"; - -visibleWidth("\x1b[32mHello\x1b[0m"); // Returns 5 (ignores ANSI codes) -truncateToWidth("Very long text here", 10); // "Very lo..." -truncateToWidth("Very long text here", 10, ""); // "Very long " (no ellipsis) -wrapTextWithAnsi("\x1b[32mLong green text\x1b[0m", 10); // Word wrap preserving ANSI -``` - -If lines exceed `width`, terminal wraps cause visual corruption. - - - -Always cache render output: - -```typescript -class CachedComponent { - private cachedWidth?: number; - private cachedLines?: string[]; - - render(width: number): string[] { - if (this.cachedLines && this.cachedWidth === width) return this.cachedLines; - const lines = this.computeLines(width); - this.cachedWidth = width; - this.cachedLines = lines; - return lines; - } - - invalidate() { this.cachedWidth = undefined; this.cachedLines = undefined; } -} -``` - -**Update cycle:** State changes → `invalidate()` → `tui.requestRender()` → `render(width)` called - -**Game loop pattern** (real-time updates): -```typescript -this.interval = setInterval(() => { - this.tick(); - this.version++; - this.tui.requestRender(); -}, 100); // 10 FPS - -// Clean up in dispose() -clearInterval(this.interval); -``` - - - -Always use theme from callback params, never import directly. - -**All foreground colors:** - -| Category | Colors | -|----------|--------| -| General | `text`, `accent`, `muted`, `dim` | -| Status | `success`, `error`, `warning` | -| Borders | `border`, `borderAccent`, `borderMuted` | -| Messages | `userMessageText`, `customMessageText`, `customMessageLabel` | -| Tools | `toolTitle`, `toolOutput` | -| Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` | -| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` | -| Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` | -| Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh` | - -**All background colors:** `selectedBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg` - -**Syntax highlighting:** -```typescript -import { highlightCode, getLanguageFromPath } from "@mariozechner/pi-coding-agent"; -const lang = getLanguageFromPath("/file.rs"); // "rust" -const highlighted = highlightCode(code, lang, theme); -``` - - - -1. **Lines exceed width** → Visual corruption. Use `truncateToWidth()` on every line. -2. **Forgetting `tui.requestRender()`** → UI doesn't update. Call after invalidate(). -3. **Importing theme directly** → Wrong colors after theme switch. Use theme from callback. -4. **Not typing DynamicBorder param** → `new DynamicBorder((s: string) => theme.fg("accent", s))`. -5. **Reusing disposed overlay components** → Create fresh instances each time. -6. **Styles bleeding across lines** → TUI resets per line. Reapply styles, or use `wrapTextWithAnsi()`. -7. **Not implementing invalidate()** → Theme changes don't take effect. -8. **Forgetting super.invalidate()** → `override invalidate() { super.invalidate(); /* cleanup */ }` -9. **Timer not cleaned up** → Call `clearInterval` before `done()`. -10. **Using ctx.ui in non-interactive mode** → Check `ctx.hasUI` first. - diff --git a/src/resources/skills/create-gsd-extension/references/events-reference.md b/src/resources/skills/create-gsd-extension/references/events-reference.md deleted file mode 100644 index 593eeaf10..000000000 --- a/src/resources/skills/create-gsd-extension/references/events-reference.md +++ /dev/null @@ -1,126 +0,0 @@ - -Complete event reference with handler signatures, return types, and type narrowing utilities. - - - - -**Session events:** `session_start`, `session_before_switch`, `session_switch`, `session_before_fork`, `session_fork`, `session_before_compact`, `session_compact`, `session_before_tree`, `session_tree`, `session_shutdown` - -**Agent events:** `before_agent_start`, `agent_start`, `agent_end`, `turn_start`, `turn_end`, `context`, `before_provider_request`, `message_start`, `message_update`, `message_end` - -**Tool events:** `tool_call`, `tool_execution_start`, `tool_execution_update`, `tool_execution_end`, `tool_result` - -**Input events:** `input` - -**Model events:** `model_select` - -**User bash events:** `user_bash` - -**Special:** `session_directory` (CLI startup only, no `ctx` — receives only event) - - - - -```typescript -pi.on("event_name", async (event, ctx: ExtensionContext) => { - // event — typed payload for this event - // ctx — access to UI, session, model, control flow - // Return undefined for no action, or a typed response -}); -``` - - - - -**before_agent_start** — Fired after user prompt, before agent loop. Primary hook for context injection and system prompt modification. -```typescript -pi.on("before_agent_start", async (event, ctx) => { - // event.prompt — user's prompt text - // event.images — attached images - // event.systemPrompt — current system prompt - return { - message: { customType: "my-ext", content: "Extra context", display: true }, - systemPrompt: event.systemPrompt + "\n\nExtra instructions...", - }; -}); -``` - -**tool_call** — Fired before tool executes. Can block. -```typescript -import { isToolCallEventType } from "@mariozechner/pi-coding-agent"; - -pi.on("tool_call", async (event, ctx) => { - if (isToolCallEventType("bash", event)) { - // event.input is typed as { command: string; timeout?: number } - if (event.input.command.includes("rm -rf")) { - return { block: true, reason: "Dangerous command" }; - } - } -}); -``` - -**tool_result** — Fired after tool executes. Can modify result. Handlers chain like middleware. -```typescript -import { isToolResultEventType } from "@mariozechner/pi-coding-agent"; - -pi.on("tool_result", async (event, ctx) => { - if (isToolResultEventType("bash", event)) { - // event.details is typed as BashToolDetails - } - // Return partial patch: { content, details, isError } - // Omitted fields keep current values -}); -``` - -**context** — Fired before each LLM call. Modify messages non-destructively. -```typescript -pi.on("context", async (event, ctx) => { - // event.messages is a deep copy — safe to modify - const filtered = event.messages.filter(m => !shouldPrune(m)); - return { messages: filtered }; -}); -``` - -**input** — Fired when user input is received, before skill/template expansion. -```typescript -pi.on("input", async (event, ctx) => { - // event.text — raw input - // event.source — "interactive", "rpc", or "extension" - if (event.text.startsWith("?quick ")) - return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` }; - return { action: "continue" }; -}); -``` - -**model_select** — Fired when model changes. -```typescript -pi.on("model_select", async (event, ctx) => { - // event.model, event.previousModel, event.source ("set"|"cycle"|"restore") -}); -``` - - - - -Built-in type guards for tool events: - -```typescript -import { isToolCallEventType, isToolResultEventType } from "@mariozechner/pi-coding-agent"; - -// Tool calls — narrows event.input type -if (isToolCallEventType("bash", event)) { /* event.input: { command, timeout? } */ } -if (isToolCallEventType("read", event)) { /* event.input: { path, offset?, limit? } */ } -if (isToolCallEventType("write", event)) { /* event.input: { path, content } */ } -if (isToolCallEventType("edit", event)) { /* event.input: { path, oldText, newText } */ } - -// Tool results — narrows event.details type -if (isToolResultEventType("bash", event)) { /* event.details: BashToolDetails */ } -``` - -For custom tools, export your input type and use explicit type params: -```typescript -if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) { - event.input.action; // typed -} -``` - diff --git a/src/resources/skills/create-gsd-extension/references/extension-lifecycle.md b/src/resources/skills/create-gsd-extension/references/extension-lifecycle.md deleted file mode 100644 index d6ab71c42..000000000 --- a/src/resources/skills/create-gsd-extension/references/extension-lifecycle.md +++ /dev/null @@ -1,64 +0,0 @@ - -The extension lifecycle from load to shutdown, including the full event flow. - - - -Extensions load when GSD starts (or on `/reload`). The default export function runs synchronously — subscribe to events and register tools/commands during this call. - -``` -GSD starts - └─► Extension default function runs - ├── pi.on("event", handler) ← Subscribe - ├── pi.registerTool({...}) ← Register tools - ├── pi.registerCommand(...) ← Register commands - └── pi.registerShortcut(...) ← Register shortcuts - └─► session_start fires -``` - - - -Full event flow per user prompt: - -``` -user sends prompt - ├─► Extension commands checked (bypass if match) - ├─► input event (can intercept/transform/handle) - ├─► Skill/template expansion - ├─► before_agent_start (inject message, modify system prompt) - ├─► agent_start - │ - │ ┌── Turn loop (repeats while LLM calls tools) ──┐ - │ │ turn_start │ - │ │ context (can modify messages sent to LLM) │ - │ │ before_provider_request (inspect/replace payload)│ - │ │ LLM responds → may call tools: │ - │ │ tool_call (can BLOCK) │ - │ │ tool_execution_start/update/end │ - │ │ tool_result (can MODIFY) │ - │ │ turn_end │ - │ └────────────────────────────────────────────────┘ - │ - └─► agent_end -``` - - - -| Event | When | Can Return | -|-------|------|------------| -| `session_start` | Session loads | — | -| `session_before_switch` | Before `/new` or `/resume` | `{ cancel: true }` | -| `session_switch` | After switch | — | -| `session_before_fork` | Before `/fork` | `{ cancel: true }`, `{ skipConversationRestore: true }` | -| `session_fork` | After fork | — | -| `session_before_compact` | Before compaction | `{ cancel: true }`, `{ compaction: {...} }` | -| `session_compact` | After compaction | — | -| `session_shutdown` | On exit | — | - - - -Extensions in auto-discovered locations hot-reload with `/reload`: -- `session_shutdown` fires for old runtime -- Resources re-scanned -- `session_start` fires for new runtime -- Code after `await ctx.reload()` still runs from the pre-reload version — treat as terminal - diff --git a/src/resources/skills/create-gsd-extension/references/extensionapi-reference.md b/src/resources/skills/create-gsd-extension/references/extensionapi-reference.md deleted file mode 100644 index 7e5f458df..000000000 --- a/src/resources/skills/create-gsd-extension/references/extensionapi-reference.md +++ /dev/null @@ -1,75 +0,0 @@ - -ExtensionAPI methods — the `pi` object received in the default export function. - - - -| Method | Purpose | -|--------|---------| -| `pi.on(event, handler)` | Subscribe to events | -| `pi.registerTool(definition)` | Register LLM-callable tool | -| `pi.registerCommand(name, options)` | Register `/command` | -| `pi.registerShortcut(key, options)` | Register keyboard shortcut | -| `pi.registerFlag(name, options)` | Register CLI flag | -| `pi.registerMessageRenderer(customType, renderer)` | Custom message rendering | -| `pi.registerProvider(name, config)` | Register/override model provider | -| `pi.unregisterProvider(name)` | Remove a provider | - - - -| Method | Purpose | -|--------|---------| -| `pi.sendMessage(message, options?)` | Inject custom message into session | -| `pi.sendUserMessage(content, options?)` | Send user message (triggers turn) | - -**Delivery modes for `sendMessage`:** -- `"steer"` (default) — Interrupts streaming after current tool -- `"followUp"` — Waits for agent to finish all tools -- `"nextTurn"` — Queued for next user prompt - -```typescript -pi.sendMessage({ - customType: "my-extension", - content: "Additional context", - display: true, - details: { ... }, -}, { deliverAs: "steer", triggerTurn: true }); -``` - - - -| Method | Purpose | -|--------|---------| -| `pi.appendEntry(customType, data?)` | Persist state (NOT sent to LLM) | -| `pi.setSessionName(name)` | Set session display name | -| `pi.getSessionName()` | Get session name | -| `pi.setLabel(entryId, label)` | Bookmark entry for `/tree` | - - - -```typescript -const active = pi.getActiveTools(); // ["read", "bash", "edit", "write"] -const all = pi.getAllTools(); // [{ name, description }, ...] -pi.setActiveTools(["read", "bash"]); // Enable/disable tools -``` - - - -```typescript -const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5"); -if (model) { - const success = await pi.setModel(model); // Returns false if no API key -} - -pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" -pi.setThinkingLevel("high"); -``` - - - -| Method | Purpose | -|--------|---------| -| `pi.exec(cmd, args, opts?)` | Shell command (prefer over child_process) | -| `pi.events` | Shared event bus for inter-extension communication | -| `pi.getFlag(name)` | Get CLI flag value | -| `pi.getCommands()` | All available slash commands | - diff --git a/src/resources/skills/create-gsd-extension/references/extensioncontext-reference.md b/src/resources/skills/create-gsd-extension/references/extensioncontext-reference.md deleted file mode 100644 index ab04ad711..000000000 --- a/src/resources/skills/create-gsd-extension/references/extensioncontext-reference.md +++ /dev/null @@ -1,53 +0,0 @@ - -ExtensionContext (`ctx`) — available in all event handlers (except `session_directory`). - - - -**Dialogs (blocking — wait for user response):** -```typescript -const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]); -const ok = await ctx.ui.confirm("Delete?", "This cannot be undone"); -const name = await ctx.ui.input("Name:", "placeholder"); -const text = await ctx.ui.editor("Edit:", "prefilled text"); - -// Timed dialog — auto-dismiss after timeout -const ok = await ctx.ui.confirm("Auto-confirm?", "Proceeds in 5s", { timeout: 5000 }); -``` - -**Non-blocking UI:** -```typescript -ctx.ui.notify("Done!", "info"); // Toast: "info" | "warning" | "error" -ctx.ui.setStatus("my-ext", "● Active"); // Footer status -ctx.ui.setStatus("my-ext", undefined); // Clear -ctx.ui.setWidget("my-id", ["Line 1", "Line 2"]); // Widget above editor -ctx.ui.setWidget("my-id", ["Below!"], { placement: "belowEditor" }); -ctx.ui.setTitle("gsd - my project"); // Terminal title -ctx.ui.setEditorText("Prefill"); // Set editor content -ctx.ui.setWorkingMessage("Analyzing..."); // Working message during streaming -ctx.ui.setToolsExpanded(true); // Expand tool output -``` - - - -| Property/Method | Purpose | -|----------------|---------| -| `ctx.hasUI` | `false` in print/JSON mode — check before dialogs | -| `ctx.cwd` | Current working directory | -| `ctx.sessionManager` | Read-only session state | -| `ctx.modelRegistry` / `ctx.model` | Model access | -| `ctx.isIdle()` / `ctx.abort()` / `ctx.hasPendingMessages()` | Agent state | -| `ctx.shutdown()` | Request graceful exit (deferred until idle) | -| `ctx.getContextUsage()` | Current context token usage | -| `ctx.compact(options?)` | Trigger compaction | -| `ctx.getSystemPrompt()` | Current effective system prompt | - - - -```typescript -ctx.sessionManager.getEntries() // All entries -ctx.sessionManager.getBranch() // Current branch -ctx.sessionManager.getLeafId() // Current leaf entry ID -ctx.sessionManager.getSessionFile() // Session JSONL path -ctx.sessionManager.getLabel(entryId) // Entry label -``` - diff --git a/src/resources/skills/create-gsd-extension/references/key-rules-gotchas.md b/src/resources/skills/create-gsd-extension/references/key-rules-gotchas.md deleted file mode 100644 index 11b300677..000000000 --- a/src/resources/skills/create-gsd-extension/references/key-rules-gotchas.md +++ /dev/null @@ -1,37 +0,0 @@ - -Non-negotiable rules and common gotchas when building GSD extensions. - - - -1. **Use `StringEnum` for string enums** — `Type.Union`/`Type.Literal` breaks Google's API. -2. **Truncate tool output** — Large output causes context overflow, compaction failures, degraded performance. Limit: 50KB / 2000 lines. -3. **Use theme from callback** — Don't import theme directly. Use the `theme` parameter from `ctx.ui.custom()` or render functions. -4. **`DynamicBorder` color param** — Type as `(s: string) => theme.fg("accent", s)`. -5. **Call `tui.requestRender()` after state changes** in `handleInput`. -6. **Return `{ render, invalidate, handleInput }`** from custom components. -7. **Lines must not exceed `width`** in `render()` — use `truncateToWidth()`. -8. **Session control methods ONLY in commands** — `waitForIdle()`, `newSession()`, `fork()`, `navigateTree()`, `reload()` will **deadlock** in event handlers. -9. **Strip leading `@` from path arguments** — some models add it. -10. **Store state in tool result `details`** for proper branching support. - - - -- Rebuild component on `invalidate()` when pre-baking theme colors -- Check `signal?.aborted` in long-running tool executions -- Use `pi.exec()` instead of `child_process` for shell commands -- Overlay components are **disposed when closed** — create fresh instances each time -- Treat `ctx.reload()` as terminal — code after runs from pre-reload version -- Check `ctx.hasUI` before dialog methods (false in print/JSON mode) -- Extension errors are logged but don't crash GSD — tool_call handler errors fail-safe (block the tool) - - - -**GSD extension paths (community/user-installed extensions):** -- Global: `~/.pi/agent/extensions/*.ts` -- Global (subdir): `~/.pi/agent/extensions/*/index.ts` -- Project-local: `.gsd/extensions/*.ts` -- Project-local (subdir): `.gsd/extensions/*/index.ts` - -Note: `~/.gsd/agent/extensions/` is reserved for bundled extensions synced from the gsd-pi package. -Community extensions placed there are silently ignored by the loader. - diff --git a/src/resources/skills/create-gsd-extension/references/mode-behavior.md b/src/resources/skills/create-gsd-extension/references/mode-behavior.md deleted file mode 100644 index 3e8db5822..000000000 --- a/src/resources/skills/create-gsd-extension/references/mode-behavior.md +++ /dev/null @@ -1,32 +0,0 @@ - -Mode behavior determines which UI methods work. Extensions may run in non-interactive modes where dialogs are unavailable. - - - -| Mode | UI Methods | Notes | -|------|-----------|-------| -| **Interactive** (default) | Full TUI | Normal operation — all UI works | -| **RPC** (`--mode rpc`) | JSON protocol | Host handles UI, dialogs work via sub-protocol | -| **JSON** (`--mode json`) | No-op | Event stream to stdout, no UI | -| **Print** (`-p`) | No-op | Extensions run but can't prompt users | - - - -**Always check `ctx.hasUI`** before calling dialog methods: - -```typescript -if (ctx.hasUI) { - const ok = await ctx.ui.confirm("Delete?", "Sure?"); - if (!ok) return; -} else { - // Default behavior for non-interactive mode - // Or just proceed without confirmation -} -``` - -`ctx.hasUI` is `false` in print mode (`-p`) and JSON mode. `true` in interactive and RPC mode. - - - -Non-blocking methods (`notify`, `setStatus`, `setWidget`, `setTitle`, `setEditorText`) are safe in all modes — they're no-ops when no UI is available. - diff --git a/src/resources/skills/create-gsd-extension/references/model-provider-management.md b/src/resources/skills/create-gsd-extension/references/model-provider-management.md deleted file mode 100644 index c57d5fb22..000000000 --- a/src/resources/skills/create-gsd-extension/references/model-provider-management.md +++ /dev/null @@ -1,89 +0,0 @@ - -Model and provider management — switching models, registering custom providers with OAuth, and reacting to model changes. - - - -```typescript -const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5"); -if (model) { - const success = await pi.setModel(model); - if (!success) ctx.ui.notify("No API key for this model", "error"); -} - -// Thinking level -pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" -pi.setThinkingLevel("high"); // Clamped to model capabilities -``` - - - -```typescript -pi.registerProvider("my-proxy", { - baseUrl: "https://proxy.example.com", - apiKey: "PROXY_API_KEY", // Env var name or literal - api: "anthropic-messages", // or "openai-completions", "openai-responses" - headers: { "X-Custom": "value" }, // Optional custom headers - authHeader: true, // Auto-add Authorization: Bearer header - models: [ - { - id: "claude-sonnet-4-20250514", - name: "Claude 4 Sonnet (proxy)", - reasoning: false, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 200000, - maxTokens: 16384, - } - ], -}); - -// Override just baseUrl for an existing provider (keeps all models) -pi.registerProvider("anthropic", { - baseUrl: "https://proxy.example.com", -}); - -// Remove a provider (restores any overridden built-in models) -pi.unregisterProvider("my-proxy"); -``` - -Takes effect immediately after initial load phase — no `/reload` required. - - - -Register a provider with OAuth support for `/login`: - -```typescript -pi.registerProvider("corporate-ai", { - baseUrl: "https://ai.corp.com", - api: "openai-responses", - models: [/* ... */], - oauth: { - name: "Corporate AI (SSO)", - async login(callbacks) { - callbacks.onAuth({ url: "https://sso.corp.com/..." }); - const code = await callbacks.onPrompt({ message: "Enter code:" }); - return { refresh: code, access: code, expires: Date.now() + 3600000 }; - }, - async refreshToken(credentials) { - return credentials; // Refresh logic - }, - getApiKey(credentials) { - return credentials.access; - }, - }, -}); -``` - - - -React to model changes: - -```typescript -pi.on("model_select", async (event, ctx) => { - // event.model — newly selected model - // event.previousModel — previous model (undefined if first) - // event.source — "set" | "cycle" | "restore" - ctx.ui.setStatus("model", `${event.model.provider}/${event.model.id}`); -}); -``` - diff --git a/src/resources/skills/create-gsd-extension/references/packaging-distribution.md b/src/resources/skills/create-gsd-extension/references/packaging-distribution.md deleted file mode 100644 index a3d603182..000000000 --- a/src/resources/skills/create-gsd-extension/references/packaging-distribution.md +++ /dev/null @@ -1,55 +0,0 @@ - -Packaging extensions for distribution via npm, git, or local paths. Creating GSD/pi packages. - - - -Add a `pi` manifest to `package.json`: - -```json -{ - "name": "my-gsd-package", - "keywords": ["pi-package"], - "pi": { - "extensions": ["./extensions"], - "skills": ["./skills"], - "prompts": ["./prompts"], - "themes": ["./themes"] - } -} -``` - - - -```bash -gsd install npm:@foo/bar@1.0.0 -gsd install git:github.com/user/repo@v1 -gsd install ./local/path - -# Try without installing: -gsd -e npm:@foo/bar -``` - - - -If no `pi` manifest exists, auto-discovers: -- `extensions/` → `.ts` and `.js` files -- `skills/` → `SKILL.md` folders -- `prompts/` → `.md` files -- `themes/` → `.json` files - - - -- List `@mariozechner/pi-ai`, `@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@sinclair/typebox` in `peerDependencies` with `"*"` — they're bundled by the runtime. -- Other npm deps go in `dependencies`. The runtime runs `npm install` on package installation. - - - -```json -{ - "pi": { - "video": "https://example.com/demo.mp4", - "image": "https://example.com/screenshot.png" - } -} -``` - diff --git a/src/resources/skills/create-gsd-extension/references/remote-execution-overrides.md b/src/resources/skills/create-gsd-extension/references/remote-execution-overrides.md deleted file mode 100644 index 1945fb80e..000000000 --- a/src/resources/skills/create-gsd-extension/references/remote-execution-overrides.md +++ /dev/null @@ -1,90 +0,0 @@ - -Remote execution via pluggable operations, spawnHook for bash, and tool override patterns. - - - -Built-in tools support pluggable operations for SSH, containers, etc.: - -```typescript -import { createReadTool, createBashTool, createWriteTool } from "@mariozechner/pi-coding-agent"; - -// Create tool with custom remote operations -const remoteBash = createBashTool(cwd, { - operations: { - execute: (cmd) => sshExec(remote, cmd), - }, -}); -``` - -**Operations interfaces:** `ReadOperations`, `WriteOperations`, `EditOperations`, `BashOperations`, `LsOperations`, `GrepOperations`, `FindOperations` - - - -The bash tool supports a `spawnHook` to modify commands before execution: - -```typescript -const bashTool = createBashTool(cwd, { - spawnHook: ({ command, cwd, env }) => ({ - command: `source ~/.profile\n${command}`, - cwd: `/mnt/sandbox${cwd}`, - env: { ...env, CI: "1" }, - }), -}); -``` - - - -Full SSH pattern with flag-based switching: - -```typescript -import { createBashTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent"; - -export default function (pi: ExtensionAPI) { - pi.registerFlag("ssh", { description: "SSH target", type: "string" }); - - const localBash = createBashTool(process.cwd()); - - pi.registerTool({ - ...localBash, - async execute(id, params, signal, onUpdate, ctx) { - const sshTarget = pi.getFlag("--ssh"); - if (sshTarget) { - const remoteBash = createBashTool(process.cwd(), { - operations: createSSHOperations(sshTarget), - }); - return remoteBash.execute(id, params, signal, onUpdate); - } - return localBash.execute(id, params, signal, onUpdate); - }, - }); -} -``` - - - -Override built-in tools for logging/access control — omit renderCall/renderResult to keep built-in rendering: - -```typescript -import { createReadTool } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; - -pi.registerTool({ - name: "read", // Same name = overrides built-in - label: "Read (Logged)", - description: "Read file contents with logging", - parameters: Type.Object({ - path: Type.String(), - offset: Type.Optional(Type.Number()), - limit: Type.Optional(Type.Number()), - }), - async execute(toolCallId, params, signal, onUpdate, ctx) { - console.log(`[AUDIT] Reading: ${params.path}`); - const builtIn = createReadTool(ctx.cwd); - return builtIn.execute(toolCallId, params, signal, onUpdate); - }, - // Omit renderCall/renderResult → built-in renderer used automatically -}); -``` - -**Must match exact result shape** including `details` type. - diff --git a/src/resources/skills/create-gsd-extension/references/state-management.md b/src/resources/skills/create-gsd-extension/references/state-management.md deleted file mode 100644 index 717293cb0..000000000 --- a/src/resources/skills/create-gsd-extension/references/state-management.md +++ /dev/null @@ -1,70 +0,0 @@ - -State management patterns for extensions — tool result details (branch-safe) and appendEntry (private). - - - -**Recommended for stateful tools.** State in `details` works correctly with branching/forking. - -```typescript -export default function (pi: ExtensionAPI) { - let items: string[] = []; - - // Reconstruct state from session on load - pi.on("session_start", async (_event, ctx) => reconstructState(ctx)); - pi.on("session_switch", async (_event, ctx) => reconstructState(ctx)); - pi.on("session_fork", async (_event, ctx) => reconstructState(ctx)); - pi.on("session_tree", async (_event, ctx) => reconstructState(ctx)); - - const reconstructState = (ctx: ExtensionContext) => { - items = []; - for (const entry of ctx.sessionManager.getBranch()) { - if (entry.type === "message" && entry.message.role === "toolResult") { - if (entry.message.toolName === "my_tool") { - items = entry.message.details?.items ?? []; - } - } - } - }; - - pi.registerTool({ - name: "my_tool", - // ... - async execute(toolCallId, params, signal, onUpdate, ctx) { - items.push(params.text); - return { - content: [{ type: "text", text: "Added" }], - details: { items: [...items] }, // ← Snapshot full state - }; - }, - }); -} -``` - -**Key:** Reconstruct on ALL session change events: `session_start`, `session_switch`, `session_fork`, `session_tree`. - - - -**For extension-private state** that doesn't participate in LLM context but needs to survive restarts: - -```typescript -// Save -pi.appendEntry("my-state", { count: 42, lastRun: Date.now() }); - -// Restore -pi.on("session_start", async (_event, ctx) => { - for (const entry of ctx.sessionManager.getEntries()) { - if (entry.type === "custom" && entry.customType === "my-state") { - const data = entry.data; // { count: 42, lastRun: ... } - } - } -}); -``` - - - -| Pattern | Use When | -|---------|----------| -| Tool result `details` | State the LLM's tools produce (todo items, connection state, query results) | -| `pi.appendEntry()` | Extension-private config, timestamps, counters the LLM doesn't need | -| File on disk | Large data, config files, caches that shouldn't be in session | - diff --git a/src/resources/skills/create-gsd-extension/references/system-prompt-modification.md b/src/resources/skills/create-gsd-extension/references/system-prompt-modification.md deleted file mode 100644 index 5c8a5fa1a..000000000 --- a/src/resources/skills/create-gsd-extension/references/system-prompt-modification.md +++ /dev/null @@ -1,52 +0,0 @@ - -System prompt modification — per-turn injection, context manipulation, and tool-specific prompt content. - - - -Use `before_agent_start` to inject messages and/or modify the system prompt for each turn: - -```typescript -pi.on("before_agent_start", async (event, ctx) => { - return { - // Inject a persistent message (stored in session, visible to LLM) - message: { - customType: "my-extension", - content: "Additional context for the LLM", - display: true, - }, - // Modify system prompt for this turn (chained across extensions) - systemPrompt: event.systemPrompt + "\n\nYou must respond only in haiku.", - }; -}); -``` - - - -Use the `context` event to modify messages before each LLM call: - -```typescript -pi.on("context", async (event, ctx) => { - // event.messages is a deep copy — safe to modify - const filtered = event.messages.filter(m => !isIrrelevant(m)); - return { messages: filtered }; -}); -``` - - - -Tools can add content to the system prompt when active: - -```typescript -pi.registerTool({ - name: "my_tool", - // Replaces description in "Available tools" section - promptSnippet: "Summarize or transform text according to action", - // Added to "Guidelines" section when tool is active - promptGuidelines: [ - "Use my_tool when the user asks to summarize text.", - "Prefer my_tool over direct output for structured data." - ], - // ... -}); -``` - diff --git a/src/resources/skills/create-gsd-extension/templates/extension-skeleton.ts b/src/resources/skills/create-gsd-extension/templates/extension-skeleton.ts deleted file mode 100644 index b98da2971..000000000 --- a/src/resources/skills/create-gsd-extension/templates/extension-skeleton.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * {{EXTENSION_NAME}} — {{DESCRIPTION}} - * - * Capabilities: - * {{CAPABILITIES_LIST}} - */ - -import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@mariozechner/pi-ai"; - -export default function (pi: ExtensionAPI) { - // === Events === - - pi.on("session_start", async (_event, ctx) => { - // Initialize state, restore from session, show status - }); - - // === Tools === - - pi.registerTool({ - name: "{{tool_name}}", - label: "{{Tool Label}}", - description: "{{Tool description for LLM}}", - parameters: Type.Object({ - action: StringEnum(["list", "add"] as const), - text: Type.Optional(Type.String({ description: "Item text" })), - }), - async execute(toolCallId, params, signal, onUpdate, ctx) { - if (signal?.aborted) { - return { content: [{ type: "text", text: "Cancelled" }] }; - } - - // Do work here - - return { - content: [{ type: "text", text: "Result for LLM" }], - details: {}, - }; - }, - }); - - // === Commands === - - pi.registerCommand("{{command_name}}", { - description: "{{Command description}}", - handler: async (args, ctx) => { - ctx.ui.notify(`Running ${args}`, "info"); - }, - }); -} diff --git a/src/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts b/src/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts deleted file mode 100644 index 1ba1483fe..000000000 --- a/src/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * {{EXTENSION_NAME}} — Stateful tool with persistence - * - * State is stored in tool result details for proper branching support. - */ - -import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; -import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@mariozechner/pi-ai"; -import { Text, truncateToWidth, matchesKey, Key } from "@mariozechner/pi-tui"; - -interface {{ItemType}} { - id: number; - // Add fields -} - -interface {{ToolDetails}} { - action: string; - items: {{ItemType}}[]; - nextId: number; - error?: string; -} - -export default function (pi: ExtensionAPI) { - let items: {{ItemType}}[] = []; - let nextId = 1; - - // Reconstruct state from session - const reconstructState = (ctx: ExtensionContext) => { - items = []; - nextId = 1; - for (const entry of ctx.sessionManager.getBranch()) { - if (entry.type === "message" && entry.message.role === "toolResult") { - if (entry.message.toolName === "{{tool_name}}") { - const details = entry.message.details as {{ToolDetails}} | undefined; - if (details) { - items = details.items; - nextId = details.nextId; - } - } - } - } - }; - - // Reconstruct on ALL session change events - pi.on("session_start", async (_event, ctx) => reconstructState(ctx)); - pi.on("session_switch", async (_event, ctx) => reconstructState(ctx)); - pi.on("session_fork", async (_event, ctx) => reconstructState(ctx)); - pi.on("session_tree", async (_event, ctx) => reconstructState(ctx)); - - // Register the tool - pi.registerTool({ - name: "{{tool_name}}", - label: "{{Tool Label}}", - description: "{{Description for LLM}}", - parameters: Type.Object({ - action: StringEnum(["list", "add", "remove"] as const), - text: Type.Optional(Type.String({ description: "Item text" })), - id: Type.Optional(Type.Number({ description: "Item ID" })), - }), - - async execute(toolCallId, params, signal, onUpdate, ctx) { - if (signal?.aborted) { - return { content: [{ type: "text", text: "Cancelled" }] }; - } - - switch (params.action) { - case "list": - return { - content: [{ type: "text", text: items.length ? JSON.stringify(items) : "No items" }], - details: { action: "list", items: [...items], nextId } as {{ToolDetails}}, - }; - - case "add": { - if (!params.text) throw new Error("text required for add"); - const item: {{ItemType}} = { id: nextId++ /* , ... */ }; - items.push(item); - return { - content: [{ type: "text", text: `Added #${item.id}` }], - details: { action: "add", items: [...items], nextId } as {{ToolDetails}}, - }; - } - - case "remove": { - if (params.id === undefined) throw new Error("id required for remove"); - const idx = items.findIndex(i => i.id === params.id); - if (idx === -1) throw new Error(`Item #${params.id} not found`); - items.splice(idx, 1); - return { - content: [{ type: "text", text: `Removed #${params.id}` }], - details: { action: "remove", items: [...items], nextId } as {{ToolDetails}}, - }; - } - - default: - throw new Error(`Unknown action: ${params.action}`); - } - }, - - // Custom rendering - renderCall(args, theme) { - let text = theme.fg("toolTitle", theme.bold("{{tool_name}} ")); - text += theme.fg("muted", args.action); - return new Text(text, 0, 0); - }, - - renderResult(result, { expanded }, theme) { - const details = result.details as {{ToolDetails}} | undefined; - if (!details) return new Text("", 0, 0); - if (details.error) return new Text(theme.fg("error", details.error), 0, 0); - return new Text(theme.fg("success", `✓ ${details.action} (${details.items.length} items)`), 0, 0); - }, - }); - - // User command to view state - pi.registerCommand("{{command_name}}", { - description: "View {{items}}", - handler: async (_args, ctx) => { - if (!ctx.hasUI) { - ctx.ui.notify("Requires interactive mode", "error"); - return; - } - await ctx.ui.custom((_tui, theme, _kb, done) => ({ - render(width: number): string[] { - const lines = [ - "", - truncateToWidth(theme.fg("accent", ` {{Items}} (${items.length}) `), width), - "", - ]; - for (const item of items) { - lines.push(truncateToWidth(` #${item.id}`, width)); - } - lines.push("", truncateToWidth(theme.fg("dim", " Press Escape to close"), width), ""); - return lines; - }, - handleInput(data: string) { - if (matchesKey(data, Key.escape)) done(); - }, - invalidate() {}, - })); - }, - }); -} diff --git a/src/resources/skills/create-gsd-extension/workflows/add-capability.md b/src/resources/skills/create-gsd-extension/workflows/add-capability.md deleted file mode 100644 index eac2e4ea1..000000000 --- a/src/resources/skills/create-gsd-extension/workflows/add-capability.md +++ /dev/null @@ -1,57 +0,0 @@ - -Read the reference file for the specific capability being added: -- Tools → references/custom-tools.md -- Commands → references/custom-commands.md -- Events → references/events-reference.md -- UI → references/custom-ui.md -- Rendering → references/custom-rendering.md -- State → references/state-management.md -- System prompt → references/system-prompt-modification.md - - - - -## Step 1: Identify the Extension - -Locate the existing extension file. Check: -- `~/.pi/agent/extensions/` (global community extensions) -- `.gsd/extensions/` (project-local) - -Read the current extension code to understand its structure. - -## Step 2: Add the Capability - -Add the new registration/hook inside the existing `export default function (pi: ExtensionAPI)` body. Follow the patterns in the relevant reference file. - -If the extension needs new imports, add them at the top of the file. - -## Step 3: Handle Structural Changes - -**Single file → Directory**: If the extension is outgrowing a single file: -1. Create `~/.pi/agent/extensions/my-extension/` -2. Move the file to `index.ts` -3. Extract helpers to separate files - -**Adding npm dependencies**: If new packages are needed: -1. Create `package.json` in the extension directory -2. Add dependencies -3. Run `npm install` -4. Add `"pi": { "extensions": ["./index.ts"] }` to package.json - -## Step 4: Test - -```bash -/reload -``` - -Verify the new capability works alongside existing ones. - - - - -Capability addition is complete when: -- [ ] New capability added without breaking existing functionality -- [ ] All new imports resolve -- [ ] `/reload` succeeds -- [ ] New tool/command/hook tested with real invocation - diff --git a/src/resources/skills/create-gsd-extension/workflows/create-extension.md b/src/resources/skills/create-gsd-extension/workflows/create-extension.md deleted file mode 100644 index a91a39ae6..000000000 --- a/src/resources/skills/create-gsd-extension/workflows/create-extension.md +++ /dev/null @@ -1,156 +0,0 @@ - -**Read these reference files before proceeding:** -1. references/extension-lifecycle.md -2. references/custom-tools.md (if building tools) -3. references/custom-commands.md (if building commands) -4. references/events-reference.md (if building event hooks) -5. references/key-rules-gotchas.md (always) - - - - -## Step 1: Determine Scope and Placement - -Ask the user: -- **Global** (`~/.pi/agent/extensions/`) — Available in all GSD sessions -- **Project-local** (`.gsd/extensions/`) — Available only in this project - -## Step 2: Determine Extension Capabilities - -Identify what the extension needs from the user's description: - -| Capability | API | When | -|------------|-----|------| -| Custom tool (LLM-callable) | `pi.registerTool()` | LLM needs to perform new actions | -| Slash command | `pi.registerCommand()` | User needs direct actions | -| Event interception | `pi.on("event", ...)` | Block/modify tool calls, inject context, react to lifecycle | -| Custom UI | `ctx.ui.custom()` | Complex interactive displays | -| System prompt modification | `before_agent_start` event | Add per-turn instructions | -| Context filtering | `context` event | Modify messages sent to LLM | -| State persistence | `details` in tool results or `pi.appendEntry()` | Stateful behavior | -| Custom rendering | `renderCall` / `renderResult` | Control how tools appear in TUI | -| Provider management | `pi.registerProvider()` | Custom model endpoints | -| Keyboard shortcut | `pi.registerShortcut()` | Hotkey triggers | - -## Step 3: Choose Extension Structure - -**Single file** — for small extensions (1-2 tools/commands, simple hooks): -``` -~/.pi/agent/extensions/my-extension.ts -``` - -**Directory with index.ts** — for multi-file extensions: -``` -~/.pi/agent/extensions/my-extension/ -├── index.ts -├── tools.ts -└── utils.ts -``` - -**Package with dependencies** — when npm packages are needed: -``` -~/.pi/agent/extensions/my-extension/ -├── package.json -├── src/index.ts -└── node_modules/ -``` - -For packages, `package.json` needs: -```json -{ - "name": "my-extension", - "dependencies": { ... }, - "pi": { "extensions": ["./src/index.ts"] } -} -``` - -## Step 4: Write the Extension - -Start with the skeleton: - -```typescript -import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; - -export default function (pi: ExtensionAPI) { - // Register events, tools, commands here -} -``` - -Then add capabilities based on Step 2. Reference the appropriate reference files for each capability. - -**Tool registration pattern:** -```typescript -import { Type } from "@sinclair/typebox"; -import { StringEnum } from "@mariozechner/pi-ai"; - -pi.registerTool({ - name: "my_tool", - label: "My Tool", - description: "What this tool does (shown to LLM)", - parameters: Type.Object({ - action: StringEnum(["list", "add"] as const), - text: Type.Optional(Type.String({ description: "Item text" })), - }), - async execute(toolCallId, params, signal, onUpdate, ctx) { - if (signal?.aborted) return { content: [{ type: "text", text: "Cancelled" }] }; - return { - content: [{ type: "text", text: "Result for LLM" }], - details: { data: "for rendering and state" }, - }; - }, -}); -``` - -**Command registration pattern:** -```typescript -pi.registerCommand("mycommand", { - description: "What this command does", - handler: async (args, ctx) => { - ctx.ui.notify(`Running with args: ${args}`, "info"); - }, -}); -``` - -**Event hook pattern:** -```typescript -pi.on("tool_call", async (event, ctx) => { - if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) { - return { block: true, reason: "Blocked dangerous command" }; - } -}); -``` - -## Step 5: Test the Extension - -```bash -# Quick test without installing -gsd -e ./path/to/my-extension.ts - -# Or place in extensions dir and reload -/reload -``` - -Verify: -- Extension loads without errors (check GSD startup output) -- Tools appear when LLM is asked to use them -- Commands respond to `/mycommand` -- Event hooks trigger at expected points - -## Step 6: Iterate - -Fix issues, add features, refine. Use `/reload` for hot-reload during development. - - - - -Extension creation is complete when: -- [ ] Extension file(s) written to correct location -- [ ] All imports resolve (TypeBox, pi-ai, pi-coding-agent, pi-tui as needed) -- [ ] Tools use `StringEnum` for string enums (not `Type.Union`/`Type.Literal`) -- [ ] Tool output is truncated if variable-length -- [ ] State stored in `details` if extension is stateful -- [ ] `ctx.hasUI` checked before dialog methods -- [ ] Extension loads on `/reload` without errors -- [ ] Tools callable by LLM, commands by user -- [ ] Tested with at least one real invocation - diff --git a/src/resources/skills/create-gsd-extension/workflows/debug-extension.md b/src/resources/skills/create-gsd-extension/workflows/debug-extension.md deleted file mode 100644 index 5a8ac2295..000000000 --- a/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +++ /dev/null @@ -1,76 +0,0 @@ - -1. references/key-rules-gotchas.md -2. references/extension-lifecycle.md - - - - -## Step 1: Identify the Symptom - -| Symptom | Likely Cause | -|---------|--------------| -| Extension not loading | File not in discovery path, syntax error, missing export default | -| Tool not appearing for LLM | Tool not registered, `pi.setActiveTools()` excluding it, tool name conflict | -| Command not responding | Command not registered, name collision with built-in | -| Event not firing | Wrong event name, handler returning too early, handler error (logged but swallowed) | -| UI not rendering | `ctx.hasUI` is false (print mode), render lines exceed width, component not returning lines | -| State lost on restart | State not stored in `details` or `appendEntry`, not reconstructing on `session_start` | -| Google API errors | Using `Type.Union`/`Type.Literal` instead of `StringEnum` | -| Context overflow | Tool output not truncated | -| Deadlock/hang | Session control methods called from event handler (must be in command handler only) | -| Render garbage | Theme imported directly instead of from callback, missing `truncateToWidth()` | - -## Step 2: Check Extension Loading - -```bash -# Test in isolation -gsd -e ./path/to/extension.ts - -# Check GSD startup output for errors -# Extension errors are logged but don't crash GSD -``` - -## Step 3: Verify File Location - -Community extensions must be in auto-discovery paths: -- `~/.pi/agent/extensions/*.ts` -- `~/.pi/agent/extensions/*/index.ts` -- `.gsd/extensions/*.ts` -- `.gsd/extensions/*/index.ts` - -Note: `~/.gsd/agent/extensions/` is reserved for bundled extensions synced from the gsd-pi package. - -The file must `export default function(pi: ExtensionAPI) { ... }`. - -## Step 4: Check for Common Mistakes - -Read `../references/key-rules-gotchas.md` and verify each rule against the extension code. - -## Step 5: Add Debugging - -```typescript -// Temporary: log to stderr (visible in GSD output) -console.error("[my-ext] Loading..."); - -pi.on("session_start", async (_event, ctx) => { - console.error("[my-ext] Session started"); - ctx.ui.notify("Extension loaded", "info"); -}); -``` - -## Step 6: Fix and Reload - -Apply the fix and test: -``` -/reload -``` - - - - -Debugging is complete when: -- [ ] Root cause identified -- [ ] Fix applied -- [ ] Extension loads and functions correctly after `/reload` -- [ ] No regression in existing functionality - diff --git a/src/tests/app-smoke.test.ts b/src/tests/app-smoke.test.ts index 0c11ea3f5..ff53f7309 100644 --- a/src/tests/app-smoke.test.ts +++ b/src/tests/app-smoke.test.ts @@ -345,7 +345,7 @@ test("loadStoredEnvKeys does not overwrite existing env vars", async (t) => { // ═══════════════════════════════════════════════════════════════════════════ test("deriveState returns pre-planning phase for empty .gsd/ directory", async (t) => { - const { deriveState } = await import("../resources/extensions/gsd/state.ts"); + const { deriveState } = await import("../resources/extensions/sf/state.ts"); const tmp = mkdtempSync(join(tmpdir(), "gsd-state-smoke-")); // Create minimal .gsd/ structure with no milestones @@ -367,7 +367,7 @@ test("deriveState returns pre-planning phase for empty .gsd/ directory", async ( }); test("deriveState returns pre-planning phase when no .gsd/ directory exists", async (t) => { - const { deriveState } = await import("../resources/extensions/gsd/state.ts"); + const { deriveState } = await import("../resources/extensions/sf/state.ts"); // Use a temp dir with no .gsd/ subdirectory at all const tmp = mkdtempSync(join(tmpdir(), "gsd-state-nogsd-")); @@ -381,7 +381,7 @@ test("deriveState returns pre-planning phase when no .gsd/ directory exists", as }); test("deriveState shape is structurally complete", async (t) => { - const { deriveState } = await import("../resources/extensions/gsd/state.ts"); + const { deriveState } = await import("../resources/extensions/sf/state.ts"); const tmp = mkdtempSync(join(tmpdir(), "gsd-state-shape-")); mkdirSync(join(tmp, ".gsd"), { recursive: true }); @@ -412,7 +412,7 @@ test("deriveState shape is structurally complete", async (t) => { // ═══════════════════════════════════════════════════════════════════════════ test("runGSDDoctor completes without throwing on empty .gsd/ directory", async (t) => { - const { runGSDDoctor } = await import("../resources/extensions/gsd/doctor.ts"); + const { runGSDDoctor } = await import("../resources/extensions/sf/doctor.ts"); const tmp = mkdtempSync(join(tmpdir(), "gsd-doctor-smoke-")); mkdirSync(join(tmp, ".gsd"), { recursive: true }); @@ -433,7 +433,7 @@ test("runGSDDoctor completes without throwing on empty .gsd/ directory", async ( }); test("runGSDDoctor issue objects have required fields", async (t) => { - const { runGSDDoctor } = await import("../resources/extensions/gsd/doctor.ts"); + const { runGSDDoctor } = await import("../resources/extensions/sf/doctor.ts"); const tmp = mkdtempSync(join(tmpdir(), "gsd-doctor-fields-")); mkdirSync(join(tmp, ".gsd"), { recursive: true }); @@ -461,7 +461,7 @@ test("runGSDDoctor issue objects have required fields", async (t) => { }); test("runGSDDoctor with fix:false never modifies the filesystem", async (t) => { - const { runGSDDoctor } = await import("../resources/extensions/gsd/doctor.ts"); + const { runGSDDoctor } = await import("../resources/extensions/sf/doctor.ts"); const tmp = mkdtempSync(join(tmpdir(), "gsd-doctor-readonly-")); const gsdDir = join(tmp, ".gsd"); mkdirSync(gsdDir, { recursive: true }); diff --git a/src/tests/auto-budget.test.ts b/src/tests/auto-budget.test.ts index 76ea01c4c..c5ff7d2da 100644 --- a/src/tests/auto-budget.test.ts +++ b/src/tests/auto-budget.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction } from "../resources/extensions/gsd/auto-budget.js"; +import { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction } from "../resources/extensions/sf/auto-budget.js"; describe("auto-budget", () => { describe("getBudgetAlertLevel", () => { diff --git a/src/tests/auto-tool-tracking.test.ts b/src/tests/auto-tool-tracking.test.ts index fe1e2cbe1..17aa61979 100644 --- a/src/tests/auto-tool-tracking.test.ts +++ b/src/tests/auto-tool-tracking.test.ts @@ -6,7 +6,7 @@ import { getOldestInFlightToolAgeMs, getInFlightToolCount, clearInFlightTools, -} from "../resources/extensions/gsd/auto-tool-tracking.js"; +} from "../resources/extensions/sf/auto-tool-tracking.js"; describe("auto-tool-tracking", () => { beforeEach(() => { diff --git a/src/tests/cross-platform-filesystem-safety.test.ts b/src/tests/cross-platform-filesystem-safety.test.ts index 0125c13f5..68c378224 100644 --- a/src/tests/cross-platform-filesystem-safety.test.ts +++ b/src/tests/cross-platform-filesystem-safety.test.ts @@ -3,7 +3,7 @@ * * Scans ALL production .ts files and flags patterns that break on * Windows, Linux, or macOS. Modelled after the git-locale static - * check in src/resources/extensions/gsd/tests/git-locale.test.ts. + * check in src/resources/extensions/sf/tests/git-locale.test.ts. * * Patterns 1, 3, 4 → hard fail (clear bugs). * Patterns 2, 5, 6 → warn only (logged, no assertion failure). diff --git a/src/tests/integration/e2e-headless.test.ts b/src/tests/integration/e2e-headless.test.ts index dfb9cd002..c78cc61b5 100644 --- a/src/tests/integration/e2e-headless.test.ts +++ b/src/tests/integration/e2e-headless.test.ts @@ -11,7 +11,7 @@ * Prerequisite: npm run build must be run first. * * Run with: - * node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs \ + * node --import ./src/resources/extensions/sf/tests/resolve-ts.mjs \ * --experimental-strip-types --test \ * src/tests/integration/e2e-headless.test.ts */ diff --git a/src/tests/integration/e2e-smoke.test.ts b/src/tests/integration/e2e-smoke.test.ts index 21025f5ab..ac946570b 100644 --- a/src/tests/integration/e2e-smoke.test.ts +++ b/src/tests/integration/e2e-smoke.test.ts @@ -9,7 +9,7 @@ * Prerequisite: npm run build must be run first. * * Run with: - * node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs \ + * node --import ./src/resources/extensions/sf/tests/resolve-ts.mjs \ * --experimental-strip-types --test \ * src/tests/integration/e2e-smoke.test.ts */ diff --git a/src/tests/integration/pack-install.test.ts b/src/tests/integration/pack-install.test.ts index e69b03ee0..dc76490c7 100644 --- a/src/tests/integration/pack-install.test.ts +++ b/src/tests/integration/pack-install.test.ts @@ -134,7 +134,7 @@ test("npm pack produces tarball with required files", async (t) => { assert.ok(files.some(f => f.includes("dist/wizard.js")), "tarball contains dist/wizard.js"); assert.ok(files.some(f => f.includes("dist/resource-loader.js")), "tarball contains dist/resource-loader.js"); assert.ok(files.some(f => f.includes("pkg/package.json")), "tarball contains pkg/package.json"); - assert.ok(files.some(f => f.includes("src/resources/extensions/gsd/index.ts")), "tarball contains bundled gsd extension"); + assert.ok(files.some(f => f.includes("src/resources/extensions/sf/index.ts")), "tarball contains bundled gsd extension"); assert.ok(files.some(f => f.includes("scripts/postinstall.js")), "tarball contains postinstall script"); // pkg/package.json must have piConfig diff --git a/src/tests/integration/web-cli-entry.test.ts b/src/tests/integration/web-cli-entry.test.ts index 6c69928a0..a8f5acc6c 100644 --- a/src/tests/integration/web-cli-entry.test.ts +++ b/src/tests/integration/web-cli-entry.test.ts @@ -21,7 +21,7 @@ test("resolveGsdCliEntry prefers the built loader for packaged standalone intera const packageRoot = makeFixture([ "dist/loader.js", "src/loader.ts", - "src/resources/extensions/gsd/tests/resolve-ts.mjs", + "src/resources/extensions/sf/tests/resolve-ts.mjs", ]); t.after(() => { rmSync(packageRoot, { recursive: true, force: true }); }); @@ -45,7 +45,7 @@ test("resolveGsdCliEntry prefers the source loader for source-dev interactive se const packageRoot = makeFixture([ "dist/loader.js", "src/loader.ts", - "src/resources/extensions/gsd/tests/resolve-ts.mjs", + "src/resources/extensions/sf/tests/resolve-ts.mjs", ]); t.after(() => { rmSync(packageRoot, { recursive: true, force: true }); }); diff --git a/src/tests/integration/web-command-parity-contract.test.ts b/src/tests/integration/web-command-parity-contract.test.ts index 96b6e2640..da3073c81 100644 --- a/src/tests/integration/web-command-parity-contract.test.ts +++ b/src/tests/integration/web-command-parity-contract.test.ts @@ -15,7 +15,7 @@ const { setCommandSurfacePending, surfaceOutcomeToOpenRequest, } = await import("../../../web/lib/command-surface-contract.ts") -const gsdExtension = await import("../../resources/extensions/gsd/index.ts") +const gsdExtension = await import("../../resources/extensions/sf/index.ts") const EXPECTED_BUILTIN_OUTCOMES = new Map([ ["settings", "surface"], diff --git a/src/tests/integration/web-state-surfaces-contract.test.ts b/src/tests/integration/web-state-surfaces-contract.test.ts index 120da7d25..ec408a0e8 100644 --- a/src/tests/integration/web-state-surfaces-contract.test.ts +++ b/src/tests/integration/web-state-surfaces-contract.test.ts @@ -6,7 +6,7 @@ import { join, resolve } from "node:path"; // ─── Imports ────────────────────────────────────────────────────────── const workspaceIndex = await import( - "../../resources/extensions/gsd/workspace-index.ts" + "../../resources/extensions/sf/workspace-index.ts" ); const filesRoute = await import("../../../web/app/api/files/route.ts"); diff --git a/src/tests/integration/web-subprocess-module-resolution.test.ts b/src/tests/integration/web-subprocess-module-resolution.test.ts index 9010eb698..58c19f343 100644 --- a/src/tests/integration/web-subprocess-module-resolution.test.ts +++ b/src/tests/integration/web-subprocess-module-resolution.test.ts @@ -44,22 +44,22 @@ test("resolveSubprocessModule returns source .ts path when NOT under node_module const packageRoot = "/home/user/projects/gsd" const result = resolveSubprocessModule( packageRoot, - "resources/extensions/gsd/workspace-index.ts", + "resources/extensions/sf/workspace-index.ts", // existsSync not needed — should return src path without checking dist ) assert.deepEqual(result, { - modulePath: join(packageRoot, "src", "resources/extensions/gsd/workspace-index.ts"), + modulePath: join(packageRoot, "src", "resources/extensions/sf/workspace-index.ts"), useCompiledJs: false, }) }) test("resolveSubprocessModule returns compiled .js path when under node_modules and dist file exists", () => { const packageRoot = "/usr/lib/node_modules/gsd-pi" - const distPath = join(packageRoot, "dist", "resources/extensions/gsd/workspace-index.js") + const distPath = join(packageRoot, "dist", "resources/extensions/sf/workspace-index.js") const result = resolveSubprocessModule( packageRoot, - "resources/extensions/gsd/workspace-index.ts", + "resources/extensions/sf/workspace-index.ts", (p: string) => p === distPath, ) @@ -73,22 +73,22 @@ test("resolveSubprocessModule falls back to source .ts when under node_modules b const packageRoot = "/usr/lib/node_modules/gsd-pi" const result = resolveSubprocessModule( packageRoot, - "resources/extensions/gsd/workspace-index.ts", + "resources/extensions/sf/workspace-index.ts", () => false, // dist file does not exist ) assert.deepEqual(result, { - modulePath: join(packageRoot, "src", "resources/extensions/gsd/workspace-index.ts"), + modulePath: join(packageRoot, "src", "resources/extensions/sf/workspace-index.ts"), useCompiledJs: false, }) }) test("resolveSubprocessModule handles Windows paths under node_modules", () => { const packageRoot = "C:\\Users\\dev\\AppData\\node_modules\\gsd-pi" - const distPath = join(packageRoot, "dist", "resources/extensions/gsd/auto.js") + const distPath = join(packageRoot, "dist", "resources/extensions/sf/auto.js") const result = resolveSubprocessModule( packageRoot, - "resources/extensions/gsd/auto.ts", + "resources/extensions/sf/auto.ts", (p: string) => p === distPath, ) @@ -103,13 +103,13 @@ test("resolveSubprocessModule strips .ts extension when building dist .js path", let checkedPath = "" resolveSubprocessModule( packageRoot, - "resources/extensions/gsd/doctor.ts", + "resources/extensions/sf/doctor.ts", (p: string) => { checkedPath = p; return true }, ) assert.equal( checkedPath, - join(packageRoot, "dist", "resources/extensions/gsd/doctor.js"), + join(packageRoot, "dist", "resources/extensions/sf/doctor.js"), "should check for .js file in dist/, not .ts", ) }) diff --git a/src/tests/marketplace-discovery.test.ts b/src/tests/marketplace-discovery.test.ts index 80e61f443..d2881ce62 100644 --- a/src/tests/marketplace-discovery.test.ts +++ b/src/tests/marketplace-discovery.test.ts @@ -19,8 +19,8 @@ import { inspectPlugin, discoverMarketplace, resolvePluginRoot -} from '../resources/extensions/gsd/marketplace-discovery.js'; -import { getMarketplaceFixtures } from '../resources/extensions/gsd/tests/marketplace-test-fixtures.js'; +} from '../resources/extensions/sf/marketplace-discovery.js'; +import { getMarketplaceFixtures } from '../resources/extensions/sf/tests/marketplace-test-fixtures.js'; const fixtureSetup = getMarketplaceFixtures(import.meta.dirname); const fixtures = fixtureSetup.fixtures; diff --git a/src/tests/offline-mode.test.ts b/src/tests/offline-mode.test.ts index 07c19b642..cdaef404f 100644 --- a/src/tests/offline-mode.test.ts +++ b/src/tests/offline-mode.test.ts @@ -78,21 +78,21 @@ test("isAllLocalChain returns false for empty list", () => { test("INFRA_ERROR_CODES includes ECONNREFUSED", async () => { const { INFRA_ERROR_CODES } = await import( - "../../src/resources/extensions/gsd/auto/infra-errors.ts" + "../../src/resources/extensions/sf/auto/infra-errors.ts" ); assert.strictEqual(INFRA_ERROR_CODES.has("ECONNREFUSED"), true); }); test("INFRA_ERROR_CODES includes ENOTFOUND", async () => { const { INFRA_ERROR_CODES } = await import( - "../../src/resources/extensions/gsd/auto/infra-errors.ts" + "../../src/resources/extensions/sf/auto/infra-errors.ts" ); assert.strictEqual(INFRA_ERROR_CODES.has("ENOTFOUND"), true); }); test("INFRA_ERROR_CODES includes ENETUNREACH", async () => { const { INFRA_ERROR_CODES } = await import( - "../../src/resources/extensions/gsd/auto/infra-errors.ts" + "../../src/resources/extensions/sf/auto/infra-errors.ts" ); assert.strictEqual(INFRA_ERROR_CODES.has("ENETUNREACH"), true); }); @@ -101,7 +101,7 @@ test("INFRA_ERROR_CODES includes ENETUNREACH", async () => { test("isInfrastructureError returns code for ECONNREFUSED when offline", async () => { const { isInfrastructureError } = await import( - "../../src/resources/extensions/gsd/auto/infra-errors.ts" + "../../src/resources/extensions/sf/auto/infra-errors.ts" ); const savedOffline = process.env.PI_OFFLINE; process.env.PI_OFFLINE = "1"; diff --git a/src/tests/resolve-ts-loader.test.ts b/src/tests/resolve-ts-loader.test.ts index 8ab680a59..172792011 100644 --- a/src/tests/resolve-ts-loader.test.ts +++ b/src/tests/resolve-ts-loader.test.ts @@ -1,7 +1,7 @@ import test from "node:test" import assert from "node:assert/strict" -import { load as loadWithTestLoader, resolve as resolveWithTestLoader } from "../resources/extensions/gsd/tests/dist-redirect.mjs" +import { load as loadWithTestLoader, resolve as resolveWithTestLoader } from "../resources/extensions/sf/tests/dist-redirect.mjs" const nextResolve = async (specifier: string) => ({ url: specifier }) diff --git a/src/tests/rtk-execution-seams.test.ts b/src/tests/rtk-execution-seams.test.ts index 0d1a781ec..d41d1a134 100644 --- a/src/tests/rtk-execution-seams.test.ts +++ b/src/tests/rtk-execution-seams.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { rewriteCommandWithRtk as rewriteSharedCommandWithRtk } from "../resources/extensions/shared/rtk.ts"; -import { runVerificationGate } from "../resources/extensions/gsd/verification-gate.ts"; +import { runVerificationGate } from "../resources/extensions/sf/verification-gate.ts"; import { AsyncJobManager } from "../resources/extensions/async-jobs/job-manager.ts"; import { createAsyncBashTool } from "../resources/extensions/async-jobs/async-bash-tool.ts"; import { cleanupAll, startProcess } from "../resources/extensions/bg-shell/process-manager.ts"; diff --git a/src/tests/token-counter.test.ts b/src/tests/token-counter.test.ts index 5cfd98696..3e764c26f 100644 --- a/src/tests/token-counter.test.ts +++ b/src/tests/token-counter.test.ts @@ -5,7 +5,7 @@ import { countTokensSync, initTokenCounter, isAccurateCountingAvailable, -} from "../resources/extensions/gsd/token-counter.ts"; +} from "../resources/extensions/sf/token-counter.ts"; describe("token-counter", () => { it("countTokensSync returns heuristic estimate before init", () => { diff --git a/src/tests/web-subprocess-runner.test.ts b/src/tests/web-subprocess-runner.test.ts index ab3004619..9b463719a 100644 --- a/src/tests/web-subprocess-runner.test.ts +++ b/src/tests/web-subprocess-runner.test.ts @@ -15,7 +15,7 @@ test("resolveModulePaths returns tsLoaderPath and validates it exists", () => { }) assert.equal( result.tsLoaderPath, - "/fake/package/src/resources/extensions/gsd/tests/resolve-ts.mjs", + "/fake/package/src/resources/extensions/sf/tests/resolve-ts.mjs", ) }) @@ -39,7 +39,7 @@ test("resolveModulePaths throws when TS loader is missing", () => { test("resolveModulePaths throws when any module path is missing", () => { const packageRoot = "/fake/package" const existingSets = new Set([ - "/fake/package/src/resources/extensions/gsd/tests/resolve-ts.mjs", + "/fake/package/src/resources/extensions/sf/tests/resolve-ts.mjs", ]) assert.throws( () => diff --git a/src/web/auto-dashboard-service.ts b/src/web/auto-dashboard-service.ts index 972c7474f..4156d2eaa 100644 --- a/src/web/auto-dashboard-service.ts +++ b/src/web/auto-dashboard-service.ts @@ -115,7 +115,7 @@ export async function collectAuthoritativeAutoDashboardData( const testModulePath = env[TEST_AUTO_DASHBOARD_MODULE_ENV]; const moduleResolution = testModulePath ? { modulePath: testModulePath, useCompiledJs: false } - : resolveSubprocessModule(packageRoot, "resources/extensions/gsd/auto.ts", checkExists); + : resolveSubprocessModule(packageRoot, "resources/extensions/sf/auto.ts", checkExists); const autoModulePath = moduleResolution.modulePath; if (!moduleResolution.useCompiledJs && (!checkExists(resolveTsLoader) || !checkExists(autoModulePath))) { diff --git a/src/web/bridge-service.ts b/src/web/bridge-service.ts index b5f87cdce..fc0fa691b 100644 --- a/src/web/bridge-service.ts +++ b/src/web/bridge-service.ts @@ -986,7 +986,7 @@ async function loadWorkspaceIndexViaChildProcess(basePath: string, packageRoot: const resolveTsLoader = join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs"); const moduleResolution = resolveSubprocessModule( packageRoot, - "resources/extensions/gsd/workspace-index.ts", + "resources/extensions/sf/workspace-index.ts", checkExists, ); const workspaceModulePath = moduleResolution.modulePath; diff --git a/src/web/captures-service.ts b/src/web/captures-service.ts index 2a8b4c9b8..eec38b752 100644 --- a/src/web/captures-service.ts +++ b/src/web/captures-service.ts @@ -24,7 +24,7 @@ export async function collectCapturesData(projectCwdOverride?: string): Promise< const { packageRoot, projectCwd } = config const resolveTsLoader = resolveTsLoaderPath(packageRoot) - const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/captures.ts") + const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/sf/captures.ts") const capturesModulePath = moduleResolution.modulePath if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(capturesModulePath))) { @@ -95,7 +95,7 @@ export async function resolveCaptureAction(request: CaptureResolveRequest, proje const { packageRoot, projectCwd } = config const resolveTsLoader = resolveTsLoaderPath(packageRoot) - const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/captures.ts") + const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/sf/captures.ts") const capturesModulePath = moduleResolution.modulePath if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(capturesModulePath))) { diff --git a/src/web/cleanup-service.ts b/src/web/cleanup-service.ts index 2ef778a4e..9097ad507 100644 --- a/src/web/cleanup-service.ts +++ b/src/web/cleanup-service.ts @@ -24,7 +24,7 @@ export async function collectCleanupData(projectCwdOverride?: string): Promise