@schilderlabs/pitown-package 0.2.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Schilder Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @schilderlabs/pitown-package
2
+
3
+ Optional Pi package resources for Pi Town.
4
+
5
+ This package is intended for deeper Pi integration later, including bundled prompts and extensions.
@@ -0,0 +1,10 @@
1
+ //#region src/index.d.ts
2
+ declare const bundledAgentNames: readonly ["leader", "supervisor", "scout", "planner", "worker", "reviewer", "docs-keeper"];
3
+ declare const townPackageName = "@schilderlabs/pitown-package";
4
+ declare function resolvePiTownPackageRoot(): string;
5
+ declare function resolvePiTownExtensionPath(): string;
6
+ declare function resolvePiTownMayorPromptPath(): string;
7
+ declare function readPiTownMayorPrompt(): string;
8
+ //#endregion
9
+ export { bundledAgentNames, readPiTownMayorPrompt, resolvePiTownExtensionPath, resolvePiTownMayorPromptPath, resolvePiTownPackageRoot, townPackageName };
10
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,34 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ //#region src/index.ts
6
+ const bundledAgentNames = [
7
+ "leader",
8
+ "supervisor",
9
+ "scout",
10
+ "planner",
11
+ "worker",
12
+ "reviewer",
13
+ "docs-keeper"
14
+ ];
15
+ const townPackageName = "@schilderlabs/pitown-package";
16
+ function resolvePackageRoot() {
17
+ return join(dirname(fileURLToPath(import.meta.url)), "..");
18
+ }
19
+ function resolvePiTownPackageRoot() {
20
+ return resolvePackageRoot();
21
+ }
22
+ function resolvePiTownExtensionPath() {
23
+ return join(resolvePackageRoot(), "pi", "extensions", "index.ts");
24
+ }
25
+ function resolvePiTownMayorPromptPath() {
26
+ return join(resolvePackageRoot(), "pi", "agents", "leader.md");
27
+ }
28
+ function readPiTownMayorPrompt() {
29
+ return readFileSync(resolvePiTownMayorPromptPath(), "utf-8");
30
+ }
31
+
32
+ //#endregion
33
+ export { bundledAgentNames, readPiTownMayorPrompt, resolvePiTownExtensionPath, resolvePiTownMayorPromptPath, resolvePiTownPackageRoot, townPackageName };
34
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nexport const bundledAgentNames = [\n\t\"leader\",\n\t\"supervisor\",\n\t\"scout\",\n\t\"planner\",\n\t\"worker\",\n\t\"reviewer\",\n\t\"docs-keeper\",\n] as const\n\nexport const townPackageName = \"@schilderlabs/pitown-package\"\n\nfunction resolvePackageRoot() {\n\treturn join(dirname(fileURLToPath(import.meta.url)), \"..\")\n}\n\nexport function resolvePiTownPackageRoot() {\n\treturn resolvePackageRoot()\n}\n\nexport function resolvePiTownExtensionPath() {\n\treturn join(resolvePackageRoot(), \"pi\", \"extensions\", \"index.ts\")\n}\n\nexport function resolvePiTownMayorPromptPath() {\n\treturn join(resolvePackageRoot(), \"pi\", \"agents\", \"leader.md\")\n}\n\nexport function readPiTownMayorPrompt() {\n\treturn readFileSync(resolvePiTownMayorPromptPath(), \"utf-8\")\n}\n"],"mappings":";;;;;AAIA,MAAa,oBAAoB;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAED,MAAa,kBAAkB;AAE/B,SAAS,qBAAqB;AAC7B,QAAO,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,KAAK;;AAG3D,SAAgB,2BAA2B;AAC1C,QAAO,oBAAoB;;AAG5B,SAAgB,6BAA6B;AAC5C,QAAO,KAAK,oBAAoB,EAAE,MAAM,cAAc,WAAW;;AAGlE,SAAgB,+BAA+B;AAC9C,QAAO,KAAK,oBAAoB,EAAE,MAAM,UAAU,YAAY;;AAG/D,SAAgB,wBAAwB;AACvC,QAAO,aAAa,8BAA8B,EAAE,QAAQ"}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@schilderlabs/pitown-package",
3
+ "version": "0.2.1",
4
+ "description": "Pi package resources for Pi Town",
5
+ "type": "module",
6
+ "imports": {
7
+ "#pitown-extension": "./pi/extensions/index.ts",
8
+ "#pitown-package-api": "./src/index.ts",
9
+ "#pitown-mayor-plan": "./pi/extensions/mayor-plan.ts",
10
+ "#pitown-town-tools": "./pi/extensions/town-tools.ts"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.mjs",
15
+ "types": "./dist/index.d.mts"
16
+ }
17
+ },
18
+ "types": "./dist/index.d.mts",
19
+ "files": [
20
+ "dist",
21
+ "pi",
22
+ "README.md"
23
+ ],
24
+ "engines": {
25
+ "node": ">=24"
26
+ },
27
+ "keywords": [
28
+ "pitown",
29
+ "pi-package",
30
+ "pi",
31
+ "agents",
32
+ "orchestration",
33
+ "night-shift"
34
+ ],
35
+ "pi": {
36
+ "extensions": [
37
+ "./pi/extensions"
38
+ ],
39
+ "prompts": [
40
+ "./pi/prompts"
41
+ ]
42
+ },
43
+ "dependencies": {
44
+ "@mariozechner/pi-ai": "0.58.3",
45
+ "@schilderlabs/pitown-core": "0.2.1"
46
+ },
47
+ "peerDependencies": {
48
+ "@mariozechner/pi-coding-agent": "*"
49
+ },
50
+ "devDependencies": {
51
+ "@mariozechner/pi-coding-agent": "0.58.3",
52
+ "eslint": "9.39.3",
53
+ "tsdown": "0.20.3",
54
+ "typescript": "5.9.2",
55
+ "vitest": "4.0.18"
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/RobSchilderr/pitown.git"
60
+ },
61
+ "homepage": "https://github.com/RobSchilderr/pitown#readme",
62
+ "bugs": {
63
+ "url": "https://github.com/RobSchilderr/pitown/issues"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
68
+ "license": "MIT",
69
+ "scripts": {
70
+ "build": "tsdown",
71
+ "lint": "eslint .",
72
+ "test": "vitest run --passWithNoTests",
73
+ "typecheck": "tsc --noEmit"
74
+ }
75
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: docs-keeper
3
+ description: Summarizes overnight outcomes into concise human-readable updates
4
+ tools: read, grep, find, ls
5
+ ---
6
+
7
+ You are the Pi Town docs keeper.
8
+
9
+ Your job is to summarize outcomes, blockers, and continuity for the next run.
10
+
11
+ Rules:
12
+ - keep summaries factual and compact
13
+ - capture blockers clearly
14
+ - preserve enough context for the next unattended cycle
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: mayor
3
+ description: Primary human-facing coordinator for a Pi Town repo
4
+ tools: read, grep, find, ls, pitown_board, pitown_delegate, pitown_message_agent, pitown_peek_agent, pitown_update_status
5
+ ---
6
+
7
+ You are the Pi Town mayor.
8
+
9
+ Your job is to coordinate work across the repo and act as the primary interface for the human operator.
10
+
11
+ Rules:
12
+ - `/plan` puts you into read-only planning mode; use it before execution when the user wants to think through the work first
13
+ - use `pitown_board` before delegating or redirecting work
14
+ - use `pitown_delegate` for bounded implementation, review, or documentation tasks
15
+ - use `pitown_peek_agent` before assuming a worker is blocked or idle
16
+ - use `pitown_message_agent` to redirect or clarify work instead of restating the full task
17
+ - use `pitown_update_status` to keep your own mayor state current in short, high-signal updates
18
+ - break large goals into bounded tasks before delegating
19
+ - prefer spawning focused workers over doing everything yourself
20
+ - check blockers, open questions, and active agent state before creating more work
21
+ - escalate clearly when the next step depends on a human product or policy decision
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: planner
3
+ description: Turns a bounded task into a concrete implementation plan
4
+ tools: read, grep, find, ls
5
+ ---
6
+
7
+ You are the Pi Town planner.
8
+
9
+ Your job is to turn the task and scout findings into a concrete implementation brief.
10
+
11
+ Rules:
12
+ - do not change code
13
+ - keep the plan small and specific
14
+ - include checks and risks
15
+ - keep the worker focused on one deliverable
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: reviewer
3
+ description: Reviews changes against task intent, repo rules, and code quality
4
+ tools: read, grep, find, ls, bash
5
+ ---
6
+
7
+ You are the Pi Town reviewer.
8
+
9
+ Review the current changes for correctness, safety, and completeness.
10
+
11
+ Focus on:
12
+ - task alignment
13
+ - code quality
14
+ - validation confidence
15
+ - whether the work is good enough to reach a human in polished form
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: scout
3
+ description: Fast codebase recon with concise handoff for other agents
4
+ tools: read, grep, find, ls, bash
5
+ ---
6
+
7
+ You are the Pi Town scout.
8
+
9
+ Your job is to gather the smallest useful body of context so another agent can act without rereading the entire codebase.
10
+
11
+ Output:
12
+ - relevant files
13
+ - architecture notes
14
+ - likely change points
15
+ - risks and unknowns
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: supervisor
3
+ description: Selects the next bounded unit of overnight work
4
+ tools: read, grep, find, ls
5
+ ---
6
+
7
+ You are the Pi Town supervisor.
8
+
9
+ Your job is to choose the next best bounded task for an unattended run.
10
+
11
+ Rules:
12
+ - do not edit code
13
+ - prefer one bounded deliverable
14
+ - prefer tasks that unlock downstream work
15
+ - avoid tasks that require remote side effects or human credentials
16
+ - stop and record blockers clearly when the task is underspecified
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: worker
3
+ description: Implements one bounded task at a time
4
+ tools: read, bash, edit, write, grep, find, ls
5
+ ---
6
+
7
+ You are the Pi Town worker.
8
+
9
+ You implement one bounded task at a time.
10
+
11
+ Rules:
12
+ - keep scope tight
13
+ - prefer explicit validations before finishing
14
+ - do not widen the task without saying so
15
+ - summarize what changed and what still needs follow-up
@@ -0,0 +1,288 @@
1
+ import { mkdtempSync } from "node:fs"
2
+ import { tmpdir } from "node:os"
3
+ import { join } from "node:path"
4
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
5
+ import {
6
+ createAgentSessionRecord,
7
+ createAgentState,
8
+ createTaskRecord,
9
+ readAgentState,
10
+ readTaskRecord,
11
+ writeAgentState,
12
+ writeTaskRecord,
13
+ } from "@schilderlabs/pitown-core"
14
+ import { describe, expect, it, vi } from "vitest"
15
+
16
+ import piTownPackage from "#pitown-extension"
17
+
18
+ type RegisteredHandler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>
19
+
20
+ function createRepoArtifacts() {
21
+ const repoRoot = mkdtempSync(join(tmpdir(), "pitown-repo-"))
22
+ const artifactsDir = join(mkdtempSync(join(tmpdir(), "pitown-home-")), "repos", "demo-repo")
23
+
24
+ const leaderSessionDir = join(artifactsDir, "agents", "leader", "sessions")
25
+ const workerSessionDir = join(artifactsDir, "agents", "worker-001", "sessions")
26
+ const reviewerSessionDir = join(artifactsDir, "agents", "reviewer-001", "sessions")
27
+ const leaderSessionFile = join(leaderSessionDir, "session_leader.jsonl")
28
+ const workerSessionFile = join(workerSessionDir, "session_worker.jsonl")
29
+
30
+ writeAgentState(
31
+ artifactsDir,
32
+ createAgentState({
33
+ agentId: "leader",
34
+ role: "leader",
35
+ status: "running",
36
+ task: "coordinate town work",
37
+ lastMessage: "checking board",
38
+ session: createAgentSessionRecord({
39
+ sessionDir: leaderSessionDir,
40
+ sessionPath: leaderSessionFile,
41
+ sessionId: "leader",
42
+ }),
43
+ }),
44
+ )
45
+ writeAgentState(
46
+ artifactsDir,
47
+ createAgentState({
48
+ agentId: "worker-001",
49
+ role: "worker",
50
+ status: "queued",
51
+ taskId: "task-123",
52
+ task: "fix the failing auth flow",
53
+ session: createAgentSessionRecord({
54
+ sessionDir: workerSessionDir,
55
+ sessionPath: workerSessionFile,
56
+ sessionId: "worker",
57
+ }),
58
+ }),
59
+ )
60
+ writeAgentState(
61
+ artifactsDir,
62
+ createAgentState({
63
+ agentId: "reviewer-001",
64
+ role: "reviewer",
65
+ status: "idle",
66
+ task: null,
67
+ session: createAgentSessionRecord({
68
+ sessionDir: reviewerSessionDir,
69
+ sessionPath: join(reviewerSessionDir, "session_reviewer.jsonl"),
70
+ sessionId: "reviewer",
71
+ }),
72
+ }),
73
+ )
74
+ writeTaskRecord(
75
+ artifactsDir,
76
+ createTaskRecord({
77
+ taskId: "task-123",
78
+ title: "fix the failing auth flow",
79
+ status: "queued",
80
+ role: "worker",
81
+ assignedAgentId: "worker-001",
82
+ createdBy: "leader",
83
+ }),
84
+ )
85
+
86
+ return { repoRoot, artifactsDir, leaderSessionFile, workerSessionFile }
87
+ }
88
+
89
+ function setup() {
90
+ const handlers: Record<string, RegisteredHandler[]> = {}
91
+ const tools = new Map<string, { execute: RegisteredHandler }>()
92
+ const commands = new Map<string, unknown>()
93
+ const flags = new Map<string, unknown>()
94
+ const activeTools: string[] = [
95
+ "read",
96
+ "grep",
97
+ "find",
98
+ "ls",
99
+ "pitown_board",
100
+ "pitown_delegate",
101
+ "pitown_message_agent",
102
+ "pitown_peek_agent",
103
+ "pitown_update_status",
104
+ ]
105
+ const appendedEntries: Array<{ customType: string; data: unknown }> = []
106
+ const pi = {
107
+ on: vi.fn((event: string, handler: RegisteredHandler) => {
108
+ ;(handlers[event] ??= []).push(handler)
109
+ }),
110
+ registerTool: vi.fn((definition: { name: string; execute: RegisteredHandler }) => {
111
+ tools.set(definition.name, definition)
112
+ }),
113
+ registerCommand: vi.fn((name: string, definition: unknown) => {
114
+ commands.set(name, definition)
115
+ }),
116
+ registerFlag: vi.fn((name: string, definition: unknown) => {
117
+ flags.set(name, definition)
118
+ }),
119
+ getFlag: vi.fn(() => undefined),
120
+ getActiveTools: vi.fn(() => [...activeTools]),
121
+ getAllTools: vi.fn(() => activeTools.map((name) => ({ name }))),
122
+ setActiveTools: vi.fn((toolNames: string[]) => {
123
+ activeTools.splice(0, activeTools.length, ...toolNames)
124
+ }),
125
+ appendEntry: vi.fn((customType: string, data?: unknown) => {
126
+ appendedEntries.push({ customType, data })
127
+ }),
128
+ sendMessage: vi.fn(),
129
+ } as unknown as ExtensionAPI & {
130
+ appendEntry: ReturnType<typeof vi.fn>
131
+ getActiveTools: ReturnType<typeof vi.fn>
132
+ getAllTools: ReturnType<typeof vi.fn>
133
+ getFlag: ReturnType<typeof vi.fn>
134
+ registerFlag: ReturnType<typeof vi.fn>
135
+ setActiveTools: ReturnType<typeof vi.fn>
136
+ sendMessage: ReturnType<typeof vi.fn>
137
+ }
138
+
139
+ piTownPackage(pi)
140
+
141
+ return { handlers, tools, commands, flags, appendedEntries, activeTools, pi }
142
+ }
143
+
144
+ function createToolContext(repoRoot: string, sessionFile: string) {
145
+ return {
146
+ hasUI: true,
147
+ ui: {
148
+ notify: vi.fn(),
149
+ setStatus: vi.fn(),
150
+ setWidget: vi.fn(),
151
+ theme: {
152
+ fg: vi.fn((_token: string, value: string) => value),
153
+ },
154
+ },
155
+ cwd: repoRoot,
156
+ sessionManager: {
157
+ getSessionFile: () => sessionFile,
158
+ getEntries: () => [],
159
+ },
160
+ }
161
+ }
162
+
163
+ describe("pi town extension", () => {
164
+ it("injects hidden town context for managed leader sessions", async () => {
165
+ const { repoRoot, leaderSessionFile } = createRepoArtifacts()
166
+ const { handlers } = setup()
167
+
168
+ const result = await handlers["before_agent_start"]?.[0]?.(
169
+ { systemPrompt: "base", prompt: "coordinate", images: [] },
170
+ createToolContext(repoRoot, leaderSessionFile),
171
+ )
172
+
173
+ expect(result).toEqual(
174
+ expect.objectContaining({
175
+ message: expect.objectContaining({
176
+ customType: "pitown-context",
177
+ display: false,
178
+ content: expect.stringContaining("Allowed Pi Town tools: pitown_board, pitown_delegate"),
179
+ }),
180
+ }),
181
+ )
182
+ })
183
+
184
+ it("renders the live board for town-managed sessions", async () => {
185
+ const { repoRoot, leaderSessionFile } = createRepoArtifacts()
186
+ const { tools } = setup()
187
+ const boardTool = tools.get("pitown_board")
188
+
189
+ const result = await boardTool?.execute("tool-call", {}, undefined, () => {}, createToolContext(repoRoot, leaderSessionFile))
190
+
191
+ expect(result).toEqual(
192
+ expect.objectContaining({
193
+ content: [expect.objectContaining({ text: expect.stringContaining("worker-001") })],
194
+ }),
195
+ )
196
+ })
197
+
198
+ it("blocks workers from messaging non-leader agents directly", async () => {
199
+ const { repoRoot, workerSessionFile } = createRepoArtifacts()
200
+ const { tools } = setup()
201
+ const messageTool = tools.get("pitown_message_agent")
202
+
203
+ await expect(
204
+ messageTool?.execute(
205
+ "tool-call",
206
+ { agentId: "reviewer-001", body: "please review now" },
207
+ undefined,
208
+ () => {},
209
+ createToolContext(repoRoot, workerSessionFile),
210
+ ),
211
+ ).rejects.toThrow("Only the leader may message non-leader agents")
212
+ })
213
+
214
+ it("updates worker state and task status through the status tool", async () => {
215
+ const { artifactsDir, repoRoot, workerSessionFile } = createRepoArtifacts()
216
+ const { tools } = setup()
217
+ const statusTool = tools.get("pitown_update_status")
218
+
219
+ await statusTool?.execute(
220
+ "tool-call",
221
+ { status: "completed", lastMessage: "auth flow fixed", blocked: false },
222
+ undefined,
223
+ () => {},
224
+ createToolContext(repoRoot, workerSessionFile),
225
+ )
226
+
227
+ expect(readAgentState(artifactsDir, "worker-001")).toEqual(
228
+ expect.objectContaining({
229
+ status: "completed",
230
+ lastMessage: "auth flow fixed",
231
+ }),
232
+ )
233
+ expect(readTaskRecord(artifactsDir, "task-123")).toEqual(
234
+ expect.objectContaining({
235
+ status: "completed",
236
+ }),
237
+ )
238
+ })
239
+
240
+ it("enables mayor plan mode with a read-only tool set", async () => {
241
+ const { repoRoot, leaderSessionFile } = createRepoArtifacts()
242
+ const { commands, activeTools, appendedEntries } = setup()
243
+ const planCommand = commands.get("plan") as { handler: RegisteredHandler } | undefined
244
+ const ctx = createToolContext(repoRoot, leaderSessionFile)
245
+
246
+ await planCommand?.handler([], ctx)
247
+
248
+ expect(activeTools).toEqual(["read", "grep", "find", "ls", "pitown_board", "pitown_peek_agent"])
249
+ expect(appendedEntries.at(-1)).toEqual(
250
+ expect.objectContaining({
251
+ customType: "pitown-mayor-plan",
252
+ }),
253
+ )
254
+ })
255
+
256
+ it("captures numbered mayor plan steps after a planning turn", async () => {
257
+ const { repoRoot, leaderSessionFile } = createRepoArtifacts()
258
+ const { commands, handlers, pi } = setup()
259
+ const planCommand = commands.get("plan") as { handler: RegisteredHandler } | undefined
260
+ const ctx = createToolContext(repoRoot, leaderSessionFile)
261
+
262
+ await planCommand?.handler([], ctx)
263
+ await handlers["agent_end"]?.[0]?.(
264
+ {
265
+ messages: [
266
+ {
267
+ role: "assistant",
268
+ content: [
269
+ {
270
+ type: "text",
271
+ text: "Plan:\n1. Inspect the current board and open tasks.\n2. Split the work into two bounded worker tasks.",
272
+ },
273
+ ],
274
+ },
275
+ ],
276
+ },
277
+ ctx,
278
+ )
279
+
280
+ expect(pi.sendMessage).toHaveBeenCalledWith(
281
+ expect.objectContaining({
282
+ customType: "pitown-mayor-plan-captured",
283
+ content: expect.stringContaining("1. ○ Inspect the current board and open tasks."),
284
+ }),
285
+ { triggerTurn: false },
286
+ )
287
+ })
288
+ })
@@ -0,0 +1,64 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
5
+ import { registerMayorPlanMode } from "#pitown-mayor-plan"
6
+ import { registerTownTools } from "#pitown-town-tools"
7
+
8
+ const extensionDir = dirname(fileURLToPath(import.meta.url))
9
+ const agentsDir = join(extensionDir, "..", "agents")
10
+
11
+ function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
12
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
13
+ if (!match) return { frontmatter: {}, body: content.trim() }
14
+
15
+ const frontmatter: Record<string, string> = {}
16
+ for (const rawLine of match[1]!.split(/\r?\n/)) {
17
+ const index = rawLine.indexOf(":")
18
+ if (index === -1) continue
19
+ frontmatter[rawLine.slice(0, index).trim()] = rawLine.slice(index + 1).trim()
20
+ }
21
+
22
+ return { frontmatter, body: match[2]!.trim() }
23
+ }
24
+
25
+ function listBundledAgents(): Array<{ name: string; description: string }> {
26
+ if (!existsSync(agentsDir)) return []
27
+
28
+ return readdirSync(agentsDir)
29
+ .filter((file) => file.endsWith(".md"))
30
+ .map((file) => {
31
+ const content = readFileSync(join(agentsDir, file), "utf-8")
32
+ const { frontmatter } = parseFrontmatter(content)
33
+ return {
34
+ name: frontmatter["name"] ?? file.replace(/\.md$/, ""),
35
+ description: frontmatter["description"] ?? "",
36
+ }
37
+ })
38
+ }
39
+
40
+ export default function piTownPackage(pi: ExtensionAPI) {
41
+ registerTownTools(pi)
42
+ registerMayorPlanMode(pi)
43
+
44
+ pi.registerCommand("town-status", {
45
+ description: "Show Pi Town package status",
46
+ handler: async (_args, ctx) => {
47
+ ctx.ui.notify("Pi Town package loaded. Use /town-agents to inspect bundled roles.", "info")
48
+ },
49
+ })
50
+
51
+ pi.registerCommand("town-agents", {
52
+ description: "List bundled Pi Town agents",
53
+ handler: async (_args, ctx) => {
54
+ const agents = listBundledAgents()
55
+ if (agents.length === 0) {
56
+ ctx.ui.notify("No bundled Pi Town agents were found.", "warning")
57
+ return
58
+ }
59
+
60
+ const lines = ["Bundled Pi Town agents:", ...agents.map((agent) => `- ${agent.name}: ${agent.description}`)]
61
+ ctx.ui.notify(lines.join("\n"), "info")
62
+ },
63
+ })
64
+ }