pi-short-links 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ # Template Sync Checklist
2
+
3
+ `create-pi-extension` CLI を publish する前に、テンプレートソース(このリポジトリルート)と CLI にバンドルされるテンプレートが一致していることを確認するためのチェックリスト。
4
+
5
+ ## Before sync
6
+
7
+ - [ ] `docs/examples.md` が現在の extension / skill / prompt / theme の example を正しく説明している
8
+ - [ ] 新しく追加したファイルが `sync-template.ts` の exclude リストで除外されていない
9
+ - [ ] 削除したファイルが `sync-template.ts` の exclude リストから削除されている
10
+ - [ ] 新規 resource type(extension, skill, prompt, theme)を追加した場合、`docs/examples.md` に例を追記した
11
+ - [ ] `scaffold/package-readme.md` に新しい resource type の記述が反映されている(該当する場合)
12
+
13
+ ## Run sync
14
+
15
+ - [ ] `bun run sync:template` を実行して `packages/create-pi-extension/template/` を再生成する
16
+ - [ ] 生成されたファイル数が期待通りであることを確認する(目安: 47 ファイル。`ROADMAP.md` は maintainer-only のため除外)
17
+
18
+ ## Verify synced template
19
+
20
+ - [ ] `packages/create-pi-extension/template/` に必要なファイルがすべて含まれている
21
+ - [ ] `package.json`(workspaces 削除・sync:template script 削除済み)
22
+ - [ ] `README.md`(scaffold/package-readme.md の内容)
23
+ - [ ] `extensions/hello.ts`, `extensions/index.ts`, `extensions/tui-dashboard.ts`, `extensions/skill-bridge/`, `extensions/package-layout/`
24
+ - [ ] `skills/example-skill/SKILL.md`
25
+ - [ ] `prompts/example.md`
26
+ - [ ] `themes/example-theme.json`
27
+ - [ ] `docs/examples.md`
28
+ - [ ] `.github/workflows/ci.yml` と `.github/workflows/publish.yml`
29
+ - [ ] 除外されるべきディレクトリが含まれていないこと
30
+ - [ ] `packages/` がない
31
+ - [ ] `.git/` がない
32
+ - [ ] `node_modules/` がない
33
+ - [ ] `package-lock.json` がない
34
+
35
+ ## Verify template package.json
36
+
37
+ - [ ] `workspaces` フィールドが削除されている
38
+ - [ ] `scripts.ci` が `"npm run typecheck && npm test && npm run pack:check"` になっている
39
+ - [ ] `scripts["sync:template"]` と `scripts["sync:template:check"]` が削除されている
40
+ - [ ] `scripts["pack:check"]` が `"npm pack --dry-run"` になっている
41
+ - [ ] `version` が root `package.json` の repository version と一致している
42
+
43
+ ## CI check
44
+
45
+ - [ ] `npm run typecheck` が通る
46
+ - [ ] `npm test` がすべてのテストにパスする
47
+ - [ ] `tests/greeting.test.mjs`
48
+ - [ ] `tests/format-table.test.mjs`
49
+ - [ ] `tests/smoke.test.mjs`
50
+ - [ ] `tests/create-pi-extension-cli.test.mjs`
51
+ - [ ] `npm run sync:template` を再実行して `tests/sync-template.test.mjs` がパスする
52
+ - [ ] `node --test tests/sync-template.test.mjs` がパスする
53
+
54
+ ## CLI scaffold test(オプションだが推奨)
55
+
56
+ - [ ] 一時ディレクトリで `node packages/create-pi-extension/src/cli.mjs test-refresh-pkg` を実行する
57
+ - [ ] 生成されたプロジェクトで `npm run ci` が通る
58
+ - [ ] `pi -e .` が extension をロードできる(Pi が利用可能な環境で)
59
+
60
+ ## Before publish
61
+
62
+ - [ ] CHANGELOG.md に新しいバージョンのリリースノートを追記した
63
+ - [ ] 既存の issue / PR と競合していないことを確認した
64
+ - [ ] `docs/release.md` の手順に従って publish する準備ができている
65
+
66
+ ## Post-publish
67
+
68
+ - [ ] `npm view create-pi-extension` で新しいバージョンが反映されていることを確認する
69
+ - [ ] 新しいバージョンの CLI で scaffold して動作確認する
@@ -0,0 +1,42 @@
1
+ # Template sync for create-pi-extension
2
+
3
+ The `create-pi-extension` CLI package ships a generated copy of the **template source** (this repository root). Maintainers refresh that bundle before publishing the CLI.
4
+
5
+ ## Command
6
+
7
+ From the repository root:
8
+
9
+ ```bash
10
+ bun run sync:template
11
+ ```
12
+
13
+ This writes to `packages/create-pi-extension/template/`. The directory is gitignored because it is generated output.
14
+
15
+ ## What gets copied
16
+
17
+ Everything under the repository root is copied recursively, except the **sync exclusion list**:
18
+
19
+ - `packages/`
20
+ - `scaffold/`
21
+ - `node_modules/`
22
+ - `.git/`
23
+ - root `package-lock.json`
24
+ - root `bun.lock` / `bun.lockb`
25
+ - `ROADMAP.md` (maintainer-only planning doc for this repository)
26
+
27
+ ## Version sync
28
+
29
+ The root `package.json` **repository version** is copied into `packages/create-pi-extension/package.json` on every sync.
30
+
31
+ ## Template package.json cleanup
32
+
33
+ The synced `template/package.json` has monorepo-only fields removed (`workspaces`, `sync:template` script) so the bundled template stays a standalone Pi package.
34
+
35
+ ## CI check
36
+
37
+ `npm run ci` runs `sync:template:check`, which regenerates the template and runs `tests/sync-template.test.mjs`.
38
+
39
+ ## See also
40
+
41
+ - [`docs/template-sync-checklist.md`](template-sync-checklist.md) — maintainer checklist before publishing the CLI
42
+ - [`packages/create-pi-extension/`](../packages/create-pi-extension/) — scaffold CLI source
@@ -0,0 +1,97 @@
1
+ # TypeScript Guide
2
+
3
+ > **Template bootstrap doc.** Use while learning this template's TypeScript layout and dependency rules. Delete this file or summarize the policies you keep in README Development once setup is done, unless it still adds maintainer value.
4
+
5
+ This is a TypeScript-first Pi package template.
6
+
7
+ ## Layout
8
+
9
+ ```txt
10
+ extensions/*.ts Pi extension entrypoints
11
+ lib/*.ts Shared TypeScript helpers
12
+ skills/*/SKILL.md Agent Skills
13
+ prompts/*.md Prompt templates
14
+ themes/*.json Themes
15
+ tests/*.test.mjs Smoke tests
16
+ tests/*.test.ts Optional TypeScript tests if you add a TS test runner
17
+ ```
18
+
19
+ Pi loads TypeScript extensions directly, so no build step is required for normal use.
20
+
21
+ ## Strict mode
22
+
23
+ `tsconfig.json` keeps `strict: true`. Prefer fixing types over loosening compiler options.
24
+
25
+ ## Extension entrypoints
26
+
27
+ Two entrypoint styles are shown:
28
+
29
+ - `extensions/hello.ts`: single-file extension
30
+ - `extensions/index.ts`: index-style extension that imports shared code from `lib/`
31
+
32
+ For larger packages, keep entrypoints thin and put reusable logic in `lib/`.
33
+
34
+ Both files are listed explicitly under `pi.extensions` in `package.json`. Do not
35
+ "simplify" this to the directory shorthand `["./extensions"]`: Pi resolves a
36
+ manifest directory entry to its `index.ts`, so `hello.ts` (and its
37
+ `/template-hello` command) would silently fail to load. Listing each entry file
38
+ keeps every documented entrypoint active.
39
+
40
+ ## TypeBox schemas
41
+
42
+ Use TypeBox schemas for custom tool parameters.
43
+
44
+ ```ts
45
+ import { Type } from "typebox";
46
+
47
+ const parameters = Type.Object({
48
+ name: Type.String({ description: "Name to greet" }),
49
+ });
50
+ ```
51
+
52
+ ## String enums
53
+
54
+ For string choices, use `StringEnum` from `@earendil-works/pi-ai`.
55
+
56
+ ```ts
57
+ import { StringEnum } from "@earendil-works/pi-ai";
58
+
59
+ const mode = StringEnum(["short", "friendly"] as const, {
60
+ description: "Greeting style",
61
+ });
62
+ ```
63
+
64
+ This emits a JSON Schema `enum`, which is friendlier to model providers than a union of string literals.
65
+
66
+ ## TUI availability (`ctx.hasUI`)
67
+
68
+ Extensions may run in headless or RPC-only sessions where `ctx.ui` is unavailable.
69
+ Guard every `ctx.ui` call with `ctx.hasUI` (or return early from command handlers when `!ctx.hasUI`).
70
+
71
+ ```ts
72
+ pi.on("session_start", async (_event, ctx) => {
73
+ if (ctx.hasUI) {
74
+ ctx.ui.setStatus("my-ext", "ready");
75
+ }
76
+ });
77
+ ```
78
+
79
+ ## Runtime dependencies vs peer dependencies
80
+
81
+ Pi bundles core packages for extension authors. Keep Pi-provided packages as `peerDependencies` and also install them as `devDependencies` for local typechecking.
82
+
83
+ Use `peerDependencies` for:
84
+
85
+ - `@earendil-works/pi-coding-agent`
86
+ - `@earendil-works/pi-agent-core`
87
+ - `@earendil-works/pi-ai`
88
+ - `@earendil-works/pi-tui`
89
+ - `typebox`
90
+
91
+ Use `dependencies` for runtime packages your extension imports that Pi does not provide.
92
+
93
+ Use `devDependencies` for local-only tools such as TypeScript, test runners, and linters.
94
+
95
+ ## Package contents
96
+
97
+ Control npm package contents with `package.json` `files`. Prefer this over `.npmignore` so the published package stays explicit.
@@ -0,0 +1,128 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Box, Text } from "@earendil-works/pi-tui";
3
+
4
+ interface TemplateStatusEntry {
5
+ message: string;
6
+ timestamp: number;
7
+ }
8
+
9
+ export default function (pi: ExtensionAPI) {
10
+ let injectTemplateHint = true;
11
+ let turnCount = 0;
12
+
13
+ pi.registerEntryRenderer<TemplateStatusEntry>("template-status", (entry, { expanded }, theme) => {
14
+ const data = entry.data ?? { message: "Template status", timestamp: Date.now() };
15
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
16
+ box.addChild(new Text(`${theme.fg("accent", "[template]")} ${data.message}`, 0, 0));
17
+ if (expanded) {
18
+ box.addChild(new Text(theme.fg("dim", new Date(data.timestamp).toLocaleString()), 0, 0));
19
+ }
20
+ return box;
21
+ });
22
+
23
+ // Fires once when a Pi session starts (new or resumed).
24
+ pi.on("session_start", async (_event, ctx) => {
25
+ turnCount = 0;
26
+ if (ctx.hasUI) {
27
+ ctx.ui.setStatus("template", "Pi template loaded");
28
+ }
29
+ });
30
+
31
+ // Fires before each agent turn, after the system prompt is assembled.
32
+ // Return { systemPrompt } to append teaching context without blocking the turn.
33
+ pi.on("before_agent_start", async (event) => {
34
+ if (!injectTemplateHint) {
35
+ return undefined;
36
+ }
37
+ injectTemplateHint = false;
38
+
39
+ return {
40
+ systemPrompt:
41
+ event.systemPrompt +
42
+ "\n\n[pi-extension-template] Event handlers in extensions/hello.ts are active. Type ?template for help, or try /template-dashboard and /template-layout.",
43
+ };
44
+ });
45
+
46
+ // Fires when the user submits input, before the agent processes it.
47
+ // Return { action: "handled" } to short-circuit the agent for simple commands.
48
+ pi.on("input", async (event, ctx) => {
49
+ if (event.source === "extension") {
50
+ return { action: "continue" };
51
+ }
52
+
53
+ if (event.text.trim() === "?template") {
54
+ if (ctx.hasUI) {
55
+ ctx.ui.notify(
56
+ "Pi extension template loaded. Try /template-hello, /template-dashboard, /template-layout, /template-skill-info, or inspect tool_call / tool_result logs.",
57
+ "info",
58
+ );
59
+ }
60
+ return { action: "handled" };
61
+ }
62
+
63
+ return { action: "continue" };
64
+ });
65
+
66
+ pi.on("turn_start", async (_event, ctx) => {
67
+ turnCount += 1;
68
+ if (ctx.hasUI) {
69
+ ctx.ui.setStatus("template", `Turn ${turnCount}...`);
70
+ }
71
+ });
72
+
73
+ pi.on("turn_end", async (_event, ctx) => {
74
+ if (ctx.hasUI) {
75
+ ctx.ui.setStatus("template", `Turn ${turnCount} complete`);
76
+ }
77
+ });
78
+
79
+ // Fires when tool execution begins. Use alongside tool_call when you need both
80
+ // lifecycle logging and argument mutation/blocking hooks.
81
+ pi.on("tool_execution_start", async (event) => {
82
+ console.log(`[pi-extension-template] tool_execution_start: ${event.toolName} (${event.toolCallId})`);
83
+ return undefined;
84
+ });
85
+
86
+ // Fires before a built-in or extension tool executes. Non-blocking logging only.
87
+ pi.on("tool_call", async (event) => {
88
+ console.log(`[pi-extension-template] tool_call: ${event.toolName} (${event.toolCallId})`);
89
+ return undefined;
90
+ });
91
+
92
+ // Fires after a tool finishes. Logs outcome; extensions may also return content/details overrides.
93
+ pi.on("tool_result", async (event) => {
94
+ const status = event.isError ? "error" : "ok";
95
+ console.log(`[pi-extension-template] tool_result: ${event.toolName} ${status} (${event.toolCallId})`);
96
+ return undefined;
97
+ });
98
+
99
+ pi.on("tool_execution_end", async (event) => {
100
+ const status = event.isError ? "error" : "ok";
101
+ console.log(`[pi-extension-template] tool_execution_end: ${event.toolName} ${status} (${event.toolCallId})`);
102
+ return undefined;
103
+ });
104
+
105
+ pi.on("session_shutdown", async () => {
106
+ turnCount = 0;
107
+ });
108
+
109
+ pi.registerCommand("template-hello", {
110
+ description: "Show a hello message from this Pi package template",
111
+ handler: async (args, ctx) => {
112
+ const name = args.trim() || "Pi";
113
+ if (ctx.hasUI) {
114
+ ctx.ui.notify(`Hello, ${name}!`, "info");
115
+ }
116
+ },
117
+ });
118
+
119
+ pi.registerCommand("template-status", {
120
+ description: "Append a TUI-only status card that is not sent to the LLM",
121
+ handler: async (args) => {
122
+ pi.appendEntry<TemplateStatusEntry>("template-status", {
123
+ message: args.trim() || "Template package loaded",
124
+ timestamp: Date.now(),
125
+ });
126
+ },
127
+ });
128
+ }
@@ -0,0 +1,90 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Text } from "@earendil-works/pi-tui";
3
+ import { Type } from "typebox";
4
+ import { formatGreeting, type GreetingMode } from "../lib/greeting.ts";
5
+ import { StringEnum } from "@earendil-works/pi-ai";
6
+
7
+ const greetParameters = Type.Object({
8
+ name: Type.String({ description: "Name to greet" }),
9
+ mode: StringEnum(["short", "friendly"] as const, {
10
+ description: "Greeting style. Prefer short unless the user asks for more warmth.",
11
+ }),
12
+ });
13
+
14
+ type GreetParams = {
15
+ name: string;
16
+ mode: GreetingMode;
17
+ };
18
+
19
+ function normalizeGreetArgs(args: unknown): GreetParams {
20
+ if (!args || typeof args !== "object") {
21
+ return args as GreetParams;
22
+ }
23
+
24
+ const input = args as {
25
+ name?: string;
26
+ mode?: GreetingMode;
27
+ greetingStyle?: GreetingMode;
28
+ };
29
+
30
+ if (input.greetingStyle && input.mode === undefined) {
31
+ return { name: input.name ?? "Pi", mode: input.greetingStyle };
32
+ }
33
+
34
+ return args as GreetParams;
35
+ }
36
+
37
+ export default function (pi: ExtensionAPI) {
38
+ pi.registerCommand("template-info", {
39
+ description: "Show TypeScript template information",
40
+ handler: async (_args, ctx) => {
41
+ if (ctx.hasUI) {
42
+ ctx.ui.notify("TypeScript-first Pi package template loaded.", "info");
43
+ }
44
+ },
45
+ });
46
+
47
+ pi.registerTool({
48
+ name: "template_greet",
49
+ label: "Template Greet",
50
+ description: "Return a typed greeting from the Pi package template",
51
+ promptSnippet: "template_greet: return a typed greeting from the template package",
52
+ promptGuidelines: [
53
+ "Use template_greet only when testing this template package or greeting the user.",
54
+ ],
55
+ parameters: greetParameters,
56
+ prepareArguments: normalizeGreetArgs,
57
+ async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
58
+ if (signal?.aborted) {
59
+ return { content: [{ type: "text", text: "Cancelled" }], details: {} };
60
+ }
61
+
62
+ const message = formatGreeting(params);
63
+
64
+ return {
65
+ content: [{ type: "text", text: message }],
66
+ details: { message, mode: params.mode },
67
+ };
68
+ },
69
+
70
+ renderCall(args, theme, _context) {
71
+ let text = theme.fg("toolTitle", theme.bold("template_greet "));
72
+ text += theme.fg("accent", `name=${args.name}`);
73
+ text += theme.fg("dim", ` mode=${args.mode}`);
74
+ return new Text(text, 0, 0);
75
+ },
76
+
77
+ renderResult(result, { expanded }, theme, _context) {
78
+ const details = result.details as { message: string; mode: string } | undefined;
79
+ const content = result.content[0];
80
+ const message =
81
+ details?.message ?? (content?.type === "text" ? content.text : "");
82
+
83
+ let text = theme.fg("success", "→ ") + theme.fg("text", message);
84
+ if (expanded && details) {
85
+ text += `\n${theme.fg("dim", `mode: ${details.mode}`)}`;
86
+ }
87
+ return new Text(text, 0, 0);
88
+ },
89
+ });
90
+ }
@@ -0,0 +1,49 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { formatTable } from "../../lib/format-table.ts";
3
+ import { defaultPackageLayoutConfig } from "./lib/config.ts";
4
+ import { getPackageResourceRows } from "./lib/stats.ts";
5
+
6
+ const config = defaultPackageLayoutConfig;
7
+
8
+ export default function (pi: ExtensionAPI) {
9
+ pi.on("session_start", async (_event, ctx) => {
10
+ if (ctx.hasUI) {
11
+ ctx.ui.setStatus(config.statusLabel, config.title);
12
+ }
13
+ });
14
+
15
+ pi.registerCommand("template-layout", {
16
+ description: "Show Pi package resource layout from a multi-file extension",
17
+ handler: async (_args, ctx) => {
18
+ if (!ctx.hasUI) {
19
+ return;
20
+ }
21
+
22
+ const rows = getPackageResourceRows();
23
+ const tableLines = formatTable(rows, [
24
+ { header: "Kind", render: (row) => row.kind },
25
+ { header: "Path", render: (row) => row.path },
26
+ { header: "Examples", render: (row) => row.examples },
27
+ ]);
28
+
29
+ const widgetLines = [config.title, "", ...tableLines];
30
+ if (config.showSkillHint) {
31
+ widgetLines.push("", "Skills: static skills/ dir + extensions/skill-bridge via resources_discover");
32
+ }
33
+
34
+ ctx.ui.setWidget("template-layout", widgetLines, { placement: "belowEditor" });
35
+ ctx.ui.notify("Package layout shown below the editor. Clear with /template-layout-clear.", "info");
36
+ },
37
+ });
38
+
39
+ pi.registerCommand("template-layout-clear", {
40
+ description: "Clear the package layout widget",
41
+ handler: async (_args, ctx) => {
42
+ if (!ctx.hasUI) {
43
+ return;
44
+ }
45
+
46
+ ctx.ui.setWidget("template-layout", undefined);
47
+ },
48
+ });
49
+ }
@@ -0,0 +1,9 @@
1
+ import { parsePackageLayoutConfig, type PackageLayoutConfig } from "../../../lib/config-contract.ts";
2
+
3
+ export type { PackageLayoutConfig } from "../../../lib/config-contract.ts";
4
+
5
+ export const defaultPackageLayoutConfig: PackageLayoutConfig = parsePackageLayoutConfig({
6
+ title: "Pi package layout",
7
+ statusLabel: "layout",
8
+ showSkillHint: true,
9
+ });
@@ -0,0 +1,35 @@
1
+ export interface PackageResourceRow {
2
+ kind: string;
3
+ path: string;
4
+ examples: string;
5
+ }
6
+
7
+ export function getPackageResourceRows(): PackageResourceRow[] {
8
+ return [
9
+ {
10
+ kind: "extensions",
11
+ path: "extensions/",
12
+ examples: "hello.ts, index.ts, tui-dashboard.ts, skill-bridge/, package-layout/",
13
+ },
14
+ {
15
+ kind: "skills",
16
+ path: "skills/",
17
+ examples: "example-skill/SKILL.md",
18
+ },
19
+ {
20
+ kind: "prompts",
21
+ path: "prompts/",
22
+ examples: "example.md",
23
+ },
24
+ {
25
+ kind: "themes",
26
+ path: "themes/",
27
+ examples: "example-theme.json",
28
+ },
29
+ {
30
+ kind: "shared lib",
31
+ path: "lib/",
32
+ examples: "greeting.ts, format-table.ts",
33
+ },
34
+ ];
35
+ }
@@ -0,0 +1,64 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { getCapabilities, setCapabilities } from "@earendil-works/pi-tui";
3
+ import {
4
+ getMaxLabelWidth,
5
+ shortenAssistantText,
6
+ shouldForceHyperlinks,
7
+ } from "../lib/shorten.ts";
8
+
9
+ function enableHyperlinksIfNeeded(): boolean {
10
+ if (!shouldForceHyperlinks()) return false;
11
+ const caps = getCapabilities();
12
+ if (caps.hyperlinks) return false;
13
+ setCapabilities({ ...caps, hyperlinks: true });
14
+ return true;
15
+ }
16
+
17
+ export default function (pi: ExtensionAPI) {
18
+ const forcedAtLoad = enableHyperlinksIfNeeded();
19
+
20
+ pi.on("session_start", async (_event, ctx) => {
21
+ const justForced = enableHyperlinksIfNeeded();
22
+ if (!ctx.hasUI || !(forcedAtLoad || justForced) || !getCapabilities().hyperlinks) {
23
+ return;
24
+ }
25
+ ctx.ui.setStatus("pi-short-links", "short-links: OSC 8 on");
26
+ setTimeout(() => {
27
+ try {
28
+ ctx.ui.setStatus("pi-short-links", undefined);
29
+ } catch {
30
+ // session may already be gone
31
+ }
32
+ }, 2500);
33
+ });
34
+
35
+ pi.on("message_end", (event, ctx) => {
36
+ if (event.message.role !== "assistant") return;
37
+
38
+ const message = event.message;
39
+ if (!("content" in message) || !Array.isArray(message.content)) return;
40
+
41
+ const maxWidth = getMaxLabelWidth();
42
+ let changed = false;
43
+ const content = message.content.map((block) => {
44
+ if (!block || typeof block !== "object" || !("type" in block)) return block;
45
+ if (block.type !== "text" || typeof block.text !== "string") return block;
46
+ const rewritten = shortenAssistantText(block.text, {
47
+ maxWidth,
48
+ cwd: ctx.cwd,
49
+ });
50
+ if (rewritten === block.text) return block;
51
+ changed = true;
52
+ return { ...block, text: rewritten };
53
+ });
54
+
55
+ if (!changed) return;
56
+
57
+ return {
58
+ message: {
59
+ ...message,
60
+ content,
61
+ },
62
+ };
63
+ });
64
+ }
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: template-skill-bridge
3
+ description: Template skill contributed by extensions/skill-bridge via resources_discover. Use when testing extension-provided Agent Skills or /skill:template-skill-bridge.
4
+ ---
5
+
6
+ # Template Skill Bridge
7
+
8
+ This skill is registered at runtime by `extensions/skill-bridge/index.ts` through the `resources_discover` event.
9
+
10
+ ## When to use
11
+
12
+ - Verify that an extension can contribute skills without editing `package.json`
13
+ - Demonstrate dynamic skill discovery alongside static `skills/` package resources
14
+
15
+ ## Response pattern
16
+
17
+ - Confirm the skill loaded from the extension directory
18
+ - Mention `/template-skill-info` for the companion command
19
+ - Point maintainers to `extensions/skill-bridge/SKILL.md` as the file to customize
@@ -0,0 +1,25 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+
5
+ const baseDir = dirname(fileURLToPath(import.meta.url));
6
+
7
+ export default function (pi: ExtensionAPI) {
8
+ pi.on("resources_discover", () => ({
9
+ skillPaths: [join(baseDir, "SKILL.md")],
10
+ }));
11
+
12
+ pi.registerCommand("template-skill-info", {
13
+ description: "Explain how this extension contributes an Agent Skill",
14
+ handler: async (_args, ctx) => {
15
+ if (!ctx.hasUI) {
16
+ return;
17
+ }
18
+
19
+ ctx.ui.notify(
20
+ "Skill bridge active. Try /skill:template-skill-bridge or ask the agent to use the template skill.",
21
+ "info",
22
+ );
23
+ },
24
+ });
25
+ }