@matyah00/openpi 0.1.4 → 0.1.5

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.
@@ -1,10 +1,13 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { Type } from "@sinclair/typebox";
3
- import { Text } from "@mariozechner/pi-tui";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { Text } from "@earendil-works/pi-tui";
4
4
  import { spawn } from "child_process";
5
- import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from "fs";
5
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "fs";
6
+ import { tmpdir } from "os";
6
7
  import { join, resolve } from "path";
8
+ import { parse as parseYaml } from "yaml";
7
9
  import { bundledAgentsDir } from "./lib/packagePaths.ts";
10
+ import { arrayField, parseMarkdownFrontmatter, stringField } from "./lib/markdown.ts";
8
11
 
9
12
  type ChainStep = { agent: string; prompt: string };
10
13
  type ChainDef = { name: string; description: string; steps: ChainStep[] };
@@ -16,57 +19,48 @@ function displayName(name: string): string {
16
19
  }
17
20
 
18
21
  function parseChainYaml(raw: string): ChainDef[] {
19
- const chains: ChainDef[] = [];
20
- let current: ChainDef | null = null;
21
- let currentStep: ChainStep | null = null;
22
+ const parsed = parseYaml(raw);
23
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return [];
22
24
 
23
- for (const line of raw.split("\n")) {
24
- const chainMatch = line.match(/^(\S[^:]*):$/);
25
- if (chainMatch) {
26
- if (current && currentStep) current.steps.push(currentStep);
27
- current = { name: chainMatch[1].trim(), description: "", steps: [] };
28
- currentStep = null;
29
- chains.push(current);
30
- continue;
31
- }
32
- const descMatch = line.match(/^\s+description:\s+(.+)$/);
33
- if (descMatch && current && !currentStep) {
34
- current.description = descMatch[1].trim().replace(/^["']|["']$/g, "");
35
- continue;
36
- }
37
- const agentMatch = line.match(/^\s+-\s+agent:\s+(.+)$/);
38
- if (agentMatch && current) {
39
- if (currentStep) current.steps.push(currentStep);
40
- currentStep = { agent: agentMatch[1].trim(), prompt: "" };
41
- continue;
42
- }
43
- const promptMatch = line.match(/^\s+prompt:\s+(.+)$/);
44
- if (promptMatch && currentStep) {
45
- currentStep.prompt = promptMatch[1].trim().replace(/^["']|["']$/g, "").replace(/\\n/g, "\n");
46
- }
47
- }
48
- if (current && currentStep) current.steps.push(currentStep);
49
- return chains;
25
+ return Object.entries(parsed as Record<string, unknown>).flatMap(([name, value]) => {
26
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
27
+ const chain = value as { description?: unknown; steps?: unknown };
28
+ if (!Array.isArray(chain.steps)) return [];
29
+ const steps = chain.steps.flatMap((step): ChainStep[] => {
30
+ if (!step || typeof step !== "object" || Array.isArray(step)) return [];
31
+ const item = step as { agent?: unknown; prompt?: unknown };
32
+ if (typeof item.agent !== "string" || typeof item.prompt !== "string") return [];
33
+ return [{ agent: item.agent, prompt: item.prompt }];
34
+ });
35
+ if (!steps.length) return [];
36
+ return [{
37
+ name,
38
+ description: typeof chain.description === "string" ? chain.description : "",
39
+ steps,
40
+ }];
41
+ });
50
42
  }
51
43
 
52
44
  function parseAgentFile(filePath: string): AgentDef | null {
53
45
  const raw = readFileSync(filePath, "utf-8");
54
- const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
55
- if (!match) return null;
56
- const frontmatter: Record<string, string> = {};
57
- for (const line of match[1].split("\n")) {
58
- const idx = line.indexOf(":");
59
- if (idx > 0) frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
60
- }
61
- if (!frontmatter.name) return null;
46
+ const { frontmatter, body } = parseMarkdownFrontmatter(raw);
47
+ const name = stringField(frontmatter.name);
48
+ if (!name) return null;
62
49
  return {
63
- name: frontmatter.name,
64
- description: frontmatter.description || "",
65
- tools: frontmatter.tools || "read,grep,find,ls",
66
- systemPrompt: match[2].trim(),
50
+ name,
51
+ description: stringField(frontmatter.description),
52
+ tools: arrayField(frontmatter.tools).join(",") || "read,grep,find,ls",
53
+ systemPrompt: body,
67
54
  };
68
55
  }
69
56
 
57
+ function writeSystemPromptFile(agentName: string, systemPrompt: string): { dir: string; filePath: string } {
58
+ const dir = mkdtempSync(join(tmpdir(), "openpi-chain-"));
59
+ const filePath = join(dir, `system-${agentName.replace(/[^\w.-]+/g, "_")}.md`);
60
+ writeFileSync(filePath, systemPrompt, { encoding: "utf-8", mode: 0o600 });
61
+ return { dir, filePath };
62
+ }
63
+
70
64
  function scanAgentDirs(cwd: string): Map<string, AgentDef> {
71
65
  const dirs = [join(cwd, ".pi", "agents"), join(cwd, "agents"), bundledAgentsDir];
72
66
  const agents = new Map<string, AgentDef>();
@@ -125,13 +119,14 @@ export default function (pi: ExtensionAPI) {
125
119
  const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
126
120
  const agentKey = agentDef.name.toLowerCase().replace(/\s+/g, "-");
127
121
  const agentSessionFile = join(sessionDir, `chain-${agentKey}.json`);
122
+ const systemPrompt = writeSystemPromptFile(agentKey, agentDef.systemPrompt);
128
123
  const args = [
129
124
  "--mode", "json",
130
125
  "-p",
131
126
  "--no-extensions",
132
127
  "--tools", agentDef.tools,
133
128
  "--thinking", "off",
134
- "--append-system-prompt", agentDef.systemPrompt,
129
+ "--append-system-prompt", systemPrompt.filePath,
135
130
  "--session", agentSessionFile,
136
131
  ];
137
132
  if (model) args.splice(4, 0, "--model", model);
@@ -143,6 +138,7 @@ export default function (pi: ExtensionAPI) {
143
138
  const chunks: string[] = [];
144
139
 
145
140
  return new Promise((resolveDone) => {
141
+ const cleanupPrompt = () => rmSync(systemPrompt.dir, { recursive: true, force: true });
146
142
  const proc = spawn("pi", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } });
147
143
  const timer = setInterval(() => {
148
144
  state.elapsed = Date.now() - start;
@@ -171,12 +167,14 @@ export default function (pi: ExtensionAPI) {
171
167
  proc.stderr!.on("data", () => {});
172
168
  proc.on("close", (code) => {
173
169
  clearInterval(timer);
170
+ cleanupPrompt();
174
171
  state.elapsed = Date.now() - start;
175
172
  if (code === 0) agentSessions.set(agentKey, agentSessionFile);
176
173
  resolveDone({ output: chunks.join(""), exitCode: code ?? 1, elapsed: state.elapsed });
177
174
  });
178
175
  proc.on("error", (error) => {
179
176
  clearInterval(timer);
177
+ cleanupPrompt();
180
178
  resolveDone({ output: `Error spawning agent: ${error.message}`, exitCode: 1, elapsed: Date.now() - start });
181
179
  });
182
180
  });
@@ -1,10 +1,13 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { Type } from "@sinclair/typebox";
3
- import { Text } from "@mariozechner/pi-tui";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { Text } from "@earendil-works/pi-tui";
4
4
  import { spawn } from "child_process";
5
- import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from "fs";
5
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "fs";
6
+ import { tmpdir } from "os";
6
7
  import { join, resolve } from "path";
8
+ import { parse as parseYaml } from "yaml";
7
9
  import { bundledAgentsDir } from "./lib/packagePaths.ts";
10
+ import { arrayField, parseMarkdownFrontmatter, stringField } from "./lib/markdown.ts";
8
11
 
9
12
  type AgentDef = {
10
13
  name: string;
@@ -28,42 +31,39 @@ function displayName(name: string): string {
28
31
  }
29
32
 
30
33
  function parseTeamsYaml(raw: string): Record<string, string[]> {
34
+ const parsed = parseYaml(raw);
35
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
36
+
31
37
  const teams: Record<string, string[]> = {};
32
- let current: string | null = null;
33
- for (const line of raw.split("\n")) {
34
- const teamMatch = line.match(/^(\S[^:]*):$/);
35
- if (teamMatch) {
36
- current = teamMatch[1].trim();
37
- teams[current] = [];
38
- continue;
39
- }
40
- const itemMatch = line.match(/^\s+-\s+(.+)$/);
41
- if (itemMatch && current) teams[current].push(itemMatch[1].trim());
38
+ for (const [name, members] of Object.entries(parsed as Record<string, unknown>)) {
39
+ if (!Array.isArray(members)) continue;
40
+ teams[name] = members.filter((member): member is string => typeof member === "string");
42
41
  }
43
42
  return teams;
44
43
  }
45
44
 
46
45
  function parseAgentFile(filePath: string): AgentDef | null {
47
46
  const raw = readFileSync(filePath, "utf-8");
48
- const match = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
49
- if (!match) return null;
50
-
51
- const frontmatter: Record<string, string> = {};
52
- for (const line of match[1].split("\n")) {
53
- const idx = line.indexOf(":");
54
- if (idx > 0) frontmatter[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
55
- }
56
- if (!frontmatter.name) return null;
47
+ const { frontmatter, body } = parseMarkdownFrontmatter(raw);
48
+ const name = stringField(frontmatter.name);
49
+ if (!name) return null;
57
50
 
58
51
  return {
59
- name: frontmatter.name,
60
- description: frontmatter.description || "",
61
- tools: frontmatter.tools || "read,grep,find,ls",
62
- systemPrompt: match[2].trim(),
52
+ name,
53
+ description: stringField(frontmatter.description),
54
+ tools: arrayField(frontmatter.tools).join(",") || "read,grep,find,ls",
55
+ systemPrompt: body,
63
56
  file: filePath,
64
57
  };
65
58
  }
66
59
 
60
+ function writeSystemPromptFile(agentKey: string, systemPrompt: string): { dir: string; filePath: string } {
61
+ const dir = mkdtempSync(join(tmpdir(), "openpi-team-"));
62
+ const filePath = join(dir, `system-${agentKey.replace(/[^\w.-]+/g, "_")}.md`);
63
+ writeFileSync(filePath, systemPrompt, { encoding: "utf-8", mode: 0o600 });
64
+ return { dir, filePath };
65
+ }
66
+
67
67
  function scanAgentDirs(cwd: string): AgentDef[] {
68
68
  const dirs = [join(cwd, ".pi", "agents"), join(cwd, "agents"), bundledAgentsDir];
69
69
  const agents: AgentDef[] = [];
@@ -161,13 +161,14 @@ export default function (pi: ExtensionAPI) {
161
161
  const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
162
162
  const agentKey = state.def.name.toLowerCase().replace(/\s+/g, "-");
163
163
  const agentSessionFile = join(sessionDir, `${agentKey}.json`);
164
+ const systemPrompt = writeSystemPromptFile(agentKey, state.def.systemPrompt);
164
165
  const args = [
165
166
  "--mode", "json",
166
167
  "-p",
167
168
  "--no-extensions",
168
169
  "--tools", state.def.tools,
169
170
  "--thinking", "off",
170
- "--append-system-prompt", state.def.systemPrompt,
171
+ "--append-system-prompt", systemPrompt.filePath,
171
172
  "--session", agentSessionFile,
172
173
  ];
173
174
  if (model) args.splice(4, 0, "--model", model);
@@ -178,6 +179,7 @@ export default function (pi: ExtensionAPI) {
178
179
  const chunks: string[] = [];
179
180
 
180
181
  return new Promise((resolveDone) => {
182
+ const cleanupPrompt = () => rmSync(systemPrompt.dir, { recursive: true, force: true });
181
183
  const proc = spawn("pi", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } });
182
184
  const timer = setInterval(() => {
183
185
  state.elapsed = Date.now() - start;
@@ -206,6 +208,7 @@ export default function (pi: ExtensionAPI) {
206
208
  proc.stderr!.on("data", () => {});
207
209
  proc.on("close", (code) => {
208
210
  clearInterval(timer);
211
+ cleanupPrompt();
209
212
  state.elapsed = Date.now() - start;
210
213
  state.status = code === 0 ? "done" : "error";
211
214
  if (code === 0) state.sessionFile = agentSessionFile;
@@ -214,6 +217,7 @@ export default function (pi: ExtensionAPI) {
214
217
  });
215
218
  proc.on("error", (error) => {
216
219
  clearInterval(timer);
220
+ cleanupPrompt();
217
221
  state.status = "error";
218
222
  state.lastWork = error.message;
219
223
  updateWidget();
@@ -12,8 +12,8 @@
12
12
  * Usage: pi -e extensions/damage-control-continue.ts
13
13
  */
14
14
 
15
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
- import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
17
17
  import { parse as yamlParse } from "yaml";
18
18
  import * as fs from "fs";
19
19
  import * as path from "path";
@@ -6,9 +6,9 @@
6
6
  * Usage: pi -e extensions/minimal.ts
7
7
  */
8
8
 
9
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
10
  import { applyExtensionDefaults } from "./themeMap.ts";
11
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
11
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
12
12
 
13
13
  export default function (pi: ExtensionAPI) {
14
14
  pi.on("session_start", async (_event, ctx) => {
@@ -31,4 +31,4 @@ export default function (pi: ExtensionAPI) {
31
31
  },
32
32
  }));
33
33
  });
34
- }
34
+ }
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
@@ -7,7 +7,7 @@
7
7
  * Usage: pi -e examples/extensions/pure-focus.ts
8
8
  */
9
9
 
10
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
11
  import { applyExtensionDefaults } from "./themeMap.ts";
12
12
 
13
13
  export default function (pi: ExtensionAPI) {
@@ -8,8 +8,8 @@
8
8
  * Usage: pi -e extensions/purpose-gate.ts
9
9
  */
10
10
 
11
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
- import { Text, truncateToWidth } from "@mariozechner/pi-tui";
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import { Text, truncateToWidth } from "@earendil-works/pi-tui";
13
13
  import { applyExtensionDefaults } from "./themeMap.ts";
14
14
 
15
15
  // synthwave: bgWarm #4a1e6a → rgb(74,30,106)
@@ -17,8 +17,8 @@
17
17
  * Usage: pi -e extensions/theme-cycler.ts -e extensions/minimal.ts
18
18
  */
19
19
 
20
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
21
- import { truncateToWidth } from "@mariozechner/pi-tui";
20
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
21
+ import { truncateToWidth } from "@earendil-works/pi-tui";
22
22
  import { applyExtensionDefaults } from "./themeMap.ts";
23
23
 
24
24
  export default function (pi: ExtensionAPI) {
@@ -11,7 +11,7 @@
11
11
  * synthwave · tokyo-night
12
12
  */
13
13
 
14
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
14
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
15
15
  import { basename } from "path";
16
16
  import { fileURLToPath } from "url";
17
17
 
@@ -7,8 +7,8 @@
7
7
  * Usage: pi -e extensions/tool-counter-widget.ts
8
8
  */
9
9
 
10
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
- import { Box, Text } from "@mariozechner/pi-tui";
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import { Box, Text } from "@earendil-works/pi-tui";
12
12
  import { applyExtensionDefaults } from "./themeMap.ts";
13
13
 
14
14
  const palette = [
@@ -10,9 +10,9 @@
10
10
  * Usage: pi -e extensions/tool-counter.ts
11
11
  */
12
12
 
13
- import type { AssistantMessage } from "@mariozechner/pi-ai";
14
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
15
- import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
13
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
16
16
  import { basename } from "node:path";
17
17
  import { applyExtensionDefaults } from "./themeMap.ts";
18
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matyah00/openpi",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Pi-native commands, skills, agents, and workflows.",
6
6
  "keywords": [
@@ -24,6 +24,7 @@
24
24
  "agents",
25
25
  "extensions",
26
26
  "prompts",
27
+ "scripts",
27
28
  "skills",
28
29
  "themes",
29
30
  "types",
@@ -35,6 +36,12 @@
35
36
  "publishConfig": {
36
37
  "access": "public"
37
38
  },
39
+ "scripts": {
40
+ "check": "npm run validate && npm run typecheck && npm run pack:check",
41
+ "pack:check": "npm pack --dry-run",
42
+ "typecheck": "tsc --noEmit",
43
+ "validate": "node scripts/validate-package.mjs"
44
+ },
38
45
  "dependencies": {
39
46
  "yaml": "^2.8.0"
40
47
  },
@@ -0,0 +1,100 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { basename, join, relative, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parse as parseYaml } from "yaml";
5
+
6
+ const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
7
+ const failures = [];
8
+
9
+ function fail(message) {
10
+ failures.push(message);
11
+ }
12
+
13
+ function read(path) {
14
+ return readFileSync(join(root, path), "utf-8");
15
+ }
16
+
17
+ function walk(dir, predicate = () => true) {
18
+ const absolute = join(root, dir);
19
+ if (!existsSync(absolute)) return [];
20
+ const files = [];
21
+ for (const entry of readdirSync(absolute, { withFileTypes: true })) {
22
+ const full = join(absolute, entry.name);
23
+ if (entry.isDirectory()) files.push(...walk(relative(root, full), predicate));
24
+ else if (predicate(entry.name)) files.push(relative(root, full).replace(/\\/g, "/"));
25
+ }
26
+ return files;
27
+ }
28
+
29
+ function parseMarkdown(file) {
30
+ const raw = read(file);
31
+ const match = raw.match(/^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n([\s\S]*)$/);
32
+ if (!match) return { frontmatter: {}, body: raw };
33
+ const frontmatter = parseYaml(match[1]) ?? {};
34
+ if (!frontmatter || typeof frontmatter !== "object" || Array.isArray(frontmatter)) {
35
+ fail(`${file}: frontmatter must be a mapping`);
36
+ return { frontmatter: {}, body: match[2] };
37
+ }
38
+ return { frontmatter, body: match[2] };
39
+ }
40
+
41
+ for (const file of walk("extensions", (name) => name.endsWith(".ts"))) {
42
+ const content = read(file);
43
+ if (content.includes("@mariozechner/")) fail(`${file}: uses old @mariozechner namespace`);
44
+ if (content.includes("@sinclair/typebox")) fail(`${file}: uses @sinclair/typebox instead of typebox`);
45
+ if (/--append-system-prompt",\s*(state\.def|agentDef)\.systemPrompt/.test(content)) {
46
+ fail(`${file}: passes raw system prompt instead of a temp file`);
47
+ }
48
+ }
49
+
50
+ const promptNames = new Map();
51
+ for (const file of walk("prompts", (name) => name.endsWith(".md"))) {
52
+ const { frontmatter, body } = parseMarkdown(file);
53
+ const name = typeof frontmatter.name === "string" ? frontmatter.name : basename(file, ".md");
54
+ if (!body.trim()) fail(`${file}: prompt body is empty`);
55
+ if (promptNames.has(name)) fail(`${file}: duplicate prompt name "${name}" also used by ${promptNames.get(name)}`);
56
+ promptNames.set(name, file);
57
+ }
58
+
59
+ const agentNames = new Set();
60
+ for (const file of walk("agents", (name) => name.endsWith(".md"))) {
61
+ const { frontmatter, body } = parseMarkdown(file);
62
+ if (typeof frontmatter.name !== "string" || !frontmatter.name.trim()) fail(`${file}: missing agent name`);
63
+ if (typeof frontmatter.description !== "string" || !frontmatter.description.trim()) fail(`${file}: missing agent description`);
64
+ if (!body.trim()) fail(`${file}: agent body is empty`);
65
+ if (typeof frontmatter.name === "string") agentNames.add(frontmatter.name);
66
+ }
67
+
68
+ const teams = parseYaml(read("agents/teams.yaml")) ?? {};
69
+ for (const [team, members] of Object.entries(teams)) {
70
+ if (!Array.isArray(members)) {
71
+ fail(`agents/teams.yaml: ${team} must be a list`);
72
+ continue;
73
+ }
74
+ for (const member of members) {
75
+ if (!agentNames.has(member)) fail(`agents/teams.yaml: ${team} references unknown agent ${member}`);
76
+ }
77
+ }
78
+
79
+ const chains = parseYaml(read("agents/agent-chain.yaml")) ?? {};
80
+ for (const [chainName, chain] of Object.entries(chains)) {
81
+ if (!chain || typeof chain !== "object" || Array.isArray(chain) || !Array.isArray(chain.steps)) {
82
+ fail(`agents/agent-chain.yaml: ${chainName} must have steps`);
83
+ continue;
84
+ }
85
+ for (const [index, step] of chain.steps.entries()) {
86
+ if (!step || typeof step !== "object" || Array.isArray(step)) {
87
+ fail(`agents/agent-chain.yaml: ${chainName} step ${index + 1} must be a mapping`);
88
+ continue;
89
+ }
90
+ if (!agentNames.has(step.agent)) fail(`agents/agent-chain.yaml: ${chainName} references unknown agent ${step.agent}`);
91
+ if (typeof step.prompt !== "string" || !step.prompt.trim()) fail(`agents/agent-chain.yaml: ${chainName} step ${index + 1} missing prompt`);
92
+ }
93
+ }
94
+
95
+ if (failures.length) {
96
+ console.error(failures.map((item) => `- ${item}`).join("\n"));
97
+ process.exit(1);
98
+ }
99
+
100
+ console.log("openpi package validation passed");
@@ -1,11 +1,3 @@
1
- declare module "@mariozechner/pi-coding-agent" {
2
- export type ExtensionContext = any;
3
- export type ExtensionAPI = any;
4
- export const DynamicBorder: any;
5
- export function isToolCallEventType(...args: any[]): boolean;
6
- export function getMarkdownTheme(...args: any[]): any;
7
- }
8
-
9
1
  declare module "@earendil-works/pi-coding-agent" {
10
2
  export type ExtensionContext = any;
11
3
  export type ExtensionAPI = any;
@@ -17,36 +9,6 @@ declare module "@earendil-works/pi-coding-agent" {
17
9
  export function withFileMutationQueue<T = any>(...args: any[]): Promise<T>;
18
10
  }
19
11
 
20
- declare module "@mariozechner/pi-tui" {
21
- export class Text {
22
- constructor(...args: any[]);
23
- setText(...args: any[]): any;
24
- render(...args: any[]): any;
25
- invalidate(...args: any[]): any;
26
- }
27
- export class Box {
28
- constructor(...args: any[]);
29
- }
30
- export class Markdown {
31
- constructor(...args: any[]);
32
- }
33
- export class Container {
34
- constructor(...args: any[]);
35
- addChild(...args: any[]): any;
36
- render(...args: any[]): any;
37
- invalidate(...args: any[]): any;
38
- }
39
- export class Spacer {
40
- constructor(...args: any[]);
41
- }
42
- export type AutocompleteItem = any;
43
- export const Key: any;
44
- export function matchesKey(...args: any[]): any;
45
- export function truncateToWidth(...args: any[]): any;
46
- export function visibleWidth(...args: any[]): any;
47
- export function getMarkdownTheme(...args: any[]): any;
48
- }
49
-
50
12
  declare module "@earendil-works/pi-tui" {
51
13
  export class Text {
52
14
  constructor(...args: any[]);
@@ -77,16 +39,6 @@ declare module "@earendil-works/pi-tui" {
77
39
  export function getMarkdownTheme(...args: any[]): any;
78
40
  }
79
41
 
80
- declare module "@sinclair/typebox" {
81
- export const Type: any;
82
- }
83
-
84
- declare module "@mariozechner/pi-ai" {
85
- export type AssistantMessage = any;
86
- export type Message = any;
87
- export function StringEnum(...args: any[]): any;
88
- }
89
-
90
42
  declare module "@earendil-works/pi-ai" {
91
43
  export type AssistantMessage = any;
92
44
  export type Message = any;