@maxiaochao/pi-toolkit 0.9.3 → 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.
- package/CHANGELOG.md +8 -0
- package/README.md +40 -90
- package/extensions/{btw.ts → btw/index.ts} +1 -1
- package/extensions/{tests/btw-history.mjs → btw/tests/history.mjs} +1 -1
- package/package.json +19 -10
- package/packages/cache-export/CHANGELOG.md +5 -0
- package/packages/cache-export/README.md +70 -0
- package/packages/cache-export/TESTING.md +17 -0
- package/packages/cache-export/docs/demo.gif +0 -0
- package/packages/cache-export/docs/screenshot.png +0 -0
- package/{extensions/cache-export/index.ts → packages/cache-export/extensions/cache-export.ts} +1 -1
- package/packages/cache-export/package.json +22 -0
- package/{extensions/cache-export → packages/cache-export/src}/render.ts +1 -1
- package/{extensions → packages}/cache-export/tests/e2e-browser.mjs +2 -2
- package/{extensions → packages}/cache-export/tests/run-tests.mjs +2 -2
- package/packages/codex-edit/CHANGELOG.md +32 -0
- package/packages/codex-edit/README.md +123 -0
- package/packages/codex-edit/TESTING.md +20 -0
- package/{extensions → packages}/codex-edit/codex-edit-explainer.html +41 -20
- package/packages/codex-edit/docs/demo.gif +0 -0
- package/packages/codex-edit/docs/screenshot.png +0 -0
- package/{extensions/codex-edit/index.ts → packages/codex-edit/extensions/codex-edit.ts} +3 -3
- package/packages/codex-edit/package.json +48 -0
- package/{extensions/codex-edit → packages/codex-edit/src}/parser.ts +1 -2
- package/{extensions → packages}/codex-edit/summary.md +1 -1
- package/{extensions → packages}/codex-edit/tests/package.test.mjs +12 -7
- package/{extensions → packages}/codex-edit/tests/parser.test.mjs +100 -3
- package/packages/tiny-subagent/CHANGELOG.md +12 -0
- package/packages/tiny-subagent/README.md +128 -0
- package/packages/tiny-subagent/TESTING.md +20 -0
- package/packages/tiny-subagent/agents/worker.md +9 -0
- package/packages/tiny-subagent/docs/demo.gif +0 -0
- package/packages/tiny-subagent/docs/screenshot.png +0 -0
- package/packages/tiny-subagent/extensions/tiny-subagent.ts +228 -0
- package/packages/tiny-subagent/package.json +44 -0
- package/packages/tiny-subagent/src/personas.ts +110 -0
- package/packages/tiny-subagent/src/runner.ts +319 -0
- package/packages/tiny-subagent/tests/run-tests.mjs +111 -0
- package/packages/todo/CHANGELOG.md +5 -0
- package/packages/todo/README.md +63 -0
- package/packages/todo/TESTING.md +20 -0
- package/packages/todo/docs/demo.gif +0 -0
- package/packages/todo/docs/screenshot.png +0 -0
- package/{extensions → packages/todo/extensions}/todo.ts +1 -1
- package/packages/todo/package.json +25 -0
- package/{extensions → packages/todo}/tests/todo-state.mjs +1 -1
- package/packages/worktree/CHANGELOG.md +5 -0
- package/packages/worktree/README.md +60 -0
- package/packages/worktree/TESTING.md +21 -0
- package/packages/worktree/docs/demo.gif +0 -0
- package/packages/worktree/docs/screenshot.png +0 -0
- package/packages/worktree/package.json +20 -0
- package/packages/worktree/scripts/install.mjs +26 -0
- package/packages/worktree/tests/install.test.mjs +25 -0
- package/packages/worktree/tests/run-tests.sh +46 -0
- package/scripts/install-agents.mjs +8 -18
- package/extensions/codex-edit/README.md +0 -53
- /package/extensions/{btw-scroll.ts → btw/scroll.ts} +0 -0
- /package/{extensions/cache-export → packages/cache-export/src}/tau-assets.ts +0 -0
- /package/{extensions → packages}/codex-edit/config.json +0 -0
- /package/{extensions/codex-edit → packages/codex-edit/src}/model-routing.ts +0 -0
- /package/{extensions → packages/todo/src}/todo-state.ts +0 -0
- /package/{bin → packages/worktree/bin}/pi-worktree +0 -0
- /package/{bin → packages/worktree/bin}/pi-worktree-completion.bash +0 -0
- /package/{skills → packages/worktree/skills}/pi-worktree/SKILL.md +0 -0
|
@@ -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
|
+
}
|