@maxiaochao/pi-toolkit 0.9.2 → 0.9.4

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +40 -90
  3. package/extensions/{btw.ts → btw/index.ts} +1 -1
  4. package/extensions/{tests/btw-history.mjs → btw/tests/history.mjs} +1 -1
  5. package/global/AGENTS.md +24 -49
  6. package/package.json +19 -10
  7. package/packages/cache-export/CHANGELOG.md +5 -0
  8. package/packages/cache-export/README.md +70 -0
  9. package/packages/cache-export/TESTING.md +17 -0
  10. package/packages/cache-export/docs/demo.gif +0 -0
  11. package/packages/cache-export/docs/screenshot.png +0 -0
  12. package/{extensions/cache-export/index.ts → packages/cache-export/extensions/cache-export.ts} +1 -1
  13. package/packages/cache-export/package.json +22 -0
  14. package/{extensions/cache-export → packages/cache-export/src}/render.ts +1 -1
  15. package/{extensions → packages}/cache-export/tests/e2e-browser.mjs +2 -2
  16. package/{extensions → packages}/cache-export/tests/run-tests.mjs +2 -2
  17. package/packages/codex-edit/CHANGELOG.md +32 -0
  18. package/packages/codex-edit/README.md +123 -0
  19. package/packages/codex-edit/TESTING.md +20 -0
  20. package/{extensions → packages}/codex-edit/codex-edit-explainer.html +41 -20
  21. package/packages/codex-edit/docs/demo.gif +0 -0
  22. package/packages/codex-edit/docs/screenshot.png +0 -0
  23. package/{extensions/codex-edit/index.ts → packages/codex-edit/extensions/codex-edit.ts} +3 -3
  24. package/packages/codex-edit/package.json +48 -0
  25. package/{extensions/codex-edit → packages/codex-edit/src}/parser.ts +1 -2
  26. package/{extensions → packages}/codex-edit/summary.md +1 -1
  27. package/{extensions → packages}/codex-edit/tests/package.test.mjs +12 -7
  28. package/{extensions → packages}/codex-edit/tests/parser.test.mjs +100 -3
  29. package/packages/tiny-subagent/CHANGELOG.md +12 -0
  30. package/packages/tiny-subagent/README.md +128 -0
  31. package/packages/tiny-subagent/TESTING.md +20 -0
  32. package/packages/tiny-subagent/agents/worker.md +9 -0
  33. package/packages/tiny-subagent/docs/demo.gif +0 -0
  34. package/packages/tiny-subagent/docs/screenshot.png +0 -0
  35. package/packages/tiny-subagent/extensions/tiny-subagent.ts +228 -0
  36. package/packages/tiny-subagent/package.json +44 -0
  37. package/packages/tiny-subagent/src/personas.ts +110 -0
  38. package/packages/tiny-subagent/src/runner.ts +319 -0
  39. package/packages/tiny-subagent/tests/run-tests.mjs +111 -0
  40. package/packages/todo/CHANGELOG.md +5 -0
  41. package/packages/todo/README.md +63 -0
  42. package/packages/todo/TESTING.md +20 -0
  43. package/packages/todo/docs/demo.gif +0 -0
  44. package/packages/todo/docs/screenshot.png +0 -0
  45. package/{extensions → packages/todo/extensions}/todo.ts +1 -1
  46. package/packages/todo/package.json +25 -0
  47. package/{extensions → packages/todo}/tests/todo-state.mjs +1 -1
  48. package/packages/worktree/CHANGELOG.md +5 -0
  49. package/packages/worktree/README.md +60 -0
  50. package/packages/worktree/TESTING.md +21 -0
  51. package/packages/worktree/docs/demo.gif +0 -0
  52. package/packages/worktree/docs/screenshot.png +0 -0
  53. package/packages/worktree/package.json +20 -0
  54. package/packages/worktree/scripts/install.mjs +26 -0
  55. package/packages/worktree/tests/install.test.mjs +25 -0
  56. package/packages/worktree/tests/run-tests.sh +46 -0
  57. package/scripts/install-agents.mjs +8 -18
  58. package/extensions/codex-edit/README.md +0 -53
  59. /package/extensions/{btw-scroll.ts → btw/scroll.ts} +0 -0
  60. /package/{extensions/cache-export → packages/cache-export/src}/tau-assets.ts +0 -0
  61. /package/{extensions → packages}/codex-edit/config.json +0 -0
  62. /package/{extensions/codex-edit → packages/codex-edit/src}/model-routing.ts +0 -0
  63. /package/{extensions → packages/todo/src}/todo-state.ts +0 -0
  64. /package/{bin → packages/worktree/bin}/pi-worktree +0 -0
  65. /package/{bin → packages/worktree/bin}/pi-worktree-completion.bash +0 -0
  66. /package/{skills → packages/worktree/skills}/pi-worktree/SKILL.md +0 -0
@@ -0,0 +1,128 @@
1
+ # @maxiaochao/pi-tiny-subagent
2
+
3
+ A small Pi extension that runs one fresh Pi child process for one focused prompt. It does not use the `pi-subagents` package and does not inherit the parent conversation or extensions.
4
+
5
+ ![Tiny Subagent production runner screenshot](docs/screenshot.png)
6
+
7
+ ## Why this package exists
8
+
9
+ This is the toolkit's deliberately small subagent model: one persona, one focused prompt, one isolated child context. It avoids a general orchestration framework and keeps child transcript noise out of the parent context while preserving process and protocol diagnostics.
10
+
11
+ ## Demo
12
+
13
+ ![Tiny Subagent demo](docs/demo.gif)
14
+
15
+ ```text
16
+ tiny_subagents({ action: "list" })
17
+ tiny_subagents({
18
+ agent: "worker",
19
+ prompt: "Inspect the authentication flow and summarize the risks"
20
+ })
21
+ ```
22
+
23
+ The child receives package-local persona instructions, selected built-in tools, and a fresh context; the parent receives the focused result or structured failure details.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pi install npm:@maxiaochao/pi-tiny-subagent
29
+ ```
30
+
31
+ The full `@maxiaochao/pi-toolkit` package also bundles this extension. Install the full toolkit or this standalone child package, not both.
32
+
33
+ Update the distribution you installed:
34
+
35
+ ```bash
36
+ pi update npm:@maxiaochao/pi-toolkit # bundled copy
37
+ pi update npm:@maxiaochao/pi-tiny-subagent # standalone child
38
+ ```
39
+
40
+ The versions are independent. Publishing the child does not update an already published toolkit version.
41
+
42
+ The package registers one tool: `tiny_subagents`.
43
+
44
+ ## Personas
45
+
46
+ The default `agents/worker.md` has this shape:
47
+
48
+ ```md
49
+ ---
50
+ name: worker
51
+ description: General-purpose worker for one focused task
52
+ tools: read, bash, edit, write, grep, find, ls
53
+ ---
54
+
55
+ You are a focused worker subagent.
56
+ ```
57
+
58
+ The supported frontmatter fields are:
59
+
60
+ - `name` (required): Persona name passed in the `agent` field.
61
+ - `description` (required): Short description returned by `action: "list"` so the parent model can choose a persona.
62
+ - `model` (optional): Default child model, such as `provider/model:high`. A call-level `model` overrides it; if both are absent, the child inherits the current parent-session model.
63
+ - `tools` (optional): Comma-separated allowlist of Pi built-in tools. Supported values are `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls`, and `powershell` on Windows. Extension tools such as `contact_supervisor` are not allowed. If omitted, the child uses Pi's default built-in tools.
64
+
65
+ For example, a read-only reviewer persona can restrict its tools and select a default model:
66
+
67
+ ```md
68
+ ---
69
+ name: reviewer
70
+ description: Review code and report correctness risks without modifying files
71
+ model: provider/model:high
72
+ tools: read, grep, find, ls
73
+ ---
74
+
75
+ Review the assigned code. Do not edit files.
76
+ ```
77
+
78
+ Persona files are loaded only from this package's `agents/*.md`; they do not share configuration with `pi-subagents`.
79
+
80
+ Before execution, the parent model should discover the package-local personas:
81
+
82
+ ```text
83
+ tiny_subagents({ action: "list" })
84
+ ```
85
+
86
+ It can then match the task to a persona by name or description and execute without `action`:
87
+
88
+ ```text
89
+ tiny_subagents({
90
+ agent: "worker",
91
+ prompt: "Inspect the authentication flow and summarize the risks"
92
+ })
93
+ ```
94
+
95
+ ## Execution
96
+
97
+ The child is started in the current working directory with a fresh context:
98
+
99
+ ```text
100
+ pi --mode json --print --no-session --no-extensions --no-skills
101
+ ```
102
+
103
+ The selected persona body is passed as an additional system prompt. Only built-in Pi tools can be enabled by `tools` in the persona file.
104
+
105
+ ## Live output
106
+
107
+ `tiny_subagents` sends a TUI update when the child starts and after each child `message_end` or `tool_result_end` event. While the child is running, the collapsed row shows the latest activity.
108
+
109
+ Press Pi's `Ctrl+O` tool-output expansion key to inspect the current child execution. The expanded `tiny_subagents` view shows:
110
+
111
+ - selected persona name and description
112
+ - selected model
113
+ - the task prompt
114
+ - the persona system prompt
115
+ - child assistant tool calls, tool results, and assistant text
116
+ - the final output and errors
117
+
118
+ This uses Pi's standard tool-output expansion state, so `Ctrl+O` also changes the expanded/collapsed display state of other tool rows. The added content and event tracking are local to `tiny_subagents`; they do not change other tools or the parent model context.
119
+
120
+ ## Errors and large output
121
+
122
+ Successful calls return the final assistant text. Failures return the Pi error message, stop reason, exit code, signal, stderr, protocol errors, and output produced before failure. Inline output is limited to 16 KiB. If stdout, stderr, or the final answer exceeds that size, the complete files are retained in a private temporary artifact directory and its path, byte counts, and `cleanup: "remove directory when no longer needed"` instruction are returned. Small-result temporary directories are removed automatically.
123
+
124
+ The child uses the same Pi installation, provider configuration, model catalog, and authentication environment as the parent process. The package only selects the model; it does not define providers or credentials.
125
+
126
+ ## Development and tests
127
+
128
+ The authoritative implementation is under `extensions/` and `src/`. See [TESTING.md](TESTING.md) for the test strategy, fixture cases, and latest baseline.
@@ -0,0 +1,20 @@
1
+ # Tiny Subagent test strategy
2
+
3
+ ## Flow
4
+
5
+ 1. `npm --prefix packages/tiny-subagent test` runs deterministic process and persona tests.
6
+ 2. `pi -ne -ns --no-session -e ./packages/tiny-subagent/extensions/tiny-subagent.ts` verifies extension loading.
7
+ 3. `npm pack ./packages/tiny-subagent --dry-run` verifies package contents.
8
+ 4. Verify the README example and the production-runner media `docs/screenshot.png` and `docs/demo.gif` are present in the tarball.
9
+
10
+ ## Cases
11
+
12
+ - Resolve package-local personas and validate frontmatter.
13
+ - Start an isolated child Pi process with selected model and tools.
14
+ - Stream child activity while keeping it outside the parent conversation.
15
+ - Preserve detailed process/protocol failures and oversized artifacts.
16
+ - Render collapsed and expanded tool output.
17
+
18
+ ## Latest baseline
19
+
20
+ All deterministic child-process and persona cases pass. Release validation also requires the extension load check.
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: worker
3
+ description: General-purpose worker for one focused task
4
+ tools: read, bash, edit, write, grep, find, ls
5
+ ---
6
+
7
+ You are a focused worker subagent.
8
+
9
+ Work only on the task in the user prompt. Inspect the current project before making claims. Use the available built-in tools when needed. Do not delegate to another agent. Finish with a concise result, including changes made, validation performed, and any remaining risks.
@@ -0,0 +1,228 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { StringEnum } from "@earendil-works/pi-ai";
4
+ import { getMarkdownTheme, keyHint } from "@earendil-works/pi-coding-agent";
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
7
+ import { Type } from "typebox";
8
+ import { discoverPackageAgents } from "../src/personas.ts";
9
+ import { runTinySubagent, type TinySubagentResult } from "../src/runner.ts";
10
+
11
+ const ACTIONS = ["list"] as const;
12
+ const TinySubagentParams = Type.Union([
13
+ Type.Object({
14
+ action: StringEnum(ACTIONS, { description: "Discover package-local personas before execution." }),
15
+ }),
16
+ Type.Object({
17
+ prompt: Type.String({ minLength: 1, description: "One focused prompt for the subagent." }),
18
+ agent: Type.Optional(Type.String({ minLength: 1, description: "Persona name selected from action=list. Defaults to worker." })),
19
+ model: Type.Optional(Type.String({ minLength: 1, description: "Optional Pi model, for example provider/model:high." })),
20
+ }),
21
+ ]);
22
+
23
+ type ToolParams = {
24
+ action?: "list";
25
+ prompt?: string;
26
+ agent?: string;
27
+ model?: string;
28
+ };
29
+
30
+ const packageAgentsDir = fileURLToPath(new URL("../agents/", import.meta.url));
31
+
32
+ function modelId(ctx: { model?: { provider: string; id: string } }): string | undefined {
33
+ return ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined;
34
+ }
35
+
36
+ function listText(discovery: ReturnType<typeof discoverPackageAgents>): string {
37
+ const lines = ["Available tiny-subagent personas:"];
38
+ if (discovery.agents.length === 0) lines.push("- (none)");
39
+ for (const agent of discovery.agents) {
40
+ const model = agent.model ?? "inherits current session model";
41
+ const tools = agent.tools?.join(", ") ?? "Pi default built-in tools";
42
+ lines.push(`- ${agent.name}: ${agent.description} (model: ${model}; tools: ${tools})`);
43
+ }
44
+ if (discovery.diagnostics.length > 0) {
45
+ lines.push("", "Persona diagnostics:", ...discovery.diagnostics.map((diagnostic) => `- ${diagnostic}`));
46
+ }
47
+ return lines.join("\n");
48
+ }
49
+
50
+ function errorText(result: TinySubagentResult): string {
51
+ const lines = ["Tiny subagent failed."];
52
+ if (result.errorMessage) lines.push(`Error: ${result.errorMessage}`);
53
+ if (result.stopReason) lines.push(`Stop reason: ${result.stopReason}`);
54
+ if (result.exitCode !== null) lines.push(`Exit code: ${result.exitCode}`);
55
+ if (result.signal) lines.push(`Signal: ${result.signal}`);
56
+ if (result.stderr.trim()) lines.push(`stderr:\n${result.stderr.trim()}`);
57
+ if (result.protocolErrors.length > 0) lines.push(`Protocol errors:\n${result.protocolErrors.join("\n")}`);
58
+ if (result.output.trim()) lines.push(`Output before failure:\n${result.output.trim()}`);
59
+ if (result.artifacts) lines.push(`Full output artifacts: ${result.artifacts.directory}`);
60
+ return lines.join("\n");
61
+ }
62
+
63
+ function renderSummary(result: TinySubagentResult): string {
64
+ const text = result.ok ? result.output || "(no output)" : errorText(result);
65
+ if (!result.ok || !result.artifacts) return text;
66
+ return `${text}\n\nFull output artifacts: ${result.artifacts.directory}`;
67
+ }
68
+
69
+ type TinySubagentDetails = TinySubagentResult & {
70
+ agent: string;
71
+ description: string;
72
+ systemPrompt: string;
73
+ prompt: string;
74
+ personaPath: string;
75
+ };
76
+
77
+ function messageText(message: TinySubagentDetails["messages"][number]): string {
78
+ if (typeof message.content === "string") return message.content;
79
+ if (!Array.isArray(message.content)) return "";
80
+ return message.content
81
+ .filter((part) => part.type === "text" && typeof part.text === "string")
82
+ .map((part) => part.text as string)
83
+ .join("");
84
+ }
85
+
86
+ function messageLines(message: TinySubagentDetails["messages"][number]): string[] {
87
+ if (message.role === "user") return [];
88
+ if (message.role === "assistant" && Array.isArray(message.content)) {
89
+ return message.content.flatMap((part) => {
90
+ if (part.type === "toolCall") {
91
+ const args = part.arguments === undefined ? "" : ` ${JSON.stringify(part.arguments)}`;
92
+ return [`→ ${part.name ?? "tool"}${args}`];
93
+ }
94
+ if (part.type === "text" && typeof part.text === "string") return [`assistant: ${part.text}`];
95
+ return [];
96
+ });
97
+ }
98
+ if (message.role === "toolResult") {
99
+ const prefix = `← ${message.toolName ?? "tool"}${message.isError ? " [error]" : ""}`;
100
+ const text = messageText(message);
101
+ return [prefix, ...(text ? text.split("\n").map((line) => ` ${line}`) : [])];
102
+ }
103
+ const text = messageText(message);
104
+ return text ? [`${message.role ?? "message"}: ${text}`] : [];
105
+ }
106
+
107
+ function latestActivity(messages: TinySubagentDetails["messages"]): string {
108
+ const lines = messages.flatMap(messageLines);
109
+ return lines.length > 0 ? lines[lines.length - 1] : "starting child Pi...";
110
+ }
111
+
112
+ export default function (pi: ExtensionAPI) {
113
+ if (process.env.PI_SUBAGENT_CHILD === "1") return;
114
+
115
+ pi.registerTool({
116
+ name: "tiny_subagents",
117
+ label: "Tiny Subagent",
118
+ description: "Delegate one focused task to a package-local persona in a fresh Pi context. Supports persona discovery with action=list and execution with agent, prompt, and an optional model override. No parent conversation or extensions are inherited.",
119
+ promptSnippet: "Run one focused task through a package-local tiny subagent persona",
120
+ promptGuidelines: ["Before execution, call tiny_subagents with action 'list' once and choose an executable persona by name or description. After receiving the list, do not list again; omit action and run one focused prompt with the selected agent."],
121
+ parameters: TinySubagentParams,
122
+
123
+ async execute(_toolCallId, params: ToolParams, signal, onUpdate, ctx) {
124
+ const discovery = discoverPackageAgents(packageAgentsDir);
125
+ if (params.action === "list") {
126
+ return {
127
+ content: [{ type: "text" as const, text: listText(discovery) }],
128
+ details: { action: "list", agents: discovery.agents, diagnostics: discovery.diagnostics },
129
+ };
130
+ }
131
+
132
+ if (!params.prompt?.trim()) {
133
+ const text = "prompt is required for execution. Use action: list to inspect available personas.";
134
+ return { content: [{ type: "text" as const, text }], details: { errorMessage: text }, isError: true };
135
+ }
136
+
137
+ const selectedName = params.agent?.trim() || "worker";
138
+ const persona = discovery.agents.find((agent) => agent.name === selectedName);
139
+ if (!persona) {
140
+ const text = `Unknown persona '${selectedName}'.\n\n${listText(discovery)}`;
141
+ return { content: [{ type: "text" as const, text }], details: { errorMessage: text, availableAgents: discovery.agents.map((agent) => agent.name) }, isError: true };
142
+ }
143
+
144
+ const explicitModel = params.model?.trim();
145
+ const selectedModel = explicitModel || persona.model || modelId(ctx);
146
+ const inheritThinking = !explicitModel && !persona.model ? ctx.thinkingLevel : undefined;
147
+ const addDisplayDetails = (partial: TinySubagentResult) => ({
148
+ ...partial,
149
+ agent: persona.name,
150
+ description: persona.description,
151
+ systemPrompt: persona.systemPrompt,
152
+ prompt: params.prompt as string,
153
+ personaPath: persona.filePath,
154
+ });
155
+ const result = await runTinySubagent(params.prompt, {
156
+ ...(selectedModel ? { model: selectedModel } : {}),
157
+ ...(inheritThinking ? { thinking: inheritThinking } : {}),
158
+ ...(persona.tools ? { tools: persona.tools } : {}),
159
+ systemPrompt: persona.systemPrompt,
160
+ cwd: ctx.cwd,
161
+ signal,
162
+ onUpdate: (partial) => {
163
+ onUpdate?.({
164
+ content: [{ type: "text" as const, text: partial.output || "(running...)" }],
165
+ details: addDisplayDetails(partial),
166
+ });
167
+ },
168
+ });
169
+ return {
170
+ content: [{ type: "text" as const, text: renderSummary(result) }],
171
+ details: addDisplayDetails(result),
172
+ ...(result.ok ? {} : { isError: true }),
173
+ };
174
+ },
175
+
176
+ renderCall(args, theme) {
177
+ if (args.action === "list") return new Text(theme.fg("toolTitle", theme.bold("tiny_subagents list")), 0, 0);
178
+ const agent = args.agent || "worker";
179
+ const model = args.model ? ` ${theme.fg("accent", String(args.model))}` : "";
180
+ const prompt = args.prompt ? String(args.prompt).replace(/\s+/g, " ") : "...";
181
+ const preview = prompt.length > 80 ? `${prompt.slice(0, 80)}...` : prompt;
182
+ return new Text(`${theme.fg("toolTitle", theme.bold("tiny_subagents"))} ${theme.fg("accent", agent)}${model}\n ${theme.fg("dim", preview)}`, 0, 0);
183
+ },
184
+
185
+ renderResult(result, { expanded, isPartial }, theme) {
186
+ const details = result.details as TinySubagentDetails | undefined;
187
+ if (!details?.agent || !details.messages) {
188
+ const content = result.content[0];
189
+ return new Text(content?.type === "text" ? content.text : "(no output)", 0, 0);
190
+ }
191
+
192
+ const model = details.model ?? "parent session model";
193
+ const status = isPartial ? "running" : details.ok ? "completed" : "failed";
194
+ const statusColor = isPartial ? "warning" : details.ok ? "success" : "error";
195
+ const statusText = theme.fg(statusColor, status);
196
+ const fullOutputPath = details.artifacts?.finalOutput;
197
+ let finalOutput = details.output || "(no output)";
198
+ if (expanded && fullOutputPath && existsSync(fullOutputPath)) finalOutput = readFileSync(fullOutputPath, "utf8");
199
+
200
+ if (expanded) {
201
+ const container = new Container();
202
+ container.addChild(new Text(`${theme.fg("toolTitle", theme.bold(details.agent))} ${statusText}`, 0, 0));
203
+ container.addChild(new Text(theme.fg("muted", `Model: ${model}`), 0, 0));
204
+ container.addChild(new Text(theme.fg("muted", `Description: ${details.description}`), 0, 0));
205
+ container.addChild(new Spacer(1));
206
+ container.addChild(new Text(theme.fg("muted", "--- Prompt ---"), 0, 0));
207
+ container.addChild(new Text(details.prompt, 0, 0));
208
+ container.addChild(new Spacer(1));
209
+ container.addChild(new Text(theme.fg("muted", "--- Agent identity ---"), 0, 0));
210
+ container.addChild(new Markdown(details.systemPrompt || "(empty)", 0, 0, getMarkdownTheme()));
211
+ container.addChild(new Spacer(1));
212
+ container.addChild(new Text(theme.fg("muted", "--- Child transcript ---"), 0, 0));
213
+ const transcript = details.messages.flatMap(messageLines);
214
+ if (transcript.length === 0) container.addChild(new Text(theme.fg("dim", "waiting for child events..."), 0, 0));
215
+ else for (const line of transcript) container.addChild(new Text(line, 0, 0));
216
+ container.addChild(new Spacer(1));
217
+ container.addChild(new Text(theme.fg("muted", "--- Final output ---"), 0, 0));
218
+ container.addChild(new Markdown(finalOutput, 0, 0, getMarkdownTheme()));
219
+ if (details.errorMessage) container.addChild(new Text(theme.fg("error", `Error: ${details.errorMessage}`), 0, 0));
220
+ return container;
221
+ }
222
+
223
+ const preview = latestActivity(details.messages).replace(/\s+/g, " ");
224
+ const compactPreview = preview.length > 100 ? `${preview.slice(0, 100)}...` : preview;
225
+ return new Text(`${theme.fg("toolTitle", theme.bold(details.agent))} ${statusText} ${theme.fg("dim", `(${model})`)}\n ${theme.fg("dim", compactPreview)}\n ${theme.fg("muted", `(${keyHint("app.tools.expand", "to expand")})`)}`, 0, 0);
226
+ },
227
+ });
228
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@maxiaochao/pi-tiny-subagent",
3
+ "version": "0.1.1",
4
+ "description": "A tiny Pi subagent tool with package-local personas and detailed process errors.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "subagent"
10
+ ],
11
+ "files": [
12
+ "extensions",
13
+ "src",
14
+ "agents",
15
+ "docs",
16
+ "README.md",
17
+ "TESTING.md",
18
+ "CHANGELOG.md",
19
+ "package.json"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/chaochaoxiaochao/pi-toolkit.git",
24
+ "directory": "packages/tiny-subagent"
25
+ },
26
+ "pi": {
27
+ "extensions": [
28
+ "./extensions/tiny-subagent.ts"
29
+ ]
30
+ },
31
+ "peerDependencies": {
32
+ "@earendil-works/pi-ai": "*",
33
+ "@earendil-works/pi-coding-agent": "*",
34
+ "@earendil-works/pi-tui": "*",
35
+ "typebox": "*"
36
+ },
37
+ "scripts": {
38
+ "test": "node --experimental-strip-types tests/run-tests.mjs",
39
+ "load-test": "pi -ne -ns --no-session -e ./extensions/tiny-subagent.ts"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }
@@ -0,0 +1,110 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export const BUILTIN_TOOLS = new Set([
5
+ "read",
6
+ "bash",
7
+ "edit",
8
+ "write",
9
+ "grep",
10
+ "find",
11
+ "ls",
12
+ "powershell",
13
+ ]);
14
+
15
+ export interface AgentDefinition {
16
+ name: string;
17
+ description: string;
18
+ model?: string;
19
+ tools?: string[];
20
+ systemPrompt: string;
21
+ filePath: string;
22
+ }
23
+
24
+ export interface AgentDiscovery {
25
+ agents: AgentDefinition[];
26
+ diagnostics: string[];
27
+ }
28
+
29
+ function unquote(value: string): string {
30
+ const trimmed = value.trim();
31
+ if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
32
+ return trimmed.slice(1, -1);
33
+ }
34
+ return trimmed;
35
+ }
36
+
37
+ function parseList(value: string): string[] | undefined {
38
+ let source = value.trim();
39
+ if (!source) return undefined;
40
+ if (source.startsWith("[") && source.endsWith("]")) source = source.slice(1, -1);
41
+ const items = source.split(",").map((item) => unquote(item).trim()).filter(Boolean);
42
+ return items.length > 0 ? items : undefined;
43
+ }
44
+
45
+ export function parseAgentMarkdown(content: string, filePath = "<memory>"): AgentDefinition {
46
+ const lines = content.replace(/^\uFEFF/, "").split(/\r?\n/);
47
+ if (lines[0]?.trim() !== "---") throw new Error("missing YAML frontmatter");
48
+
49
+ const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
50
+ if (end < 0) throw new Error("unterminated YAML frontmatter");
51
+
52
+ const fields = new Map<string, string>();
53
+ for (const line of lines.slice(1, end)) {
54
+ if (!line.trim() || line.trim().startsWith("#")) continue;
55
+ const match = line.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/);
56
+ if (!match) throw new Error(`invalid frontmatter line: ${line}`);
57
+ fields.set(match[1], unquote(match[2]));
58
+ }
59
+
60
+ const name = fields.get("name")?.trim();
61
+ const description = fields.get("description")?.trim();
62
+ if (!name) throw new Error("frontmatter name is required");
63
+ if (!description) throw new Error("frontmatter description is required");
64
+
65
+ const rawTools = fields.get("tools");
66
+ const tools = rawTools === undefined ? undefined : parseList(rawTools);
67
+ if (rawTools !== undefined && !tools) throw new Error("frontmatter tools must not be empty");
68
+ for (const tool of tools ?? []) {
69
+ if (!BUILTIN_TOOLS.has(tool)) throw new Error(`unsupported non-builtin tool: ${tool}`);
70
+ }
71
+
72
+ const model = fields.get("model")?.trim() || undefined;
73
+ return {
74
+ name,
75
+ description,
76
+ ...(model ? { model } : {}),
77
+ ...(tools ? { tools } : {}),
78
+ systemPrompt: lines.slice(end + 1).join("\n").trim(),
79
+ filePath,
80
+ };
81
+ }
82
+
83
+ export function discoverPackageAgents(agentsDir: string): AgentDiscovery {
84
+ const agents: AgentDefinition[] = [];
85
+ const diagnostics: string[] = [];
86
+ let entries: string[];
87
+ try {
88
+ entries = readdirSync(agentsDir).filter((entry) => entry.endsWith(".md")).sort();
89
+ } catch (error) {
90
+ return { agents, diagnostics: [`Cannot read persona directory ${agentsDir}: ${error instanceof Error ? error.message : String(error)}`] };
91
+ }
92
+
93
+ const names = new Set<string>();
94
+ for (const entry of entries) {
95
+ const filePath = join(agentsDir, entry);
96
+ try {
97
+ if (!statSync(filePath).isFile()) continue;
98
+ const definition = parseAgentMarkdown(readFileSync(filePath, "utf8"), filePath);
99
+ if (names.has(definition.name)) {
100
+ diagnostics.push(`${filePath}: duplicate persona name '${definition.name}'`);
101
+ continue;
102
+ }
103
+ names.add(definition.name);
104
+ agents.push(definition);
105
+ } catch (error) {
106
+ diagnostics.push(`${filePath}: ${error instanceof Error ? error.message : String(error)}`);
107
+ }
108
+ }
109
+ return { agents, diagnostics };
110
+ }