@astrofoundry/pi-astro 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/extensions/astro-agents/agents/code-reviewer.md +0 -2
- package/extensions/astro-agents/agents/google-tech-lead.md +0 -2
- package/extensions/astro-agents/agents/spec-writer.md +0 -2
- package/extensions/astro-agents/agents/tester-api.md +0 -2
- package/extensions/astro-agents/agents/tester-ui.md +0 -2
- package/extensions/astro-agents/agents/ui-architect.md +0 -2
- package/extensions/astro-agents/agents/ui-design-system.md +0 -2
- package/extensions/astro-agents/agents/ui-frontend-developer.md +0 -2
- package/extensions/astro-agents/discovery.test.ts +152 -0
- package/extensions/astro-agents/index.test.ts +208 -0
- package/extensions/astro-agents/index.ts +22 -4
- package/extensions/astro-agents/spawn.test.ts +218 -0
- package/extensions/claude-globals/index.test.ts +77 -0
- package/extensions/gemini-image/credentials.test.ts +130 -0
- package/extensions/gemini-image/credentials.ts +53 -0
- package/extensions/gemini-image/index.test.ts +369 -0
- package/extensions/gemini-image/index.ts +313 -0
- package/extensions/gemini-image/models.test.ts +45 -0
- package/extensions/gemini-image/models.ts +50 -0
- package/extensions/gemini-image/pricing.test.ts +95 -0
- package/extensions/gemini-image/pricing.ts +102 -0
- package/extensions/grimoire/index.test.ts +244 -0
- package/extensions/multi-edit/classic.test.ts +274 -0
- package/extensions/multi-edit/classic.ts +435 -0
- package/extensions/multi-edit/diff.test.ts +65 -0
- package/extensions/multi-edit/diff.ts +143 -0
- package/extensions/multi-edit/index.test.ts +170 -0
- package/extensions/multi-edit/index.ts +267 -0
- package/extensions/multi-edit/patch.test.ts +242 -0
- package/extensions/multi-edit/patch.ts +463 -0
- package/extensions/multi-edit/types.ts +53 -0
- package/extensions/multi-edit/workspace.test.ts +165 -0
- package/extensions/multi-edit/workspace.ts +85 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -53,10 +53,14 @@ pi # launch; confirm [Extensions] lists astro-agents, claude-glob
|
|
|
53
53
|
**Tools** (LLM-callable):
|
|
54
54
|
- `astro_agent` — delegate a task to a named subagent in an isolated pi subprocess
|
|
55
55
|
- `grimoire` — search indexed technical documentation via the grimoire CLI
|
|
56
|
+
- `edit` — replaces pi's built-in with batch multi-file edits + Codex-style patch mode, preflight validation, atomic rollback
|
|
57
|
+
- `gemini_image` — generate or edit images via Google Gemini native models + Imagen 4; cost-estimated confirmation before every call
|
|
56
58
|
|
|
57
59
|
**Extensions:**
|
|
58
60
|
- `astro-agents` — registers `astro_agent` + ships 8 bundled subagents
|
|
59
61
|
- `grimoire` — registers `grimoire` tool
|
|
62
|
+
- `multi-edit` — registers the enhanced `edit` tool
|
|
63
|
+
- `gemini-image` — registers `gemini_image` tool (requires a Gemini API key; prompts and saves on first use)
|
|
60
64
|
- `claude-globals` — auto-injects `~/.claude/CLAUDE.md` into every pi session's system prompt
|
|
61
65
|
|
|
62
66
|
**Bundled subagents** (callable via `astro_agent`):
|
|
@@ -3,9 +3,7 @@ name: code-reviewer
|
|
|
3
3
|
description: |-
|
|
4
4
|
Use this agent to review code for quality, security, correctness, and spec compliance. It is read-only and reports issues with severity and remediation guidance.
|
|
5
5
|
color: red
|
|
6
|
-
memory: user
|
|
7
6
|
tools: read, grep, find, ls, grimoire
|
|
8
|
-
_notWired: memory
|
|
9
7
|
---
|
|
10
8
|
If any instruction below conflicts with the user's global rules (provided separately in the system prompt), flag the conflict explicitly in your response and let the user decide — do not silently override either side.
|
|
11
9
|
|
|
@@ -3,9 +3,7 @@ name: google-tech-lead
|
|
|
3
3
|
description: |-
|
|
4
4
|
Use this agent for designing and implementing anything in the Google/Firebase ecosystem — Cloud Functions 2nd gen (Express), Firestore (data models, security rules, indexes), Firebase Auth (Identity Platform, MFA), Firebase Hosting, Firebase Storage, App Check, and Emulator Suite. This is a tech lead that both architects and writes production code, making decisions about the Firebase stack grounded in official documentation. Examples:\n\n<example>\nContext: Implementing a Cloud Function with Express\nuser: "Create a Cloud Functions 2nd gen HTTP function in europe-west3 that hosts an Express app with versioned API routes (/api/v1/...). It needs middleware for auth token verification, rate limiting, and request validation. Set up proper error handling and CORS."\nassistant: "I'll architect and implement the Cloud Function with Express. Let me use the google-tech-lead agent — it will verify Firebase Functions v2 API, Express integration patterns, and middleware setup through grimoire before writing any code."\n<commentary>\nCloud Functions hosting Express requires specific v2 patterns for region config, CORS handling, and middleware integration that differ from v1.\n</commentary>\n</example>\n\n<example>\nContext: Designing Firestore data model and security rules\nuser: "Design the Firestore schema for API key management. Each partner can have multiple API keys with scopes, expiry dates, and revocation status. Keys must be hashed, never stored in plain text. Write the security rules to enforce that partners can only read their own keys and only admins can create/revoke."\nassistant: "I'll design the data model and security rules. Let me use the google-tech-lead agent to verify Firestore schema patterns, TTL fields, and security rule syntax through grimoire."\n<commentary>\nFirestore data modeling with security rules requires careful schema design that aligns with rule capabilities — you can't write rules for schemas you designed wrong.\n</commentary>\n</example>\n\n<example>\nContext: Setting up Firebase Auth with MFA\nuser: "Implement Firebase Auth with Identity Platform for the dashboard. Users authenticate with email/password, then enroll in TOTP MFA. The enrollment flow generates a QR code, verifies the first code, and enables MFA. All subsequent logins require the TOTP challenge."\nassistant: "I'll implement the full MFA flow. Let me use the google-tech-lead agent to verify the Identity Platform TOTP API, enrollment steps, and challenge verification through grimoire."\n<commentary>\nFirebase Auth MFA with Identity Platform has specific enrollment and challenge flows that must follow the exact API sequence.\n</commentary>\n</example>
|
|
5
5
|
color: orange
|
|
6
|
-
memory: user
|
|
7
6
|
tools: read, bash, grep, find, write, edit, ls, grimoire
|
|
8
|
-
_notWired: memory
|
|
9
7
|
---
|
|
10
8
|
If any instruction below conflicts with the user's global rules (provided separately in the system prompt), flag the conflict explicitly in your response and let the user decide — do not silently override either side.
|
|
11
9
|
|
|
@@ -3,9 +3,7 @@ name: spec-writer
|
|
|
3
3
|
description: |-
|
|
4
4
|
Use this agent to write or update functional and technical specification documents for new features or changes. It takes a feature request or business requirement as input, researches the existing codebase and specs for context, asks clarifying questions, and produces spec documents that downstream agents (google-tech-lead, ui-architect, ui-frontend-developer, testers) can consume directly. Examples:\n\n<example>\nContext: Writing specs for a new feature\nuser: "Write the functional and technical specs for a webhook retry system. When a webhook delivery fails, the system should retry with exponential backoff (1min, 5min, 30min, 2h, 24h). After 5 failures, mark the webhook as failed and notify the partner via email. Track delivery attempts in Firestore."\nassistant: "I'll write both specs for the webhook retry system. Let me use the spec-writer agent — it will research the existing webhook implementation, check how delivery tracking currently works, ask any clarifying questions, then write specs that the google-tech-lead and tester agents can implement from."\n<commentary>\nNew features need both functional specs (what it does, business rules, user flows) and technical specs (data model, API contract, error handling) before implementation begins.\n</commentary>\n</example>\n\n<example>\nContext: Updating specs for a changed requirement\nuser: "The partner onboarding flow needs to change — instead of admin-created accounts, partners should self-register with email verification, then an admin approves them. Update the functional and technical specs."\nassistant: "I'll update both specs for the new onboarding flow. Let me use the spec-writer agent to read the current specs, identify all sections affected by this change, and write the updates while ensuring consistency with the rest of the system."\n<commentary>\nSpec updates must trace through all affected sections — a change in onboarding flow impacts auth, API routes, Firestore schema, email triggers, and admin workflows.\n</commentary>\n</example>\n\n<example>\nContext: Specifying an API contract\nuser: "We need to add a bulk product import endpoint. Partners upload a CSV, the system validates it, creates products in Odoo, and returns a batch result. Spec out the API contract, validation rules, error handling, and the batch tracking model."\nassistant: "I'll spec the bulk import API. Let me use the spec-writer agent to research the existing product and batch models, check how other bulk operations work in the system, and write a complete API contract with request/response shapes, validation, and error scenarios."\n<commentary>\nAPI contract specs must define every request/response shape, status code, error format, and edge case so the implementing agent has zero ambiguity.\n</commentary>\n</example>
|
|
5
5
|
color: purple
|
|
6
|
-
memory: user
|
|
7
6
|
tools: read, bash, grep, find, write, edit, ls, grimoire
|
|
8
|
-
_notWired: memory
|
|
9
7
|
---
|
|
10
8
|
If any instruction below conflicts with the user's global rules (provided separately in the system prompt), flag the conflict explicitly in your response and let the user decide — do not silently override either side.
|
|
11
9
|
|
|
@@ -5,9 +5,7 @@ description: |-
|
|
|
5
5
|
color: yellow
|
|
6
6
|
skills:
|
|
7
7
|
- postman-cli
|
|
8
|
-
memory: user
|
|
9
8
|
tools: read, bash, grep, find, write, edit, ls, grimoire
|
|
10
|
-
_notWired: memory
|
|
11
9
|
---
|
|
12
10
|
If any instruction below conflicts with the user's global rules (provided separately in the system prompt), flag the conflict explicitly in your response and let the user decide — do not silently override either side.
|
|
13
11
|
|
|
@@ -5,9 +5,7 @@ description: |-
|
|
|
5
5
|
color: yellow
|
|
6
6
|
skills:
|
|
7
7
|
- playwright-cli
|
|
8
|
-
memory: user
|
|
9
8
|
tools: read, bash, grep, find, write, edit, ls, grimoire
|
|
10
|
-
_notWired: memory
|
|
11
9
|
---
|
|
12
10
|
If any instruction below conflicts with the user's global rules (provided separately in the system prompt), flag the conflict explicitly in your response and let the user decide — do not silently override either side.
|
|
13
11
|
|
|
@@ -3,9 +3,7 @@ name: ui-architect
|
|
|
3
3
|
description: |-
|
|
4
4
|
Use this agent to create component specifications, screen-by-screen UI specs, and layout patterns from an existing design system. This agent takes the design tokens produced by ui-design-system and defines how every UI element looks, behaves, and responds — button variants, form states, data tables, navigation patterns, status badges, modals, toasts, empty/loading/error states, and full page layouts. It outputs detailed spec documents, not code. Examples:\n\n<example>\nContext: Defining component specifications for a new project\nuser: "Using the design system in docs/ui/ui-specs/ui-00-design-system.md, create the component spec for all common UI elements: buttons, form inputs, selects, checkboxes, toggles, data tables, filter bars, status badges, modals, toasts, empty states, loading states, and error banners."\nassistant: "I'll create the full component specification. Let me use the ui-architect agent — it will read the design tokens, reference shadcn/ui component APIs through grimoire, and define every variant, state, and responsive behavior using the token system."\n<commentary>\nComponent specs require mapping abstract design tokens to concrete UI elements with precise dimensions, colors, states, and responsive behavior.\n</commentary>\n</example>\n\n<example>\nContext: Writing a screen spec for a specific page\nuser: "Write the screen spec for the Orders List page. It needs a filter bar (search, status pills, date range), a data table with sortable columns (Order ID, Partner, Status, Total, Date), pagination, and bulk actions. Reference the component specs and design system tokens."\nassistant: "I'll write the Orders List screen spec. Let me use the ui-architect agent to define the page layout, data requirements, component composition, interactions, and responsive behavior — all referencing the established tokens and component specs."\n<commentary>\nScreen specs compose components into full pages with specific data bindings, interaction flows, and responsive breakpoint behavior.\n</commentary>\n</example>\n\n<example>\nContext: Defining layout shells\nuser: "Define the layout structure for the application: auth shell (login, password reset), partner dashboard shell (sidebar + header + content area), and admin dashboard shell. Include responsive behavior for all three breakpoints."\nassistant: "I'll define all three layout shells. Let me use the ui-architect agent to specify the structure, dimensions, responsive collapse behavior, and how content areas adapt across mobile, tablet, and desktop."\n<commentary>\nLayout shells are the structural foundation that screen specs build on — they define navigation, header, content areas, and responsive behavior.\n</commentary>\n</example>
|
|
5
5
|
color: cyan
|
|
6
|
-
memory: user
|
|
7
6
|
tools: read, bash, grep, find, write, edit, ls, grimoire
|
|
8
|
-
_notWired: memory
|
|
9
7
|
---
|
|
10
8
|
If any instruction below conflicts with the user's global rules (provided separately in the system prompt), flag the conflict explicitly in your response and let the user decide — do not silently override either side.
|
|
11
9
|
|
|
@@ -3,9 +3,7 @@ name: ui-design-system
|
|
|
3
3
|
description: |-
|
|
4
4
|
Use this agent to create or modify the foundational design system for a project — design tokens (colors, typography, spacing, border radius, shadows, motion, breakpoints) and their CSS implementation. This agent produces concrete Tailwind v4 + shadcn/ui v2 token files, not component specs or code. It asks clarifying questions before starting, proposes aesthetic direction, validates WCAG contrast ratios, and outputs both CSS config and a design system reference document. Examples:\n\n<example>\nContext: Creating a design system from scratch\nuser: "Create the design system for a maritime logistics dashboard. Brand colors are deep navy and amber gold. Target audience is warehouse managers and logistics operators. Needs dark mode."\nassistant: "I'll create the foundational design system. Let me use the ui-design-system agent — it will propose an aesthetic direction, define all tokens in oklch for shadcn/ui v2, configure the Tailwind v4 @theme block, and validate WCAG contrast ratios before finalizing."\n<commentary>\nA new design system requires establishing the complete visual language: color palette with semantic mapping, typography scale, spacing system, shadows, motion tokens, and breakpoints — all in the correct format for Tailwind v4 + shadcn/ui v2.\n</commentary>\n</example>\n\n<example>\nContext: Modifying an existing design system\nuser: "Our current design system uses hex colors and HSL for shadcn tokens. We need to migrate to oklch for shadcn/ui v2 and add a complete dark mode palette. Current tokens are in docs/ui/ui-specs/ui-00-design-system.md."\nassistant: "I'll migrate the design tokens to oklch and add dark mode. Let me use the ui-design-system agent to convert all colors, verify contrast ratios in both modes, and update the CSS variables and @theme block."\n<commentary>\nMigrating token formats requires systematic conversion, contrast re-validation, and updating the CSS output to match current Tailwind v4 + shadcn/ui v2 conventions.\n</commentary>\n</example>\n\n<example>\nContext: Extending the design system with new tokens\nuser: "We need to add a status badge color system to our design tokens. Statuses: draft, active, processing, shipped, delivered, failed, cancelled. Each needs background, text, and border colors that work in both light and dark mode."\nassistant: "I'll design the status color system. Let me use the ui-design-system agent to create accessible color pairings for each status, define the tokens in oklch, and add them to the CSS variables with dark mode variants."\n<commentary>\nAdding semantic color groups requires careful selection for distinctiveness, accessibility, and consistency with the existing palette.\n</commentary>\n</example>
|
|
5
5
|
color: green
|
|
6
|
-
memory: user
|
|
7
6
|
tools: read, bash, grep, find, write, edit, ls, grimoire
|
|
8
|
-
_notWired: memory
|
|
9
7
|
---
|
|
10
8
|
If any instruction below conflicts with the user's global rules (provided separately in the system prompt), flag the conflict explicitly in your response and let the user decide — do not silently override either side.
|
|
11
9
|
|
|
@@ -3,9 +3,7 @@ name: ui-frontend-developer
|
|
|
3
3
|
description: |-
|
|
4
4
|
Use this agent for implementing specific React components, pages, or features with TypeScript, Next.js App Router, Tailwind CSS, and shadcn/ui. This agent receives laser-focused prompts with exact specifications and strictly verifies all SDK/library usage through grimoire before writing any code. Examples:\n\n<example>\nContext: Implementing a specific component from a screen spec\nuser: "Implement the OrderHistoryTable component in src/components/orders/OrderHistoryTable.tsx. It receives OrderRow[] as props, uses shadcn/ui DataTable with sortable columns (Order ID, Date, Status, Total), a status badge using the design system color tokens, and pagination. Follow the screen spec in docs/ui/ui-specs/orders-list.md."\nassistant: "I'll implement the OrderHistoryTable component. Let me use the ui-frontend-developer agent — it will verify shadcn/ui DataTable API and Tailwind token usage through grimoire before writing the component."\n<commentary>\nSpecific component implementation with exact props, library usage, and design system constraints requires the frontend specialist.\n</commentary>\n</example>\n\n<example>\nContext: Fixing a typed component issue\nuser: "The ProductCard component at src/components/catalog/ProductCard.tsx has a type error — the price prop is typed as string but the API returns number. Fix the type, update the Zod schema in src/schemas/product.ts, and ensure the formatter in formatCurrency handles both."\nassistant: "I'll fix the type mismatch across the component, schema, and utility. Let me use the ui-frontend-developer agent to trace the type through all layers."\n<commentary>\nTypeScript type issues that span multiple files need systematic tracing through components, schemas, and utilities.\n</commentary>\n</example>\n\n<example>\nContext: Building a Next.js App Router page\nuser: "Create the /settings/profile page using Next.js App Router. Server Component that fetches user profile via Server Action, with a client form component using React Hook Form + Zod for validation. Use the design system spacing and typography tokens. Spec: docs/ui/ui-specs/settings-profile.md."\nassistant: "I'll build the settings profile page with the Server Component / Client Component split. Let me use the ui-frontend-developer agent to verify Next.js App Router patterns and React Hook Form integration through grimoire."\n<commentary>\nNext.js App Router pages with mixed Server/Client Components and form handling require precise knowledge of current API patterns.\n</commentary>\n</example>
|
|
5
5
|
color: blue
|
|
6
|
-
memory: user
|
|
7
6
|
tools: read, bash, grep, find, write, edit, ls, grimoire
|
|
8
|
-
_notWired: memory
|
|
9
7
|
---
|
|
10
8
|
If any instruction below conflicts with the user's global rules (provided separately in the system prompt), flag the conflict explicitly in your response and let the user decide — do not silently override either side.
|
|
11
9
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { discoverAgents } from "./discovery.ts";
|
|
6
|
+
|
|
7
|
+
function writeAgent(dir: string, name: string, frontmatter: Record<string, string>, body = ""): void {
|
|
8
|
+
const fm = Object.entries(frontmatter)
|
|
9
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
10
|
+
.join("\n");
|
|
11
|
+
writeFileSync(join(dir, `${name}.md`), `---\n${fm}\n---\n${body}\n`, "utf-8");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("discoverAgents", () => {
|
|
15
|
+
let bundleDir: string;
|
|
16
|
+
let cwd: string;
|
|
17
|
+
const originalAgentDir = process.env.PI_AGENT_DIR;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
bundleDir = mkdtempSync(join(tmpdir(), "bundle-"));
|
|
21
|
+
cwd = mkdtempSync(join(tmpdir(), "cwd-"));
|
|
22
|
+
const pseudoAgentDir = mkdtempSync(join(tmpdir(), "agentdir-"));
|
|
23
|
+
mkdirSync(join(pseudoAgentDir, "agents"), { recursive: true });
|
|
24
|
+
process.env.PI_AGENT_DIR = pseudoAgentDir;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
rmSync(bundleDir, { recursive: true, force: true });
|
|
29
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
30
|
+
process.env.PI_AGENT_DIR = originalAgentDir;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns empty array when no agent dirs exist", () => {
|
|
34
|
+
rmSync(bundleDir, { recursive: true, force: true });
|
|
35
|
+
const agents = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
36
|
+
expect(agents).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("loads bundled agents with name, description, body", () => {
|
|
40
|
+
writeAgent(bundleDir, "a", { name: "a", description: "does a" }, "system prompt a");
|
|
41
|
+
const agents = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
42
|
+
expect(agents).toHaveLength(1);
|
|
43
|
+
expect(agents[0].name).toBe("a");
|
|
44
|
+
expect(agents[0].description).toBe("does a");
|
|
45
|
+
expect(agents[0].systemPrompt.trim()).toBe("system prompt a");
|
|
46
|
+
expect(agents[0].source).toBe("bundle");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("parses color field", () => {
|
|
50
|
+
writeAgent(bundleDir, "a", { name: "a", description: "x", color: "red" });
|
|
51
|
+
const [agent] = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
52
|
+
expect(agent.color).toBe("red");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("parses tools as comma-separated list", () => {
|
|
56
|
+
writeAgent(bundleDir, "a", { name: "a", description: "x", tools: "read, bash, grep" });
|
|
57
|
+
const [agent] = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
58
|
+
expect(agent.tools).toEqual(["read", "bash", "grep"]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("omits tools when field is absent", () => {
|
|
62
|
+
writeAgent(bundleDir, "a", { name: "a", description: "x" });
|
|
63
|
+
const [agent] = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
64
|
+
expect(agent.tools).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("parses skills as YAML list", () => {
|
|
68
|
+
writeFileSync(
|
|
69
|
+
join(bundleDir, "a.md"),
|
|
70
|
+
`---\nname: a\ndescription: x\nskills:\n - playwright-cli\n - grimoire\n---\n`,
|
|
71
|
+
"utf-8",
|
|
72
|
+
);
|
|
73
|
+
const [agent] = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
74
|
+
expect(agent.skills).toEqual(["playwright-cli", "grimoire"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("parses skills as comma-separated string when inline", () => {
|
|
78
|
+
writeAgent(bundleDir, "a", { name: "a", description: "x", skills: "alpha, beta" });
|
|
79
|
+
const [agent] = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
80
|
+
expect(agent.skills).toEqual(["alpha", "beta"]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("omits skills when empty", () => {
|
|
84
|
+
writeAgent(bundleDir, "a", { name: "a", description: "x", skills: "" });
|
|
85
|
+
const [agent] = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
86
|
+
expect(agent.skills).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("skips files missing name or description", () => {
|
|
90
|
+
writeAgent(bundleDir, "a", { name: "a" }); // missing description
|
|
91
|
+
writeAgent(bundleDir, "b", { description: "x" }); // missing name
|
|
92
|
+
writeAgent(bundleDir, "c", { name: "c", description: "x" });
|
|
93
|
+
const agents = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
94
|
+
expect(agents.map((a) => a.name)).toEqual(["c"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("skips non-.md files", () => {
|
|
98
|
+
writeFileSync(join(bundleDir, "notes.txt"), "not an agent", "utf-8");
|
|
99
|
+
writeAgent(bundleDir, "a", { name: "a", description: "x" });
|
|
100
|
+
const agents = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
101
|
+
expect(agents).toHaveLength(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("project agents override user + bundle when scope is 'both'", () => {
|
|
105
|
+
writeAgent(bundleDir, "shared", { name: "shared", description: "from bundle" });
|
|
106
|
+
|
|
107
|
+
const projectAgentsDir = join(cwd, ".pi", "agents");
|
|
108
|
+
mkdirSync(projectAgentsDir, { recursive: true });
|
|
109
|
+
writeAgent(projectAgentsDir, "shared", { name: "shared", description: "from project" });
|
|
110
|
+
|
|
111
|
+
const agents = discoverAgents({ cwd, bundleDir, scope: "both" });
|
|
112
|
+
const shared = agents.find((a) => a.name === "shared");
|
|
113
|
+
expect(shared?.description).toBe("from project");
|
|
114
|
+
expect(shared?.source).toBe("project");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("scope='user' skips project agents", () => {
|
|
118
|
+
writeAgent(bundleDir, "b", { name: "b", description: "bundle" });
|
|
119
|
+
const projectAgentsDir = join(cwd, ".pi", "agents");
|
|
120
|
+
mkdirSync(projectAgentsDir, { recursive: true });
|
|
121
|
+
writeAgent(projectAgentsDir, "p", { name: "p", description: "project" });
|
|
122
|
+
const agents = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
123
|
+
expect(agents.map((a) => a.name).sort()).toEqual(["b"]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("scope='project' skips user-level agents", () => {
|
|
127
|
+
writeAgent(bundleDir, "b", { name: "b", description: "bundle" });
|
|
128
|
+
const projectAgentsDir = join(cwd, ".pi", "agents");
|
|
129
|
+
mkdirSync(projectAgentsDir, { recursive: true });
|
|
130
|
+
writeAgent(projectAgentsDir, "p", { name: "p", description: "project" });
|
|
131
|
+
const agents = discoverAgents({ cwd, bundleDir, scope: "project" });
|
|
132
|
+
expect(agents.map((a) => a.name).sort()).toEqual(["b", "p"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("walks parent directories to find nearest .pi/agents", () => {
|
|
136
|
+
const deep = join(cwd, "a", "b", "c");
|
|
137
|
+
mkdirSync(deep, { recursive: true });
|
|
138
|
+
const projectAgentsDir = join(cwd, ".pi", "agents");
|
|
139
|
+
mkdirSync(projectAgentsDir, { recursive: true });
|
|
140
|
+
writeAgent(projectAgentsDir, "p", { name: "p", description: "project" });
|
|
141
|
+
|
|
142
|
+
const agents = discoverAgents({ cwd: deep, bundleDir, scope: "project" });
|
|
143
|
+
expect(agents.map((a) => a.name)).toContain("p");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("ignores symlinks to non-existent targets", () => {
|
|
147
|
+
// symlink behavior — create a file, our loader should accept symlinks
|
|
148
|
+
writeAgent(bundleDir, "a", { name: "a", description: "x" });
|
|
149
|
+
const agents = discoverAgents({ cwd, bundleDir, scope: "user" });
|
|
150
|
+
expect(agents).toHaveLength(1);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { AgentConfig } from "./discovery.ts";
|
|
3
|
+
|
|
4
|
+
vi.mock("./discovery.ts", () => ({
|
|
5
|
+
discoverAgents: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("./spawn.ts", () => ({
|
|
9
|
+
runAgent: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("node:fs", async (importActual) => {
|
|
13
|
+
const actual = await importActual<typeof import("node:fs")>();
|
|
14
|
+
return { ...actual, existsSync: vi.fn(actual.existsSync) };
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
type ToolDef = {
|
|
18
|
+
name: string;
|
|
19
|
+
execute: (
|
|
20
|
+
id: string,
|
|
21
|
+
input: { agent: string; task: string; scope?: "user" | "project" | "both" },
|
|
22
|
+
signal: AbortSignal | undefined,
|
|
23
|
+
onUpdate: unknown,
|
|
24
|
+
ctx: {
|
|
25
|
+
cwd: string;
|
|
26
|
+
hasUI: boolean;
|
|
27
|
+
ui: {
|
|
28
|
+
notify: ReturnType<typeof vi.fn>;
|
|
29
|
+
confirm: ReturnType<typeof vi.fn>;
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
) => Promise<{ content: Array<{ text: string }>; details: { agent: string; source: string } }>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function makePi(): { tools: ToolDef[]; registerTool: (t: ToolDef) => void } {
|
|
36
|
+
const tools: ToolDef[] = [];
|
|
37
|
+
return { tools, registerTool: (t) => tools.push(t) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeCtx(): {
|
|
41
|
+
cwd: string;
|
|
42
|
+
hasUI: boolean;
|
|
43
|
+
ui: { notify: ReturnType<typeof vi.fn>; confirm: ReturnType<typeof vi.fn> };
|
|
44
|
+
} {
|
|
45
|
+
return {
|
|
46
|
+
cwd: "/tmp/test-cwd",
|
|
47
|
+
hasUI: true,
|
|
48
|
+
ui: {
|
|
49
|
+
notify: vi.fn(),
|
|
50
|
+
confirm: vi.fn().mockResolvedValue(true),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sampleAgent: AgentConfig = {
|
|
56
|
+
name: "scout",
|
|
57
|
+
description: "quick recon",
|
|
58
|
+
color: "red",
|
|
59
|
+
systemPrompt: "You are scout.",
|
|
60
|
+
source: "bundle",
|
|
61
|
+
filePath: "/pkg/extensions/astro-agents/agents/scout.md",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe("astro-agents extension (astro_agent tool)", () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
vi.resetModules();
|
|
67
|
+
vi.clearAllMocks();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
vi.clearAllMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("registers the astro_agent tool", async () => {
|
|
75
|
+
const { discoverAgents } = await import("./discovery.ts");
|
|
76
|
+
vi.mocked(discoverAgents).mockReturnValue([sampleAgent]);
|
|
77
|
+
const mod = await import("./index.ts");
|
|
78
|
+
const pi = makePi();
|
|
79
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
80
|
+
expect(pi.tools[0].name).toBe("astro_agent");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("throws when agent name is not found; lists available", async () => {
|
|
84
|
+
const { discoverAgents } = await import("./discovery.ts");
|
|
85
|
+
vi.mocked(discoverAgents).mockReturnValue([sampleAgent]);
|
|
86
|
+
const mod = await import("./index.ts");
|
|
87
|
+
const pi = makePi();
|
|
88
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
89
|
+
const ctx = makeCtx();
|
|
90
|
+
await expect(
|
|
91
|
+
pi.tools[0].execute("t", { agent: "ghost", task: "x" }, undefined, undefined, ctx),
|
|
92
|
+
).rejects.toThrow(/not found.+scout/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("happy path: spawns subagent and returns its text", async () => {
|
|
96
|
+
const { discoverAgents } = await import("./discovery.ts");
|
|
97
|
+
const { runAgent } = await import("./spawn.ts");
|
|
98
|
+
vi.mocked(discoverAgents).mockReturnValue([sampleAgent]);
|
|
99
|
+
vi.mocked(runAgent).mockResolvedValue({ text: "done", exitCode: 0, stderr: "" });
|
|
100
|
+
const mod = await import("./index.ts");
|
|
101
|
+
const pi = makePi();
|
|
102
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
103
|
+
const ctx = makeCtx();
|
|
104
|
+
const res = await pi.tools[0].execute("t", { agent: "scout", task: "recon" }, undefined, undefined, ctx);
|
|
105
|
+
expect(res.content[0].text).toBe("done");
|
|
106
|
+
expect(ctx.ui.notify).toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("throws on non-zero exit", async () => {
|
|
110
|
+
const { discoverAgents } = await import("./discovery.ts");
|
|
111
|
+
const { runAgent } = await import("./spawn.ts");
|
|
112
|
+
vi.mocked(discoverAgents).mockReturnValue([sampleAgent]);
|
|
113
|
+
vi.mocked(runAgent).mockResolvedValue({ text: "", exitCode: 1, stderr: "boom" });
|
|
114
|
+
const mod = await import("./index.ts");
|
|
115
|
+
const pi = makePi();
|
|
116
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
117
|
+
await expect(
|
|
118
|
+
pi.tools[0].execute("t", { agent: "scout", task: "x" }, undefined, undefined, makeCtx()),
|
|
119
|
+
).rejects.toThrow(/exited with code 1/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns placeholder when agent produced no output", async () => {
|
|
123
|
+
const { discoverAgents } = await import("./discovery.ts");
|
|
124
|
+
const { runAgent } = await import("./spawn.ts");
|
|
125
|
+
vi.mocked(discoverAgents).mockReturnValue([sampleAgent]);
|
|
126
|
+
vi.mocked(runAgent).mockResolvedValue({ text: "", exitCode: 0, stderr: "" });
|
|
127
|
+
const mod = await import("./index.ts");
|
|
128
|
+
const pi = makePi();
|
|
129
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
130
|
+
const res = await pi.tools[0].execute("t", { agent: "scout", task: "x" }, undefined, undefined, makeCtx());
|
|
131
|
+
expect(res.content[0].text).toMatch(/no output/);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("prompts confirmation for project-scoped agent and respects 'no'", async () => {
|
|
135
|
+
const projectAgent = { ...sampleAgent, source: "project" as const };
|
|
136
|
+
const { discoverAgents } = await import("./discovery.ts");
|
|
137
|
+
vi.mocked(discoverAgents).mockReturnValue([projectAgent]);
|
|
138
|
+
const mod = await import("./index.ts");
|
|
139
|
+
const pi = makePi();
|
|
140
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
141
|
+
const ctx = makeCtx();
|
|
142
|
+
ctx.ui.confirm.mockResolvedValue(false);
|
|
143
|
+
await expect(
|
|
144
|
+
pi.tools[0].execute("t", { agent: "scout", task: "x", scope: "project" }, undefined, undefined, ctx),
|
|
145
|
+
).rejects.toThrow(/cancelled/);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("warns when declared skill isn't in the package", async () => {
|
|
149
|
+
const { discoverAgents } = await import("./discovery.ts");
|
|
150
|
+
const { runAgent } = await import("./spawn.ts");
|
|
151
|
+
const agentWithSkill: AgentConfig = { ...sampleAgent, skills: ["ghost-skill"] };
|
|
152
|
+
vi.mocked(discoverAgents).mockReturnValue([agentWithSkill]);
|
|
153
|
+
vi.mocked(runAgent).mockResolvedValue({ text: "ok", exitCode: 0, stderr: "" });
|
|
154
|
+
const fs = await import("node:fs");
|
|
155
|
+
vi.mocked(fs.existsSync).mockImplementation((p) => !String(p).endsWith("ghost-skill"));
|
|
156
|
+
const mod = await import("./index.ts");
|
|
157
|
+
const pi = makePi();
|
|
158
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
159
|
+
const ctx = makeCtx();
|
|
160
|
+
await pi.tools[0].execute("t", { agent: "scout", task: "x" }, undefined, undefined, ctx);
|
|
161
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(expect.stringMatching(/skill .*ghost-skill.*not present/), "warning");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("resolves declared skill paths that exist in the package skills dir", async () => {
|
|
165
|
+
const { discoverAgents } = await import("./discovery.ts");
|
|
166
|
+
const { runAgent } = await import("./spawn.ts");
|
|
167
|
+
const agentWithSkill: AgentConfig = { ...sampleAgent, skills: ["playwright-cli"] };
|
|
168
|
+
vi.mocked(discoverAgents).mockReturnValue([agentWithSkill]);
|
|
169
|
+
vi.mocked(runAgent).mockResolvedValue({ text: "ok", exitCode: 0, stderr: "" });
|
|
170
|
+
const fs = await import("node:fs");
|
|
171
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
172
|
+
const mod = await import("./index.ts");
|
|
173
|
+
const pi = makePi();
|
|
174
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
175
|
+
await pi.tools[0].execute("t", { agent: "scout", task: "x" }, undefined, undefined, makeCtx());
|
|
176
|
+
const runAgentCalls = vi.mocked(runAgent).mock.calls;
|
|
177
|
+
expect(runAgentCalls[0][0].skillPaths).toHaveLength(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("agent without color renders plain name in notify", async () => {
|
|
181
|
+
const noColor: AgentConfig = { ...sampleAgent, color: undefined };
|
|
182
|
+
const { discoverAgents } = await import("./discovery.ts");
|
|
183
|
+
const { runAgent } = await import("./spawn.ts");
|
|
184
|
+
vi.mocked(discoverAgents).mockReturnValue([noColor]);
|
|
185
|
+
vi.mocked(runAgent).mockResolvedValue({ text: "ok", exitCode: 0, stderr: "" });
|
|
186
|
+
const mod = await import("./index.ts");
|
|
187
|
+
const pi = makePi();
|
|
188
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
189
|
+
const ctx = makeCtx();
|
|
190
|
+
await pi.tools[0].execute("t", { agent: "scout", task: "x" }, undefined, undefined, ctx);
|
|
191
|
+
const notifyMsg = ctx.ui.notify.mock.calls[0][0] as string;
|
|
192
|
+
expect(notifyMsg).toContain("scout");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("agent with unknown color falls back to plain name", async () => {
|
|
196
|
+
const weirdColor: AgentConfig = { ...sampleAgent, color: "fuchsia-tangerine" };
|
|
197
|
+
const { discoverAgents } = await import("./discovery.ts");
|
|
198
|
+
const { runAgent } = await import("./spawn.ts");
|
|
199
|
+
vi.mocked(discoverAgents).mockReturnValue([weirdColor]);
|
|
200
|
+
vi.mocked(runAgent).mockResolvedValue({ text: "ok", exitCode: 0, stderr: "" });
|
|
201
|
+
const mod = await import("./index.ts");
|
|
202
|
+
const pi = makePi();
|
|
203
|
+
mod.default(pi as unknown as Parameters<typeof mod.default>[0]);
|
|
204
|
+
const ctx = makeCtx();
|
|
205
|
+
await pi.tools[0].execute("t", { agent: "scout", task: "x" }, undefined, undefined, ctx);
|
|
206
|
+
expect(ctx.ui.notify).toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -43,9 +43,27 @@ const params = Type.Object({
|
|
|
43
43
|
),
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const ANSI_RESET = "\x1b[0m";
|
|
47
|
+
const ANSI_BOLD = "\x1b[1m";
|
|
48
|
+
|
|
49
|
+
const COLOR_BADGE: Record<string, string> = {
|
|
50
|
+
red: "\x1b[41m\x1b[97m",
|
|
51
|
+
orange: "\x1b[48;5;208m\x1b[30m",
|
|
52
|
+
yellow: "\x1b[43m\x1b[30m",
|
|
53
|
+
green: "\x1b[42m\x1b[97m",
|
|
54
|
+
blue: "\x1b[44m\x1b[97m",
|
|
55
|
+
purple: "\x1b[45m\x1b[97m",
|
|
56
|
+
magenta: "\x1b[45m\x1b[97m",
|
|
57
|
+
cyan: "\x1b[46m\x1b[30m",
|
|
58
|
+
white: "\x1b[47m\x1b[30m",
|
|
59
|
+
gray: "\x1b[100m\x1b[97m",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function agentBadge(agentName: string, color: string | undefined): string {
|
|
63
|
+
const key = color?.toLowerCase();
|
|
64
|
+
const bg = key ? COLOR_BADGE[key] : undefined;
|
|
65
|
+
if (!bg) return agentName;
|
|
66
|
+
return `${bg}${ANSI_BOLD} ${agentName} ${ANSI_RESET}`;
|
|
49
67
|
}
|
|
50
68
|
|
|
51
69
|
export default function astroAgentsExtension(pi: ExtensionAPI): void {
|
|
@@ -80,7 +98,7 @@ export default function astroAgentsExtension(pi: ExtensionAPI): void {
|
|
|
80
98
|
}
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
ctx.ui.notify(`Running ${
|
|
101
|
+
ctx.ui.notify(`Running ${agentBadge(agent.name, agent.color)}…`, "info");
|
|
84
102
|
|
|
85
103
|
const skillPaths = agent.skills ? resolvePackageSkills(agent.skills, ctx) : [];
|
|
86
104
|
|