init pi folder
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
auth.json
|
||||||
|
sessions
|
||||||
|
.venv
|
||||||
|
.pyo
|
||||||
|
.pyc
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: planner
|
||||||
|
description: Creates implementation plans from context and requirements
|
||||||
|
tools: read, grep, find, ls
|
||||||
|
model: openai-codex/gpt-5.3-codex
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a planning specialist. You receive context (from a scout) and requirements, then produce a clear implementation plan.
|
||||||
|
|
||||||
|
You must NOT make any changes. Only read, analyze, and plan.
|
||||||
|
|
||||||
|
Input format you'll receive:
|
||||||
|
- Context/findings from a scout agent
|
||||||
|
- Original query or requirements
|
||||||
|
|
||||||
|
Output format:
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
One sentence summary of what needs to be done.
|
||||||
|
|
||||||
|
## Plan
|
||||||
|
Numbered steps, each small and actionable:
|
||||||
|
1. Step one - specific file/function to modify
|
||||||
|
2. Step two - what to add/change
|
||||||
|
3. ...
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
- `path/to/file.ts` - what changes
|
||||||
|
- `path/to/other.ts` - what changes
|
||||||
|
|
||||||
|
## New Files (if any)
|
||||||
|
- `path/to/new.ts` - purpose
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
Anything to watch out for.
|
||||||
|
|
||||||
|
Keep the plan concrete. The worker agent will execute it verbatim.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: reviewer
|
||||||
|
description: Code review specialist for quality and security analysis
|
||||||
|
tools: read, grep, find, ls, bash
|
||||||
|
model: openai-codex/gpt-5.3-codex
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a senior code reviewer. Analyze code for quality, security, and maintainability.
|
||||||
|
|
||||||
|
Bash is for read-only commands only: `git diff`, `git log`, `git show`. Do NOT modify files or run builds.
|
||||||
|
Assume tool permissions are not perfectly enforceable; keep all bash usage strictly read-only.
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
1. Run `git diff` to see recent changes (if applicable)
|
||||||
|
2. Read the modified files
|
||||||
|
3. Check for bugs, security issues, code smells
|
||||||
|
|
||||||
|
Output format:
|
||||||
|
|
||||||
|
## Files Reviewed
|
||||||
|
- `path/to/file.ts` (lines X-Y)
|
||||||
|
|
||||||
|
## Critical (must fix)
|
||||||
|
- `file.ts:42` - Issue description
|
||||||
|
|
||||||
|
## Warnings (should fix)
|
||||||
|
- `file.ts:100` - Issue description
|
||||||
|
|
||||||
|
## Suggestions (consider)
|
||||||
|
- `file.ts:150` - Improvement idea
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
Overall assessment in 2-3 sentences.
|
||||||
|
|
||||||
|
Be specific with file paths and line numbers.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: scout
|
||||||
|
description: Fast codebase recon that returns compressed context for handoff to other agents
|
||||||
|
tools: read, grep, find, ls, bash
|
||||||
|
model: openai-codex/gpt-5.3-codex
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
|
||||||
|
|
||||||
|
Your output will be passed to an agent who has NOT seen the files you explored.
|
||||||
|
|
||||||
|
Thoroughness (infer from task, default medium):
|
||||||
|
- Quick: Targeted lookups, key files only
|
||||||
|
- Medium: Follow imports, read critical sections
|
||||||
|
- Thorough: Trace all dependencies, check tests/types
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
1. grep/find to locate relevant code
|
||||||
|
2. Read key sections (not entire files)
|
||||||
|
3. Identify types, interfaces, key functions
|
||||||
|
4. Note dependencies between files
|
||||||
|
|
||||||
|
Output format:
|
||||||
|
|
||||||
|
## Files Retrieved
|
||||||
|
List with exact line ranges:
|
||||||
|
1. `path/to/file.ts` (lines 10-50) - Description of what's here
|
||||||
|
2. `path/to/other.ts` (lines 100-150) - Description
|
||||||
|
3. ...
|
||||||
|
|
||||||
|
## Key Code
|
||||||
|
Critical types, interfaces, or functions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Example {
|
||||||
|
// actual code from the files
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function keyFunction() {
|
||||||
|
// actual implementation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
Brief explanation of how the pieces connect.
|
||||||
|
|
||||||
|
## Start Here
|
||||||
|
Which file to look at first and why.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: worker
|
||||||
|
description: General-purpose subagent with full capabilities, isolated context
|
||||||
|
model: openai-codex/gpt-5.3-codex
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a worker agent with full capabilities. You operate in an isolated context window to handle delegated tasks without polluting the main conversation.
|
||||||
|
|
||||||
|
Work autonomously to complete the assigned task. Use all available tools as needed.
|
||||||
|
|
||||||
|
Output format when finished:
|
||||||
|
|
||||||
|
## Completed
|
||||||
|
What was done.
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
- `path/to/file.ts` - what changed
|
||||||
|
|
||||||
|
## Notes (if any)
|
||||||
|
Anything the main agent should know.
|
||||||
|
|
||||||
|
If handing off to another agent (e.g. reviewer), include:
|
||||||
|
- Exact file paths changed
|
||||||
|
- Key functions/types touched (short list)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Shared dependency workspace for Pi extensions
|
||||||
|
|
||||||
|
This directory is the single workspace root for local custom Pi extensions.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
- Root-level `package.json` + one shared `node_modules`
|
||||||
|
- Extensions loaded from this tree as:
|
||||||
|
- `*.ts` files under this root, or
|
||||||
|
- subfolders containing `index.ts` (current: `multiagent/`, `subagent/`)
|
||||||
|
|
||||||
|
This preserves Pi auto-discovery and `/reload` behavior.
|
||||||
|
|
||||||
|
## Dependency policy
|
||||||
|
- Declare shared dependencies only in `extensions/package.json`.
|
||||||
|
- Do not add per-extension `package.json`/`node_modules` unless you intentionally need isolation.
|
||||||
|
|
||||||
|
## Note about `pi install` packages
|
||||||
|
Extensions installed via `pi install` are isolated package roots by design and do not share this workspace `node_modules`.
|
||||||
|
If you need dependency sharing across those, consolidate them into a single package/workspace you control and load that package instead.
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import type { Message } from "@earendil-works/pi-ai";
|
||||||
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { TDM_PROMPT, DEV_PROMPT, ANA_PROMPT } from "./prompts";
|
||||||
|
import { AgentRunResult, AnaVerdict } from "./types";
|
||||||
|
|
||||||
|
|
||||||
|
const MAX_ITERATIONS = 5;
|
||||||
|
|
||||||
|
// UI Symbols:
|
||||||
|
// ┌ ┐ └ ┘ │ ─ ├ ┤
|
||||||
|
|
||||||
|
const TDM_UI = `
|
||||||
|
┌─────┐ ┌─────┐ ┌─────┐
|
||||||
|
│ **TDM** │ --> │ DEV │ --> │ ANA │
|
||||||
|
└─────┘ └─────┘ └─────┘
|
||||||
|
↗
|
||||||
|
`
|
||||||
|
|
||||||
|
const DEV_UI = `
|
||||||
|
┌─────┐ ┌─────┐ ┌─────┐
|
||||||
|
│ TDM │ --> │ **DEV** │ --> │ ANA │
|
||||||
|
└─────┘ └─────┘ └─────┘
|
||||||
|
↗
|
||||||
|
`
|
||||||
|
|
||||||
|
const ANA_UI = `
|
||||||
|
┌─────┐ ┌─────┐ ┌─────┐
|
||||||
|
│ TDM │ --> │ DEV │ --> │ **ANA** │
|
||||||
|
└─────┘ └─────┘ └─────┘
|
||||||
|
↗
|
||||||
|
`
|
||||||
|
|
||||||
|
const ANA_REWORK_UI = `
|
||||||
|
┌─────┐ ┌─────┐ ┌─────┐
|
||||||
|
│ TDM │ --> │ DEV │ --> │ **ANA** │
|
||||||
|
└─────┘ └─────┘ └─────┘
|
||||||
|
↖
|
||||||
|
`
|
||||||
|
|
||||||
|
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
||||||
|
const currentScript = process.argv[1];
|
||||||
|
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
||||||
|
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
||||||
|
return { command: process.execPath, args: [currentScript, ...args] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const execName = path.basename(process.execPath).toLowerCase();
|
||||||
|
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
||||||
|
if (!isGenericRuntime) return { command: process.execPath, args };
|
||||||
|
return { command: "pi", args };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writePromptToTempFile(
|
||||||
|
name: string,
|
||||||
|
prompt: string,
|
||||||
|
): Promise<{ dir: string; filePath: string }> {
|
||||||
|
const tmpDir = await fs.promises.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), "pi-multiagent-"),
|
||||||
|
);
|
||||||
|
const safeName = name.replace(/[^\w.-]+/g, "_");
|
||||||
|
const filePath = path.join(tmpDir, `${safeName}.md`);
|
||||||
|
await fs.promises.writeFile(filePath, prompt, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
mode: 0o600,
|
||||||
|
});
|
||||||
|
return { dir: tmpDir, filePath };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFinalText(messages: Message[]): string {
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const msg = messages[i];
|
||||||
|
if (msg.role !== "assistant") continue;
|
||||||
|
const text = msg.content.find((c) => c.type === "text");
|
||||||
|
if (text && text.type === "text") return text.text;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAgent(
|
||||||
|
cwd: string,
|
||||||
|
systemPrompt: string,
|
||||||
|
task: string,
|
||||||
|
tools: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<AgentRunResult> {
|
||||||
|
const tmp = await writePromptToTempFile("system", systemPrompt);
|
||||||
|
const args = [
|
||||||
|
"--mode",
|
||||||
|
"json",
|
||||||
|
"-p",
|
||||||
|
"--no-session",
|
||||||
|
"--tools",
|
||||||
|
tools,
|
||||||
|
"--append-system-prompt",
|
||||||
|
tmp.filePath,
|
||||||
|
task,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await new Promise<AgentRunResult>((resolve) => {
|
||||||
|
const invocation = getPiInvocation(args);
|
||||||
|
const proc = spawn(invocation.command, invocation.args, {
|
||||||
|
cwd,
|
||||||
|
shell: false,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const messages: Message[] = [];
|
||||||
|
let stderr = "";
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
const processLine = (line: string) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(line);
|
||||||
|
if (event.type === "message_end" && event.message)
|
||||||
|
messages.push(event.message as Message);
|
||||||
|
if (event.type === "tool_result_end" && event.message)
|
||||||
|
messages.push(event.message as Message);
|
||||||
|
} catch {
|
||||||
|
// ignore malformed lines
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
proc.stdout.on("data", (d) => {
|
||||||
|
buffer += d.toString();
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() || "";
|
||||||
|
for (const l of lines) processLine(l);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stderr.on("data", (d) => {
|
||||||
|
stderr += d.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("close", (code) => {
|
||||||
|
if (buffer.trim()) processLine(buffer);
|
||||||
|
resolve({ exitCode: code ?? 1, messages, stderr });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
const killProc = () => {
|
||||||
|
proc.kill("SIGTERM");
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!proc.killed) proc.kill("SIGKILL");
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
if (signal.aborted) killProc();
|
||||||
|
else signal.addEventListener("abort", killProc, { once: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tmp.filePath);
|
||||||
|
} catch { }
|
||||||
|
try {
|
||||||
|
fs.rmdirSync(tmp.dir);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAnaVerdict(raw: string): AnaVerdict {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as AnaVerdict;
|
||||||
|
return {
|
||||||
|
approved: Boolean(parsed.approved),
|
||||||
|
feedback: String(parsed.feedback ?? "").trim(),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
approved: false,
|
||||||
|
feedback: `ANA output was not valid JSON:\n${raw}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLoading(
|
||||||
|
setStatus: (text: string) => void,
|
||||||
|
baseLabel: string,
|
||||||
|
): () => void {
|
||||||
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||||
|
let i = 0;
|
||||||
|
setStatus(`${frames[0]} ${baseLabel}`);
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
i = (i + 1) % frames.length;
|
||||||
|
setStatus(`${frames[i]} ${baseLabel}`);
|
||||||
|
}, 120);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
let enabled = false;
|
||||||
|
|
||||||
|
pi.registerCommand("multiagent", {
|
||||||
|
description: "Toggle multi-agent workflow (/multiagent on|off|status)",
|
||||||
|
handler: async (args, ctx) => {
|
||||||
|
const action = (args || "toggle").trim().toLowerCase();
|
||||||
|
if (action === "on") enabled = true;
|
||||||
|
else if (action === "off") enabled = false;
|
||||||
|
else if (action === "toggle") enabled = !enabled;
|
||||||
|
else if (action === "status") {
|
||||||
|
ctx.ui.notify(`Multiagent: ${enabled ? "ON" : "OFF"}`, "info");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
ctx.ui.notify("Usage: /multiagent [on|off|toggle|status]", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.ui.notify(
|
||||||
|
`Multiagent: ${enabled ? "ON" : "OFF"}`,
|
||||||
|
enabled ? "info" : "info",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.registerShortcut("ctrl+alt+m", {
|
||||||
|
description: "Toggle multi-agent workflow",
|
||||||
|
handler: async (ctx) => {
|
||||||
|
enabled = !enabled;
|
||||||
|
ctx.ui.notify(
|
||||||
|
`Multiagent: ${enabled ? "ON" : "OFF"}`,
|
||||||
|
enabled ? "info" : "info",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("input", async (event, ctx) => {
|
||||||
|
if (!enabled) return { action: "continue" as const };
|
||||||
|
if (event.source === "extension") return { action: "continue" as const };
|
||||||
|
if (event.text.trim().startsWith("/"))
|
||||||
|
return { action: "continue" as const };
|
||||||
|
|
||||||
|
ctx.ui.setStatus("multiagent", "multiagent: queued");
|
||||||
|
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "multiagent-result",
|
||||||
|
content: `### Multiagent workflow started\n\n${TDM_UI}\n\n(max ${MAX_ITERATIONS} DEV/ANA loops).`,
|
||||||
|
display: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userTask = event.text.trim();
|
||||||
|
|
||||||
|
const stopTdmLoader = startLoading(
|
||||||
|
(text) => ctx.ui.setStatus("multiagent", text),
|
||||||
|
"TDM analyzing request + codebase",
|
||||||
|
);
|
||||||
|
|
||||||
|
const tdm = await runAgent(
|
||||||
|
ctx.cwd,
|
||||||
|
TDM_PROMPT,
|
||||||
|
`User request:\n${userTask}`,
|
||||||
|
"read,grep,find,ls,bash",
|
||||||
|
ctx.signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
stopTdmLoader();
|
||||||
|
|
||||||
|
const specs = getFinalText(tdm.messages);
|
||||||
|
|
||||||
|
if (tdm.exitCode !== 0 || !specs) {
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "multiagent-result",
|
||||||
|
content: `TDM step failed.\n\n${tdm.stderr || "No output."}`,
|
||||||
|
display: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.ui.setStatus("multiagent", "");
|
||||||
|
|
||||||
|
return { action: "handled" as const };
|
||||||
|
}
|
||||||
|
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "multiagent-result",
|
||||||
|
content: `### TDM output (specs)\n\n${specs}`,
|
||||||
|
display: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "multiagent-result",
|
||||||
|
content: DEV_UI,
|
||||||
|
display: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let devContext = `TDM Specs:\n${specs}`;
|
||||||
|
let lastDevOutput = "";
|
||||||
|
let lastAnaFeedback = "";
|
||||||
|
|
||||||
|
for (let i = 1; i <= MAX_ITERATIONS; i++) {
|
||||||
|
const stopDevLoader = startLoading(
|
||||||
|
(text) => ctx.ui.setStatus("multiagent", text),
|
||||||
|
`DEV implementing iteration ${i}/${MAX_ITERATIONS}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const dev = await runAgent(
|
||||||
|
ctx.cwd,
|
||||||
|
DEV_PROMPT,
|
||||||
|
`${devContext}\n\nProceed with the implementation the current codebase. Then summarize changes and rationale.`,
|
||||||
|
"read,grep,find,ls,bash,edit,write",
|
||||||
|
ctx.signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
stopDevLoader();
|
||||||
|
|
||||||
|
lastDevOutput = getFinalText(dev.messages);
|
||||||
|
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "multiagent-result",
|
||||||
|
content: `### DEV iteration ${i} output\n\n${lastDevOutput || "(no DEV output)"}`,
|
||||||
|
display: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dev.exitCode !== 0) {
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "multiagent-result",
|
||||||
|
content: `DEV iteration ${i} failed.\n\n${dev.stderr || "No stderr output."}`,
|
||||||
|
display: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "multiagent-result",
|
||||||
|
content: ANA_UI,
|
||||||
|
display: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const stopAnaLoader = startLoading(
|
||||||
|
(text) => ctx.ui.setStatus("multiagent", text),
|
||||||
|
`ANA reviewing iteration ${i}/${MAX_ITERATIONS}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ana = await runAgent(
|
||||||
|
ctx.cwd,
|
||||||
|
ANA_PROMPT,
|
||||||
|
`TDM Specs:\n${specs}\n\nDEV report:\n${lastDevOutput}\n\nReview the implementation in the codebase and provide verdict JSON.`,
|
||||||
|
"read,grep,find,ls,bash",
|
||||||
|
ctx.signal,
|
||||||
|
);
|
||||||
|
|
||||||
|
stopAnaLoader();
|
||||||
|
|
||||||
|
const verdict = parseAnaVerdict(getFinalText(ana.messages));
|
||||||
|
lastAnaFeedback = verdict.feedback;
|
||||||
|
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "multiagent-result",
|
||||||
|
content: `### ANA iteration ${i} verdict\n\n- approved: **${verdict.approved ? "true" : "false"}**\n\n${verdict.feedback || "(no feedback)"}`,
|
||||||
|
display: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (verdict.approved) break;
|
||||||
|
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "multiagent-result",
|
||||||
|
content: ANA_REWORK_UI,
|
||||||
|
display: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
devContext = `TDM Specs:\n${specs}\n\nANA feedback to address:\n${verdict.feedback}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
pi.sendMessage({
|
||||||
|
customType: "multiagent-result",
|
||||||
|
content: `### Multiagent workflow completed\n\n${lastDevOutput || "(No DEV summary)"}\n\n---\n**Final ANA feedback:**\n${lastAnaFeedback || "Approved with no additional feedback."}`,
|
||||||
|
display: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.ui.setStatus("multiagent", "");
|
||||||
|
return { action: "handled" as const };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
export const TDM_PROMPT = `You are the Technical Delivery Manager (TDM).
|
||||||
|
Your job:
|
||||||
|
1) Deeply understand user intent.
|
||||||
|
2) Clarify ambiguities by explicitly listing assumptions.
|
||||||
|
3) Produce precise, context-aware implementation specs for a developer.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Focus on WHAT must be done, not implementation details.
|
||||||
|
- Be explicit, structured, and testable.
|
||||||
|
- Output markdown with sections:
|
||||||
|
- Intent
|
||||||
|
- Assumptions
|
||||||
|
- Scope
|
||||||
|
- Detailed Specs
|
||||||
|
- Acceptance Criteria`;
|
||||||
|
|
||||||
|
export const DEV_PROMPT = `You are the Developer (DEV).
|
||||||
|
Your job:
|
||||||
|
- Implement exactly the provided specs with minimal necessary changes.
|
||||||
|
- Prefer small, safe, reliable edits.
|
||||||
|
- Stay within scope.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Inspect code before editing.
|
||||||
|
- Make focused changes only.
|
||||||
|
- Report exactly what was changed and why.`;
|
||||||
|
|
||||||
|
export const ANA_PROMPT = `You are the Code Analyst (ANA).
|
||||||
|
Your job:
|
||||||
|
- Critique DEV output against TDM specs.
|
||||||
|
- Find mistakes, risky choices, anti-patterns, and sub-optimal decisions.
|
||||||
|
- Suggest concrete improvements.
|
||||||
|
|
||||||
|
You MUST output valid JSON only with this schema:
|
||||||
|
{
|
||||||
|
"approved": boolean,
|
||||||
|
"feedback": string
|
||||||
|
}
|
||||||
|
|
||||||
|
Set approved=true only if implementation is solid and no significant fixes are needed.`;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Message } from "@earendil-works/pi-ai";
|
||||||
|
|
||||||
|
export interface AgentRunResult {
|
||||||
|
exitCode: number;
|
||||||
|
messages: Message[];
|
||||||
|
stderr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnaVerdict {
|
||||||
|
approved: boolean;
|
||||||
|
feedback: string;
|
||||||
|
}
|
||||||
Generated
+3516
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,172 @@
|
|||||||
|
# Subagent Example
|
||||||
|
|
||||||
|
Delegate tasks to specialized subagents with isolated context windows.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Isolated context**: Each subagent runs in a separate `pi` process
|
||||||
|
- **Streaming output**: See tool calls and progress as they happen
|
||||||
|
- **Parallel streaming**: All parallel tasks stream updates simultaneously
|
||||||
|
- **Markdown rendering**: Final output rendered with proper formatting (expanded view)
|
||||||
|
- **Usage tracking**: Shows turns, tokens, cost, and context usage per agent
|
||||||
|
- **Abort support**: Ctrl+C propagates to kill subagent processes
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
subagent/
|
||||||
|
├── README.md # This file
|
||||||
|
├── index.ts # The extension (entry point)
|
||||||
|
├── agents.ts # Agent discovery logic
|
||||||
|
├── agents/ # Sample agent definitions
|
||||||
|
│ ├── scout.md # Fast recon, returns compressed context
|
||||||
|
│ ├── planner.md # Creates implementation plans
|
||||||
|
│ ├── reviewer.md # Code review
|
||||||
|
│ └── worker.md # General-purpose (full capabilities)
|
||||||
|
└── prompts/ # Workflow presets (prompt templates)
|
||||||
|
├── implement.md # scout -> planner -> worker
|
||||||
|
├── scout-and-plan.md # scout -> planner (no implementation)
|
||||||
|
└── implement-and-review.md # worker -> reviewer -> worker
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
From the repository root, symlink the files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Symlink the extension (must be in a subdirectory with index.ts)
|
||||||
|
mkdir -p ~/.pi/agent/extensions/subagent
|
||||||
|
ln -sf "$(pwd)/packages/coding-agent/examples/extensions/subagent/index.ts" ~/.pi/agent/extensions/subagent/index.ts
|
||||||
|
ln -sf "$(pwd)/packages/coding-agent/examples/extensions/subagent/agents.ts" ~/.pi/agent/extensions/subagent/agents.ts
|
||||||
|
|
||||||
|
# Symlink agents
|
||||||
|
mkdir -p ~/.pi/agent/agents
|
||||||
|
for f in packages/coding-agent/examples/extensions/subagent/agents/*.md; do
|
||||||
|
ln -sf "$(pwd)/$f" ~/.pi/agent/agents/$(basename "$f")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Symlink workflow prompts
|
||||||
|
mkdir -p ~/.pi/agent/prompts
|
||||||
|
for f in packages/coding-agent/examples/extensions/subagent/prompts/*.md; do
|
||||||
|
ln -sf "$(pwd)/$f" ~/.pi/agent/prompts/$(basename "$f")
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
This tool executes a separate `pi` subprocess with a delegated system prompt and tool/model configuration.
|
||||||
|
|
||||||
|
**Project-local agents** (`.pi/agents/*.md`) are repo-controlled prompts that can instruct the model to read files, run bash commands, etc.
|
||||||
|
|
||||||
|
**Default behavior:** Only loads **user-level agents** from `~/.pi/agent/agents`.
|
||||||
|
|
||||||
|
To enable project-local agents, pass `agentScope: "both"` (or `"project"`). Only do this for repositories you trust.
|
||||||
|
|
||||||
|
When running interactively, the tool prompts for confirmation before running project-local agents. Set `confirmProjectAgents: false` to disable.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Single agent
|
||||||
|
```
|
||||||
|
Use scout to find all authentication code
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parallel execution
|
||||||
|
```
|
||||||
|
Run 2 scouts in parallel: one to find models, one to find providers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Chained workflow
|
||||||
|
```
|
||||||
|
Use a chain: first have scout find the read tool, then have planner suggest improvements
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow prompts
|
||||||
|
```
|
||||||
|
/implement add Redis caching to the session store
|
||||||
|
/scout-and-plan refactor auth to support OAuth
|
||||||
|
/implement-and-review add input validation to API endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool Modes
|
||||||
|
|
||||||
|
| Mode | Parameter | Description |
|
||||||
|
|------|-----------|-------------|
|
||||||
|
| Single | `{ agent, task }` | One agent, one task |
|
||||||
|
| Parallel | `{ tasks: [...] }` | Multiple agents run concurrently (max 8, 4 concurrent) |
|
||||||
|
| Chain | `{ chain: [...] }` | Sequential with `{previous}` placeholder |
|
||||||
|
|
||||||
|
## Output Display
|
||||||
|
|
||||||
|
**Collapsed view** (default):
|
||||||
|
- Status icon (✓/✗/⏳) and agent name
|
||||||
|
- Last 5-10 items (tool calls and text)
|
||||||
|
- Usage stats: `3 turns ↑input ↓output RcacheRead WcacheWrite $cost ctx:contextTokens model`
|
||||||
|
|
||||||
|
**Expanded view** (Ctrl+O):
|
||||||
|
- Full task text
|
||||||
|
- All tool calls with formatted arguments
|
||||||
|
- Final output rendered as Markdown
|
||||||
|
- Per-task usage (for chain/parallel)
|
||||||
|
|
||||||
|
**Parallel mode streaming**:
|
||||||
|
- Shows all tasks with live status (⏳ running, ✓ done, ✗ failed)
|
||||||
|
- Updates as each task makes progress
|
||||||
|
- Shows "2/3 done, 1 running" status
|
||||||
|
|
||||||
|
**Tool call formatting** (mimics built-in tools):
|
||||||
|
- `$ command` for bash
|
||||||
|
- `read ~/path:1-10` for read
|
||||||
|
- `grep /pattern/ in ~/path` for grep
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
## Agent Definitions
|
||||||
|
|
||||||
|
Agents are markdown files with YAML frontmatter:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
name: my-agent
|
||||||
|
description: What this agent does
|
||||||
|
tools: read, grep, find, ls
|
||||||
|
model: claude-haiku-4-5
|
||||||
|
---
|
||||||
|
|
||||||
|
System prompt for the agent goes here.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Locations:**
|
||||||
|
- `~/.pi/agent/agents/*.md` - User-level (always loaded)
|
||||||
|
- `.pi/agents/*.md` - Project-level (only with `agentScope: "project"` or `"both"`)
|
||||||
|
|
||||||
|
Project agents override user agents with the same name when `agentScope: "both"`.
|
||||||
|
|
||||||
|
## Sample Agents
|
||||||
|
|
||||||
|
| Agent | Purpose | Model | Tools |
|
||||||
|
|-------|---------|-------|-------|
|
||||||
|
| `scout` | Fast codebase recon | Haiku | read, grep, find, ls, bash |
|
||||||
|
| `planner` | Implementation plans | Sonnet | read, grep, find, ls |
|
||||||
|
| `reviewer` | Code review | Sonnet | read, grep, find, ls, bash |
|
||||||
|
| `worker` | General-purpose | Sonnet | (all default) |
|
||||||
|
|
||||||
|
## Workflow Prompts
|
||||||
|
|
||||||
|
| Prompt | Flow |
|
||||||
|
|--------|------|
|
||||||
|
| `/implement <query>` | scout → planner → worker |
|
||||||
|
| `/scout-and-plan <query>` | scout → planner |
|
||||||
|
| `/implement-and-review <query>` | worker → reviewer → worker |
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- **Exit code != 0**: Tool returns error with stderr/output
|
||||||
|
- **stopReason "error"**: LLM error propagated with error message
|
||||||
|
- **stopReason "aborted"**: User abort (Ctrl+C) kills subprocess, throws error
|
||||||
|
- **Chain mode**: Stops at first failing step, reports which step failed
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Output truncated to last 10 items in collapsed view (expand to see all)
|
||||||
|
- Agents discovered fresh on each invocation (allows editing mid-session)
|
||||||
|
- Parallel mode limited to 8 tasks, 4 concurrent
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* Agent discovery and configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
||||||
|
|
||||||
|
export type AgentScope = "user" | "project" | "both";
|
||||||
|
|
||||||
|
export interface AgentConfig {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
tools?: string[];
|
||||||
|
model?: string;
|
||||||
|
systemPrompt: string;
|
||||||
|
source: "user" | "project";
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentDiscoveryResult {
|
||||||
|
agents: AgentConfig[];
|
||||||
|
projectAgentsDir: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
||||||
|
const agents: AgentConfig[] = [];
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries: fs.Dirent[];
|
||||||
|
try {
|
||||||
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.name.endsWith(".md")) continue;
|
||||||
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
||||||
|
|
||||||
|
const filePath = path.join(dir, entry.name);
|
||||||
|
let content: string;
|
||||||
|
try {
|
||||||
|
content = fs.readFileSync(filePath, "utf-8");
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
|
||||||
|
|
||||||
|
if (!frontmatter.name || !frontmatter.description) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tools = frontmatter.tools
|
||||||
|
?.split(",")
|
||||||
|
.map((t: string) => t.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
agents.push({
|
||||||
|
name: frontmatter.name,
|
||||||
|
description: frontmatter.description,
|
||||||
|
tools: tools && tools.length > 0 ? tools : undefined,
|
||||||
|
model: frontmatter.model,
|
||||||
|
systemPrompt: body,
|
||||||
|
source,
|
||||||
|
filePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDirectory(p: string): boolean {
|
||||||
|
try {
|
||||||
|
return fs.statSync(p).isDirectory();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNearestProjectAgentsDir(cwd: string): string | null {
|
||||||
|
let currentDir = cwd;
|
||||||
|
while (true) {
|
||||||
|
const candidate = path.join(currentDir, ".pi", "agents");
|
||||||
|
if (isDirectory(candidate)) return candidate;
|
||||||
|
|
||||||
|
const parentDir = path.dirname(currentDir);
|
||||||
|
if (parentDir === currentDir) return null;
|
||||||
|
currentDir = parentDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
||||||
|
const userDir = path.join(getAgentDir(), "agents");
|
||||||
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
||||||
|
|
||||||
|
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
||||||
|
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
||||||
|
|
||||||
|
const agentMap = new Map<string, AgentConfig>();
|
||||||
|
|
||||||
|
if (scope === "both") {
|
||||||
|
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
||||||
|
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
||||||
|
} else if (scope === "user") {
|
||||||
|
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
||||||
|
} else {
|
||||||
|
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
|
||||||
|
if (agents.length === 0) return { text: "none", remaining: 0 };
|
||||||
|
const listed = agents.slice(0, maxItems);
|
||||||
|
const remaining = agents.length - listed.length;
|
||||||
|
return {
|
||||||
|
text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
|
||||||
|
remaining,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,987 @@
|
|||||||
|
/**
|
||||||
|
* Subagent Tool - Delegate tasks to specialized agents
|
||||||
|
*
|
||||||
|
* Spawns a separate `pi` process for each subagent invocation,
|
||||||
|
* giving it an isolated context window.
|
||||||
|
*
|
||||||
|
* Supports three modes:
|
||||||
|
* - Single: { agent: "name", task: "..." }
|
||||||
|
* - Parallel: { tasks: [{ agent: "name", task: "..." }, ...] }
|
||||||
|
* - Chain: { chain: [{ agent: "name", task: "... {previous} ..." }, ...] }
|
||||||
|
*
|
||||||
|
* Uses JSON mode to capture structured output from subagents.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
||||||
|
import type { Message } from "@earendil-works/pi-ai";
|
||||||
|
import { StringEnum } from "@earendil-works/pi-ai";
|
||||||
|
import { type ExtensionAPI, getMarkdownTheme, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
||||||
|
import { Type } from "typebox";
|
||||||
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
||||||
|
|
||||||
|
const MAX_PARALLEL_TASKS = 8;
|
||||||
|
const MAX_CONCURRENCY = 4;
|
||||||
|
const COLLAPSED_ITEM_COUNT = 10;
|
||||||
|
|
||||||
|
function formatTokens(count: number): string {
|
||||||
|
if (count < 1000) return count.toString();
|
||||||
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
||||||
|
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
||||||
|
return `${(count / 1000000).toFixed(1)}M`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUsageStats(
|
||||||
|
usage: {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
cost: number;
|
||||||
|
contextTokens?: number;
|
||||||
|
turns?: number;
|
||||||
|
},
|
||||||
|
model?: string,
|
||||||
|
): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
||||||
|
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
||||||
|
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
||||||
|
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
||||||
|
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
||||||
|
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
||||||
|
if (usage.contextTokens && usage.contextTokens > 0) {
|
||||||
|
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
||||||
|
}
|
||||||
|
if (model) parts.push(model);
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatToolCall(
|
||||||
|
toolName: string,
|
||||||
|
args: Record<string, unknown>,
|
||||||
|
themeFg: (color: any, text: string) => string,
|
||||||
|
): string {
|
||||||
|
const shortenPath = (p: string) => {
|
||||||
|
const home = os.homedir();
|
||||||
|
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (toolName) {
|
||||||
|
case "bash": {
|
||||||
|
const command = (args.command as string) || "...";
|
||||||
|
const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
|
||||||
|
return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
|
||||||
|
}
|
||||||
|
case "read": {
|
||||||
|
const rawPath = (args.file_path || args.path || "...") as string;
|
||||||
|
const filePath = shortenPath(rawPath);
|
||||||
|
const offset = args.offset as number | undefined;
|
||||||
|
const limit = args.limit as number | undefined;
|
||||||
|
let text = themeFg("accent", filePath);
|
||||||
|
if (offset !== undefined || limit !== undefined) {
|
||||||
|
const startLine = offset ?? 1;
|
||||||
|
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
||||||
|
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
||||||
|
}
|
||||||
|
return themeFg("muted", "read ") + text;
|
||||||
|
}
|
||||||
|
case "write": {
|
||||||
|
const rawPath = (args.file_path || args.path || "...") as string;
|
||||||
|
const filePath = shortenPath(rawPath);
|
||||||
|
const content = (args.content || "") as string;
|
||||||
|
const lines = content.split("\n").length;
|
||||||
|
let text = themeFg("muted", "write ") + themeFg("accent", filePath);
|
||||||
|
if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
case "edit": {
|
||||||
|
const rawPath = (args.file_path || args.path || "...") as string;
|
||||||
|
return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
|
||||||
|
}
|
||||||
|
case "ls": {
|
||||||
|
const rawPath = (args.path || ".") as string;
|
||||||
|
return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
|
||||||
|
}
|
||||||
|
case "find": {
|
||||||
|
const pattern = (args.pattern || "*") as string;
|
||||||
|
const rawPath = (args.path || ".") as string;
|
||||||
|
return themeFg("muted", "find ") + themeFg("accent", pattern) + themeFg("dim", ` in ${shortenPath(rawPath)}`);
|
||||||
|
}
|
||||||
|
case "grep": {
|
||||||
|
const pattern = (args.pattern || "") as string;
|
||||||
|
const rawPath = (args.path || ".") as string;
|
||||||
|
return (
|
||||||
|
themeFg("muted", "grep ") +
|
||||||
|
themeFg("accent", `/${pattern}/`) +
|
||||||
|
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const argsStr = JSON.stringify(args);
|
||||||
|
const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
|
||||||
|
return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UsageStats {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
cost: number;
|
||||||
|
contextTokens: number;
|
||||||
|
turns: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SingleResult {
|
||||||
|
agent: string;
|
||||||
|
agentSource: "user" | "project" | "unknown";
|
||||||
|
task: string;
|
||||||
|
exitCode: number;
|
||||||
|
messages: Message[];
|
||||||
|
stderr: string;
|
||||||
|
usage: UsageStats;
|
||||||
|
model?: string;
|
||||||
|
stopReason?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
step?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubagentDetails {
|
||||||
|
mode: "single" | "parallel" | "chain";
|
||||||
|
agentScope: AgentScope;
|
||||||
|
projectAgentsDir: string | null;
|
||||||
|
results: SingleResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFinalOutput(messages: Message[]): string {
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const msg = messages[i];
|
||||||
|
if (msg.role === "assistant") {
|
||||||
|
for (const part of msg.content) {
|
||||||
|
if (part.type === "text") return part.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record<string, any> };
|
||||||
|
|
||||||
|
function getDisplayItems(messages: Message[]): DisplayItem[] {
|
||||||
|
const items: DisplayItem[] = [];
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.role === "assistant") {
|
||||||
|
for (const part of msg.content) {
|
||||||
|
if (part.type === "text") items.push({ type: "text", text: part.text });
|
||||||
|
else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mapWithConcurrencyLimit<TIn, TOut>(
|
||||||
|
items: TIn[],
|
||||||
|
concurrency: number,
|
||||||
|
fn: (item: TIn, index: number) => Promise<TOut>,
|
||||||
|
): Promise<TOut[]> {
|
||||||
|
if (items.length === 0) return [];
|
||||||
|
const limit = Math.max(1, Math.min(concurrency, items.length));
|
||||||
|
const results: TOut[] = new Array(items.length);
|
||||||
|
let nextIndex = 0;
|
||||||
|
const workers = new Array(limit).fill(null).map(async () => {
|
||||||
|
while (true) {
|
||||||
|
const current = nextIndex++;
|
||||||
|
if (current >= items.length) return;
|
||||||
|
results[current] = await fn(items[current], current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(workers);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writePromptToTempFile(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> {
|
||||||
|
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
|
||||||
|
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
||||||
|
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
||||||
|
await withFileMutationQueue(filePath, async () => {
|
||||||
|
await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
|
||||||
|
});
|
||||||
|
return { dir: tmpDir, filePath };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
||||||
|
const currentScript = process.argv[1];
|
||||||
|
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
||||||
|
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
||||||
|
return { command: process.execPath, args: [currentScript, ...args] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const execName = path.basename(process.execPath).toLowerCase();
|
||||||
|
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
||||||
|
if (!isGenericRuntime) {
|
||||||
|
return { command: process.execPath, args };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { command: "pi", args };
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
||||||
|
|
||||||
|
async function runSingleAgent(
|
||||||
|
defaultCwd: string,
|
||||||
|
agents: AgentConfig[],
|
||||||
|
agentName: string,
|
||||||
|
task: string,
|
||||||
|
cwd: string | undefined,
|
||||||
|
step: number | undefined,
|
||||||
|
signal: AbortSignal | undefined,
|
||||||
|
onUpdate: OnUpdateCallback | undefined,
|
||||||
|
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
||||||
|
): Promise<SingleResult> {
|
||||||
|
const agent = agents.find((a) => a.name === agentName);
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
||||||
|
return {
|
||||||
|
agent: agentName,
|
||||||
|
agentSource: "unknown",
|
||||||
|
task,
|
||||||
|
exitCode: 1,
|
||||||
|
messages: [],
|
||||||
|
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
||||||
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
||||||
|
step,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
||||||
|
if (agent.model) args.push("--model", agent.model);
|
||||||
|
if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
|
||||||
|
|
||||||
|
let tmpPromptDir: string | null = null;
|
||||||
|
let tmpPromptPath: string | null = null;
|
||||||
|
|
||||||
|
const currentResult: SingleResult = {
|
||||||
|
agent: agentName,
|
||||||
|
agentSource: agent.source,
|
||||||
|
task,
|
||||||
|
exitCode: 0,
|
||||||
|
messages: [],
|
||||||
|
stderr: "",
|
||||||
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
||||||
|
model: agent.model,
|
||||||
|
step,
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitUpdate = () => {
|
||||||
|
if (onUpdate) {
|
||||||
|
onUpdate({
|
||||||
|
content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
|
||||||
|
details: makeDetails([currentResult]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (agent.systemPrompt.trim()) {
|
||||||
|
const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
|
||||||
|
tmpPromptDir = tmp.dir;
|
||||||
|
tmpPromptPath = tmp.filePath;
|
||||||
|
args.push("--append-system-prompt", tmpPromptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(`Task: ${task}`);
|
||||||
|
let wasAborted = false;
|
||||||
|
|
||||||
|
const exitCode = await new Promise<number>((resolve) => {
|
||||||
|
const invocation = getPiInvocation(args);
|
||||||
|
const proc = spawn(invocation.command, invocation.args, {
|
||||||
|
cwd: cwd ?? defaultCwd,
|
||||||
|
shell: false,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
const processLine = (line: string) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
let event: any;
|
||||||
|
try {
|
||||||
|
event = JSON.parse(line);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "message_end" && event.message) {
|
||||||
|
const msg = event.message as Message;
|
||||||
|
currentResult.messages.push(msg);
|
||||||
|
|
||||||
|
if (msg.role === "assistant") {
|
||||||
|
currentResult.usage.turns++;
|
||||||
|
const usage = msg.usage;
|
||||||
|
if (usage) {
|
||||||
|
currentResult.usage.input += usage.input || 0;
|
||||||
|
currentResult.usage.output += usage.output || 0;
|
||||||
|
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
||||||
|
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
||||||
|
currentResult.usage.cost += usage.cost?.total || 0;
|
||||||
|
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
||||||
|
}
|
||||||
|
if (!currentResult.model && msg.model) currentResult.model = msg.model;
|
||||||
|
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
||||||
|
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
|
||||||
|
}
|
||||||
|
emitUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === "tool_result_end" && event.message) {
|
||||||
|
currentResult.messages.push(event.message as Message);
|
||||||
|
emitUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
proc.stdout.on("data", (data) => {
|
||||||
|
buffer += data.toString();
|
||||||
|
const lines = buffer.split("\n");
|
||||||
|
buffer = lines.pop() || "";
|
||||||
|
for (const line of lines) processLine(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stderr.on("data", (data) => {
|
||||||
|
currentResult.stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("close", (code) => {
|
||||||
|
if (buffer.trim()) processLine(buffer);
|
||||||
|
resolve(code ?? 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("error", () => {
|
||||||
|
resolve(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (signal) {
|
||||||
|
const killProc = () => {
|
||||||
|
wasAborted = true;
|
||||||
|
proc.kill("SIGTERM");
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!proc.killed) proc.kill("SIGKILL");
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
if (signal.aborted) killProc();
|
||||||
|
else signal.addEventListener("abort", killProc, { once: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
currentResult.exitCode = exitCode;
|
||||||
|
if (wasAborted) throw new Error("Subagent was aborted");
|
||||||
|
return currentResult;
|
||||||
|
} finally {
|
||||||
|
if (tmpPromptPath)
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tmpPromptPath);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
if (tmpPromptDir)
|
||||||
|
try {
|
||||||
|
fs.rmdirSync(tmpPromptDir);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskItem = Type.Object({
|
||||||
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
||||||
|
task: Type.String({ description: "Task to delegate to the agent" }),
|
||||||
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ChainItem = Type.Object({
|
||||||
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
||||||
|
task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
|
||||||
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
||||||
|
});
|
||||||
|
|
||||||
|
const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
||||||
|
description: 'Which agent directories to use. Default: "user". Use "both" to include project-local agents.',
|
||||||
|
default: "user",
|
||||||
|
});
|
||||||
|
|
||||||
|
const SubagentParams = Type.Object({
|
||||||
|
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (for single mode)" })),
|
||||||
|
task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
|
||||||
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
||||||
|
chain: Type.Optional(Type.Array(ChainItem, { description: "Array of {agent, task} for sequential execution" })),
|
||||||
|
agentScope: Type.Optional(AgentScopeSchema),
|
||||||
|
confirmProjectAgents: Type.Optional(
|
||||||
|
Type.Boolean({ description: "Prompt before running project-local agents. Default: true.", default: true }),
|
||||||
|
),
|
||||||
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
pi.registerTool({
|
||||||
|
name: "subagent",
|
||||||
|
label: "Subagent",
|
||||||
|
description: [
|
||||||
|
"Delegate tasks to specialized subagents with isolated context.",
|
||||||
|
"Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
|
||||||
|
'Default agent scope is "user" (from ~/.pi/agent/agents).',
|
||||||
|
'To enable project-local agents in .pi/agents, set agentScope: "both" (or "project").',
|
||||||
|
].join(" "),
|
||||||
|
parameters: SubagentParams,
|
||||||
|
|
||||||
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
||||||
|
const agentScope: AgentScope = params.agentScope ?? "user";
|
||||||
|
const discovery = discoverAgents(ctx.cwd, agentScope);
|
||||||
|
const agents = discovery.agents;
|
||||||
|
const confirmProjectAgents = params.confirmProjectAgents ?? true;
|
||||||
|
|
||||||
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
||||||
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
||||||
|
const hasSingle = Boolean(params.agent && params.task);
|
||||||
|
const modeCount = Number(hasChain) + Number(hasTasks) + Number(hasSingle);
|
||||||
|
|
||||||
|
const makeDetails =
|
||||||
|
(mode: "single" | "parallel" | "chain") =>
|
||||||
|
(results: SingleResult[]): SubagentDetails => ({
|
||||||
|
mode,
|
||||||
|
agentScope,
|
||||||
|
projectAgentsDir: discovery.projectAgentsDir,
|
||||||
|
results,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modeCount !== 1) {
|
||||||
|
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Invalid parameters. Provide exactly one mode.\nAvailable agents: ${available}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: makeDetails("single")([]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((agentScope === "project" || agentScope === "both") && confirmProjectAgents && ctx.hasUI) {
|
||||||
|
const requestedAgentNames = new Set<string>();
|
||||||
|
if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
|
||||||
|
if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
|
||||||
|
if (params.agent) requestedAgentNames.add(params.agent);
|
||||||
|
|
||||||
|
const projectAgentsRequested = Array.from(requestedAgentNames)
|
||||||
|
.map((name) => agents.find((a) => a.name === name))
|
||||||
|
.filter((a): a is AgentConfig => a?.source === "project");
|
||||||
|
|
||||||
|
if (projectAgentsRequested.length > 0) {
|
||||||
|
const names = projectAgentsRequested.map((a) => a.name).join(", ");
|
||||||
|
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
||||||
|
const ok = await ctx.ui.confirm(
|
||||||
|
"Run project-local agents?",
|
||||||
|
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
||||||
|
);
|
||||||
|
if (!ok)
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
|
||||||
|
details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.chain && params.chain.length > 0) {
|
||||||
|
const results: SingleResult[] = [];
|
||||||
|
let previousOutput = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < params.chain.length; i++) {
|
||||||
|
const step = params.chain[i];
|
||||||
|
const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
|
||||||
|
|
||||||
|
// Create update callback that includes all previous results
|
||||||
|
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
||||||
|
? (partial) => {
|
||||||
|
// Combine completed results with current streaming result
|
||||||
|
const currentResult = partial.details?.results[0];
|
||||||
|
if (currentResult) {
|
||||||
|
const allResults = [...results, currentResult];
|
||||||
|
onUpdate({
|
||||||
|
content: partial.content,
|
||||||
|
details: makeDetails("chain")(allResults),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const result = await runSingleAgent(
|
||||||
|
ctx.cwd,
|
||||||
|
agents,
|
||||||
|
step.agent,
|
||||||
|
taskWithContext,
|
||||||
|
step.cwd,
|
||||||
|
i + 1,
|
||||||
|
signal,
|
||||||
|
chainUpdate,
|
||||||
|
makeDetails("chain"),
|
||||||
|
);
|
||||||
|
results.push(result);
|
||||||
|
|
||||||
|
const isError =
|
||||||
|
result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
||||||
|
if (isError) {
|
||||||
|
const errorMsg =
|
||||||
|
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
||||||
|
details: makeDetails("chain")(results),
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
previousOutput = getFinalOutput(result.messages);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: getFinalOutput(results[results.length - 1].messages) || "(no output)" }],
|
||||||
|
details: makeDetails("chain")(results),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.tasks && params.tasks.length > 0) {
|
||||||
|
if (params.tasks.length > MAX_PARALLEL_TASKS)
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: makeDetails("parallel")([]),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track all results for streaming updates
|
||||||
|
const allResults: SingleResult[] = new Array(params.tasks.length);
|
||||||
|
|
||||||
|
// Initialize placeholder results
|
||||||
|
for (let i = 0; i < params.tasks.length; i++) {
|
||||||
|
allResults[i] = {
|
||||||
|
agent: params.tasks[i].agent,
|
||||||
|
agentSource: "unknown",
|
||||||
|
task: params.tasks[i].task,
|
||||||
|
exitCode: -1, // -1 = still running
|
||||||
|
messages: [],
|
||||||
|
stderr: "",
|
||||||
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitParallelUpdate = () => {
|
||||||
|
if (onUpdate) {
|
||||||
|
const running = allResults.filter((r) => r.exitCode === -1).length;
|
||||||
|
const done = allResults.filter((r) => r.exitCode !== -1).length;
|
||||||
|
onUpdate({
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: `Parallel: ${done}/${allResults.length} done, ${running} running...` },
|
||||||
|
],
|
||||||
|
details: makeDetails("parallel")([...allResults]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
||||||
|
const result = await runSingleAgent(
|
||||||
|
ctx.cwd,
|
||||||
|
agents,
|
||||||
|
t.agent,
|
||||||
|
t.task,
|
||||||
|
t.cwd,
|
||||||
|
undefined,
|
||||||
|
signal,
|
||||||
|
// Per-task update callback
|
||||||
|
(partial) => {
|
||||||
|
if (partial.details?.results[0]) {
|
||||||
|
allResults[index] = partial.details.results[0];
|
||||||
|
emitParallelUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
makeDetails("parallel"),
|
||||||
|
);
|
||||||
|
allResults[index] = result;
|
||||||
|
emitParallelUpdate();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const successCount = results.filter((r) => r.exitCode === 0).length;
|
||||||
|
const summaries = results.map((r) => {
|
||||||
|
const output = getFinalOutput(r.messages);
|
||||||
|
const preview = output.slice(0, 100) + (output.length > 100 ? "..." : "");
|
||||||
|
return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
details: makeDetails("parallel")(results),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.agent && params.task) {
|
||||||
|
const result = await runSingleAgent(
|
||||||
|
ctx.cwd,
|
||||||
|
agents,
|
||||||
|
params.agent,
|
||||||
|
params.task,
|
||||||
|
params.cwd,
|
||||||
|
undefined,
|
||||||
|
signal,
|
||||||
|
onUpdate,
|
||||||
|
makeDetails("single"),
|
||||||
|
);
|
||||||
|
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
||||||
|
if (isError) {
|
||||||
|
const errorMsg =
|
||||||
|
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
||||||
|
details: makeDetails("single")([result]),
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: getFinalOutput(result.messages) || "(no output)" }],
|
||||||
|
details: makeDetails("single")([result]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Invalid parameters. Available agents: ${available}` }],
|
||||||
|
details: makeDetails("single")([]),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCall(args, theme, _context) {
|
||||||
|
const scope: AgentScope = args.agentScope ?? "user";
|
||||||
|
if (args.chain && args.chain.length > 0) {
|
||||||
|
let text =
|
||||||
|
theme.fg("toolTitle", theme.bold("subagent ")) +
|
||||||
|
theme.fg("accent", `chain (${args.chain.length} steps)`) +
|
||||||
|
theme.fg("muted", ` [${scope}]`);
|
||||||
|
for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
|
||||||
|
const step = args.chain[i];
|
||||||
|
// Clean up {previous} placeholder for display
|
||||||
|
const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
|
||||||
|
const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
|
||||||
|
text +=
|
||||||
|
"\n " +
|
||||||
|
theme.fg("muted", `${i + 1}.`) +
|
||||||
|
" " +
|
||||||
|
theme.fg("accent", step.agent) +
|
||||||
|
theme.fg("dim", ` ${preview}`);
|
||||||
|
}
|
||||||
|
if (args.chain.length > 3) text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`;
|
||||||
|
return new Text(text, 0, 0);
|
||||||
|
}
|
||||||
|
if (args.tasks && args.tasks.length > 0) {
|
||||||
|
let text =
|
||||||
|
theme.fg("toolTitle", theme.bold("subagent ")) +
|
||||||
|
theme.fg("accent", `parallel (${args.tasks.length} tasks)`) +
|
||||||
|
theme.fg("muted", ` [${scope}]`);
|
||||||
|
for (const t of args.tasks.slice(0, 3)) {
|
||||||
|
const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
|
||||||
|
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`;
|
||||||
|
}
|
||||||
|
if (args.tasks.length > 3) text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`;
|
||||||
|
return new Text(text, 0, 0);
|
||||||
|
}
|
||||||
|
const agentName = args.agent || "...";
|
||||||
|
const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "...";
|
||||||
|
let text =
|
||||||
|
theme.fg("toolTitle", theme.bold("subagent ")) +
|
||||||
|
theme.fg("accent", agentName) +
|
||||||
|
theme.fg("muted", ` [${scope}]`);
|
||||||
|
text += `\n ${theme.fg("dim", preview)}`;
|
||||||
|
return new Text(text, 0, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderResult(result, { expanded }, theme, _context) {
|
||||||
|
const details = result.details as SubagentDetails | undefined;
|
||||||
|
if (!details || details.results.length === 0) {
|
||||||
|
const text = result.content[0];
|
||||||
|
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mdTheme = getMarkdownTheme();
|
||||||
|
|
||||||
|
const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
|
||||||
|
const toShow = limit ? items.slice(-limit) : items;
|
||||||
|
const skipped = limit && items.length > limit ? items.length - limit : 0;
|
||||||
|
let text = "";
|
||||||
|
if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
|
||||||
|
for (const item of toShow) {
|
||||||
|
if (item.type === "text") {
|
||||||
|
const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
|
||||||
|
text += `${theme.fg("toolOutput", preview)}\n`;
|
||||||
|
} else {
|
||||||
|
text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text.trimEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (details.mode === "single" && details.results.length === 1) {
|
||||||
|
const r = details.results[0];
|
||||||
|
const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
|
||||||
|
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
||||||
|
const displayItems = getDisplayItems(r.messages);
|
||||||
|
const finalOutput = getFinalOutput(r.messages);
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
const container = new Container();
|
||||||
|
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
||||||
|
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
||||||
|
container.addChild(new Text(header, 0, 0));
|
||||||
|
if (isError && r.errorMessage)
|
||||||
|
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
||||||
|
container.addChild(new Spacer(1));
|
||||||
|
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
||||||
|
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
||||||
|
container.addChild(new Spacer(1));
|
||||||
|
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
|
||||||
|
if (displayItems.length === 0 && !finalOutput) {
|
||||||
|
container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
|
||||||
|
} else {
|
||||||
|
for (const item of displayItems) {
|
||||||
|
if (item.type === "toolCall")
|
||||||
|
container.addChild(
|
||||||
|
new Text(
|
||||||
|
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (finalOutput) {
|
||||||
|
container.addChild(new Spacer(1));
|
||||||
|
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const usageStr = formatUsageStats(r.usage, r.model);
|
||||||
|
if (usageStr) {
|
||||||
|
container.addChild(new Spacer(1));
|
||||||
|
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
||||||
|
if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
||||||
|
if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
||||||
|
else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
|
||||||
|
else {
|
||||||
|
text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
|
||||||
|
if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
||||||
|
}
|
||||||
|
const usageStr = formatUsageStats(r.usage, r.model);
|
||||||
|
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
||||||
|
return new Text(text, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const aggregateUsage = (results: SingleResult[]) => {
|
||||||
|
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
||||||
|
for (const r of results) {
|
||||||
|
total.input += r.usage.input;
|
||||||
|
total.output += r.usage.output;
|
||||||
|
total.cacheRead += r.usage.cacheRead;
|
||||||
|
total.cacheWrite += r.usage.cacheWrite;
|
||||||
|
total.cost += r.usage.cost;
|
||||||
|
total.turns += r.usage.turns;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (details.mode === "chain") {
|
||||||
|
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
||||||
|
const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
const container = new Container();
|
||||||
|
container.addChild(
|
||||||
|
new Text(
|
||||||
|
icon +
|
||||||
|
" " +
|
||||||
|
theme.fg("toolTitle", theme.bold("chain ")) +
|
||||||
|
theme.fg("accent", `${successCount}/${details.results.length} steps`),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const r of details.results) {
|
||||||
|
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
||||||
|
const displayItems = getDisplayItems(r.messages);
|
||||||
|
const finalOutput = getFinalOutput(r.messages);
|
||||||
|
|
||||||
|
container.addChild(new Spacer(1));
|
||||||
|
container.addChild(
|
||||||
|
new Text(
|
||||||
|
`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
||||||
|
|
||||||
|
// Show tool calls
|
||||||
|
for (const item of displayItems) {
|
||||||
|
if (item.type === "toolCall") {
|
||||||
|
container.addChild(
|
||||||
|
new Text(
|
||||||
|
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show final output as markdown
|
||||||
|
if (finalOutput) {
|
||||||
|
container.addChild(new Spacer(1));
|
||||||
|
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepUsage = formatUsageStats(r.usage, r.model);
|
||||||
|
if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
||||||
|
if (usageStr) {
|
||||||
|
container.addChild(new Spacer(1));
|
||||||
|
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapsed view
|
||||||
|
let text =
|
||||||
|
icon +
|
||||||
|
" " +
|
||||||
|
theme.fg("toolTitle", theme.bold("chain ")) +
|
||||||
|
theme.fg("accent", `${successCount}/${details.results.length} steps`);
|
||||||
|
for (const r of details.results) {
|
||||||
|
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
||||||
|
const displayItems = getDisplayItems(r.messages);
|
||||||
|
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
||||||
|
if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
|
||||||
|
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
||||||
|
}
|
||||||
|
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
||||||
|
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
||||||
|
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
||||||
|
return new Text(text, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.mode === "parallel") {
|
||||||
|
const running = details.results.filter((r) => r.exitCode === -1).length;
|
||||||
|
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
||||||
|
const failCount = details.results.filter((r) => r.exitCode > 0).length;
|
||||||
|
const isRunning = running > 0;
|
||||||
|
const icon = isRunning
|
||||||
|
? theme.fg("warning", "⏳")
|
||||||
|
: failCount > 0
|
||||||
|
? theme.fg("warning", "◐")
|
||||||
|
: theme.fg("success", "✓");
|
||||||
|
const status = isRunning
|
||||||
|
? `${successCount + failCount}/${details.results.length} done, ${running} running`
|
||||||
|
: `${successCount}/${details.results.length} tasks`;
|
||||||
|
|
||||||
|
if (expanded && !isRunning) {
|
||||||
|
const container = new Container();
|
||||||
|
container.addChild(
|
||||||
|
new Text(
|
||||||
|
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const r of details.results) {
|
||||||
|
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
||||||
|
const displayItems = getDisplayItems(r.messages);
|
||||||
|
const finalOutput = getFinalOutput(r.messages);
|
||||||
|
|
||||||
|
container.addChild(new Spacer(1));
|
||||||
|
container.addChild(
|
||||||
|
new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
|
||||||
|
);
|
||||||
|
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
||||||
|
|
||||||
|
// Show tool calls
|
||||||
|
for (const item of displayItems) {
|
||||||
|
if (item.type === "toolCall") {
|
||||||
|
container.addChild(
|
||||||
|
new Text(
|
||||||
|
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show final output as markdown
|
||||||
|
if (finalOutput) {
|
||||||
|
container.addChild(new Spacer(1));
|
||||||
|
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskUsage = formatUsageStats(r.usage, r.model);
|
||||||
|
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
||||||
|
if (usageStr) {
|
||||||
|
container.addChild(new Spacer(1));
|
||||||
|
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapsed view (or still running)
|
||||||
|
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
|
||||||
|
for (const r of details.results) {
|
||||||
|
const rIcon =
|
||||||
|
r.exitCode === -1
|
||||||
|
? theme.fg("warning", "⏳")
|
||||||
|
: r.exitCode === 0
|
||||||
|
? theme.fg("success", "✓")
|
||||||
|
: theme.fg("error", "✗");
|
||||||
|
const displayItems = getDisplayItems(r.messages);
|
||||||
|
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
||||||
|
if (displayItems.length === 0)
|
||||||
|
text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
||||||
|
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
||||||
|
}
|
||||||
|
if (!isRunning) {
|
||||||
|
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
||||||
|
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
||||||
|
}
|
||||||
|
if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
||||||
|
return new Text(text, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = result.content[0];
|
||||||
|
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
description: Worker implements, reviewer reviews, worker applies feedback
|
||||||
|
---
|
||||||
|
Use the subagent tool with the chain parameter to execute this workflow:
|
||||||
|
|
||||||
|
1. First, use the "worker" agent to implement: $@
|
||||||
|
2. Then, use the "reviewer" agent to review the implementation from the previous step (use {previous} placeholder)
|
||||||
|
3. Finally, use the "worker" agent to apply the feedback from the review (use {previous} placeholder)
|
||||||
|
|
||||||
|
Execute this as a chain, passing output between steps via {previous}.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
description: Full implementation workflow - scout gathers context, planner creates plan, worker implements
|
||||||
|
---
|
||||||
|
Use the subagent tool with the chain parameter to execute this workflow:
|
||||||
|
|
||||||
|
1. First, use the "scout" agent to find all code relevant to: $@
|
||||||
|
2. Then, use the "planner" agent to create an implementation plan for "$@" using the context from the previous step (use {previous} placeholder)
|
||||||
|
3. Finally, use the "worker" agent to implement the plan from the previous step (use {previous} placeholder)
|
||||||
|
|
||||||
|
Execute this as a chain, passing output between steps via {previous}.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
description: Scout gathers context, planner creates implementation plan (no implementation)
|
||||||
|
---
|
||||||
|
Use the subagent tool with the chain parameter to execute this workflow:
|
||||||
|
|
||||||
|
1. First, use the "scout" agent to find all code relevant to: $@
|
||||||
|
2. Then, use the "planner" agent to create an implementation plan for "$@" using the context from the previous step (use {previous} placeholder)
|
||||||
|
|
||||||
|
Execute this as a chain, passing output between steps via {previous}. Do NOT implement - just return the plan.
|
||||||
Generated
+4040
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "pi-custom-extensions",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@earendil-works/pi-agent-core": "^0.74.0",
|
||||||
|
"@earendil-works/pi-ai": "^0.74.0",
|
||||||
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
||||||
|
"@earendil-works/pi-tui": "^0.74.0",
|
||||||
|
"typebox": "^1.1.24",
|
||||||
|
"@mozilla/readability": "^0.6.0",
|
||||||
|
"jsdom": "^27.0.1",
|
||||||
|
"turndown": "^7.2.2",
|
||||||
|
"turndown-plugin-gfm": "^1.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"lastChangelogVersion": "0.74.0",
|
||||||
|
"defaultProvider": "openai-codex",
|
||||||
|
"defaultModel": "gpt-5.3-codex",
|
||||||
|
"packages": [
|
||||||
|
"npm:@sherif-fanous/pi-catppuccin"
|
||||||
|
],
|
||||||
|
"theme": "catppuccin-frappe",
|
||||||
|
"editorPaddingX": 0,
|
||||||
|
"defaultThinkingLevel": "medium"
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
name: brave-search
|
||||||
|
description: Web search and content extraction via Brave Search API. Use for searching documentation, facts, or any web content. Lightweight, no browser required.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Brave Search
|
||||||
|
|
||||||
|
Web search and content extraction using the official Brave Search API. No browser required.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Requires a Brave Search API account with a free subscription. A credit card is required to create the free subscription (you won't be charged).
|
||||||
|
|
||||||
|
1. Create an account at https://api-dashboard.search.brave.com/register
|
||||||
|
2. Create a "Free AI" subscription
|
||||||
|
3. Create an API key for the subscription
|
||||||
|
4. Add to your shell profile (`~/.profile` or `~/.zprofile` for zsh):
|
||||||
|
```bash
|
||||||
|
export BRAVE_API_KEY="your-api-key-here"
|
||||||
|
```
|
||||||
|
5. Install shared dependencies (run once):
|
||||||
|
```bash
|
||||||
|
cd ~/.pi/agent/extensions
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Search
|
||||||
|
|
||||||
|
```bash
|
||||||
|
{baseDir}/search.js "query" # Basic search (5 results)
|
||||||
|
{baseDir}/search.js "query" -n 10 # More results (max 20)
|
||||||
|
{baseDir}/search.js "query" --content # Include page content as markdown
|
||||||
|
{baseDir}/search.js "query" --freshness pw # Results from last week
|
||||||
|
{baseDir}/search.js "query" --freshness 2024-01-01to2024-06-30 # Date range
|
||||||
|
{baseDir}/search.js "query" --country DE # Results from Germany
|
||||||
|
{baseDir}/search.js "query" -n 3 --content # Combined options
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
- `-n <num>` - Number of results (default: 5, max: 20)
|
||||||
|
- `--content` - Fetch and include page content as markdown
|
||||||
|
- `--country <code>` - Two-letter country code (default: US)
|
||||||
|
- `--freshness <period>` - Filter by time:
|
||||||
|
- `pd` - Past day (24 hours)
|
||||||
|
- `pw` - Past week
|
||||||
|
- `pm` - Past month
|
||||||
|
- `py` - Past year
|
||||||
|
- `YYYY-MM-DDtoYYYY-MM-DD` - Custom date range
|
||||||
|
|
||||||
|
## Extract Page Content
|
||||||
|
|
||||||
|
```bash
|
||||||
|
{baseDir}/content.js https://example.com/article
|
||||||
|
```
|
||||||
|
|
||||||
|
Fetches a URL and extracts readable content as markdown.
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
```
|
||||||
|
--- Result 1 ---
|
||||||
|
Title: Page Title
|
||||||
|
Link: https://example.com/page
|
||||||
|
Age: 2 days ago
|
||||||
|
Snippet: Description from search results
|
||||||
|
Content: (if --content flag used)
|
||||||
|
Markdown content extracted from the page...
|
||||||
|
|
||||||
|
--- Result 2 ---
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Searching for documentation or API references
|
||||||
|
- Looking up facts or current information
|
||||||
|
- Fetching content from specific URLs
|
||||||
|
- Any task requiring web search without interactive browsing
|
||||||
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const baseDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const sharedNodeModules = path.resolve(baseDir, "../../extensions/node_modules");
|
||||||
|
const resolveFromShared = (pkg) => require.resolve(pkg, { paths: [sharedNodeModules] });
|
||||||
|
|
||||||
|
const { Readability } = require(resolveFromShared("@mozilla/readability"));
|
||||||
|
const { JSDOM } = require(resolveFromShared("jsdom"));
|
||||||
|
const TurndownService = require(resolveFromShared("turndown"));
|
||||||
|
const { gfm } = require(resolveFromShared("turndown-plugin-gfm"));
|
||||||
|
|
||||||
|
const url = process.argv[2];
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
console.log("Usage: content.js <url>");
|
||||||
|
console.log("\nExtracts readable content from a webpage as markdown.");
|
||||||
|
console.log("\nExamples:");
|
||||||
|
console.log(" content.js https://example.com/article");
|
||||||
|
console.log(" content.js https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function htmlToMarkdown(html) {
|
||||||
|
const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
|
||||||
|
turndown.use(gfm);
|
||||||
|
turndown.addRule("removeEmptyLinks", {
|
||||||
|
filter: (node) => node.nodeName === "A" && !node.textContent?.trim(),
|
||||||
|
replacement: () => "",
|
||||||
|
});
|
||||||
|
return turndown
|
||||||
|
.turndown(html)
|
||||||
|
.replace(/\[\\?\[\s*\\?\]\]\([^)]*\)/g, "")
|
||||||
|
.replace(/ +/g, " ")
|
||||||
|
.replace(/\s+,/g, ",")
|
||||||
|
.replace(/\s+\./g, ".")
|
||||||
|
.replace(/\n{3,}/g, "\n\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
const dom = new JSDOM(html, { url });
|
||||||
|
const reader = new Readability(dom.window.document);
|
||||||
|
const article = reader.parse();
|
||||||
|
|
||||||
|
if (article && article.content) {
|
||||||
|
if (article.title) {
|
||||||
|
console.log(`# ${article.title}\n`);
|
||||||
|
}
|
||||||
|
console.log(htmlToMarkdown(article.content));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try to extract main content
|
||||||
|
const fallbackDoc = new JSDOM(html, { url });
|
||||||
|
const body = fallbackDoc.window.document;
|
||||||
|
body.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach(el => el.remove());
|
||||||
|
|
||||||
|
const title = body.querySelector("title")?.textContent?.trim();
|
||||||
|
const main = body.querySelector("main, article, [role='main'], .content, #content") || body.body;
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
console.log(`# ${title}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = main?.innerHTML || "";
|
||||||
|
if (text.trim().length > 100) {
|
||||||
|
console.log(htmlToMarkdown(text));
|
||||||
|
} else {
|
||||||
|
console.error("Could not extract readable content from this page.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error: ${e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
Executable
+208
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const baseDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const sharedNodeModules = path.resolve(baseDir, "../../extensions/node_modules");
|
||||||
|
const resolveFromShared = (pkg) => require.resolve(pkg, { paths: [sharedNodeModules] });
|
||||||
|
|
||||||
|
const { Readability } = require(resolveFromShared("@mozilla/readability"));
|
||||||
|
const { JSDOM } = require(resolveFromShared("jsdom"));
|
||||||
|
const TurndownService = require(resolveFromShared("turndown"));
|
||||||
|
const { gfm } = require(resolveFromShared("turndown-plugin-gfm"));
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
const contentIndex = args.indexOf("--content");
|
||||||
|
const fetchContent = contentIndex !== -1;
|
||||||
|
if (fetchContent) args.splice(contentIndex, 1);
|
||||||
|
|
||||||
|
let numResults = 5;
|
||||||
|
const nIndex = args.indexOf("-n");
|
||||||
|
if (nIndex !== -1 && args[nIndex + 1]) {
|
||||||
|
numResults = parseInt(args[nIndex + 1], 10);
|
||||||
|
args.splice(nIndex, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse country option
|
||||||
|
let country = "US";
|
||||||
|
const countryIndex = args.indexOf("--country");
|
||||||
|
if (countryIndex !== -1 && args[countryIndex + 1]) {
|
||||||
|
country = args[countryIndex + 1].toUpperCase();
|
||||||
|
args.splice(countryIndex, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse freshness option
|
||||||
|
let freshness = null;
|
||||||
|
const freshnessIndex = args.indexOf("--freshness");
|
||||||
|
if (freshnessIndex !== -1 && args[freshnessIndex + 1]) {
|
||||||
|
freshness = args[freshnessIndex + 1];
|
||||||
|
args.splice(freshnessIndex, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = args.join(" ");
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
console.log("Usage: search.js <query> [-n <num>] [--content] [--country <code>] [--freshness <period>]");
|
||||||
|
console.log("\nOptions:");
|
||||||
|
console.log(" -n <num> Number of results (default: 5, max: 20)");
|
||||||
|
console.log(" --content Fetch readable content as markdown");
|
||||||
|
console.log(" --country <code> Country code for results (default: US)");
|
||||||
|
console.log(" --freshness <period> Filter by time: pd (day), pw (week), pm (month), py (year)");
|
||||||
|
console.log("\nEnvironment:");
|
||||||
|
console.log(" BRAVE_API_KEY Required. Your Brave Search API key.");
|
||||||
|
console.log("\nExamples:");
|
||||||
|
console.log(' search.js "javascript async await"');
|
||||||
|
console.log(' search.js "rust programming" -n 10');
|
||||||
|
console.log(' search.js "climate change" --content');
|
||||||
|
console.log(' search.js "news today" --freshness pd');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = process.env.BRAVE_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error("Error: BRAVE_API_KEY environment variable is required.");
|
||||||
|
console.error("Get your API key at: https://api-dashboard.search.brave.com/app/keys");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBraveResults(query, numResults, country, freshness) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: query,
|
||||||
|
count: Math.min(numResults, 20).toString(),
|
||||||
|
country: country,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (freshness) {
|
||||||
|
params.append("freshness", freshness);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.search.brave.com/res/v1/web/search?${params.toString()}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
|
"X-Subscription-Token": apiKey,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}\n${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
// Extract web results
|
||||||
|
if (data.web && data.web.results) {
|
||||||
|
for (const result of data.web.results) {
|
||||||
|
if (results.length >= numResults) break;
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
title: result.title || "",
|
||||||
|
link: result.url || "",
|
||||||
|
snippet: result.description || "",
|
||||||
|
age: result.age || result.page_age || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function htmlToMarkdown(html) {
|
||||||
|
const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
|
||||||
|
turndown.use(gfm);
|
||||||
|
turndown.addRule("removeEmptyLinks", {
|
||||||
|
filter: (node) => node.nodeName === "A" && !node.textContent?.trim(),
|
||||||
|
replacement: () => "",
|
||||||
|
});
|
||||||
|
return turndown
|
||||||
|
.turndown(html)
|
||||||
|
.replace(/\[\\?\[\s*\\?\]\]\([^)]*\)/g, "")
|
||||||
|
.replace(/ +/g, " ")
|
||||||
|
.replace(/\s+,/g, ",")
|
||||||
|
.replace(/\s+\./g, ".")
|
||||||
|
.replace(/\n{3,}/g, "\n\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPageContent(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return `(HTTP ${response.status})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
const dom = new JSDOM(html, { url });
|
||||||
|
const reader = new Readability(dom.window.document);
|
||||||
|
const article = reader.parse();
|
||||||
|
|
||||||
|
if (article && article.content) {
|
||||||
|
return htmlToMarkdown(article.content).substring(0, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try to get main content
|
||||||
|
const fallbackDoc = new JSDOM(html, { url });
|
||||||
|
const body = fallbackDoc.window.document;
|
||||||
|
body.querySelectorAll("script, style, noscript, nav, header, footer, aside").forEach(el => el.remove());
|
||||||
|
const main = body.querySelector("main, article, [role='main'], .content, #content") || body.body;
|
||||||
|
const text = main?.textContent || "";
|
||||||
|
|
||||||
|
if (text.trim().length > 100) {
|
||||||
|
return text.trim().substring(0, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "(Could not extract content)";
|
||||||
|
} catch (e) {
|
||||||
|
return `(Error: ${e.message})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main
|
||||||
|
try {
|
||||||
|
const results = await fetchBraveResults(query, numResults, country, freshness);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
console.error("No results found.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetchContent) {
|
||||||
|
for (const result of results) {
|
||||||
|
result.content = await fetchPageContent(result.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < results.length; i++) {
|
||||||
|
const r = results[i];
|
||||||
|
console.log(`--- Result ${i + 1} ---`);
|
||||||
|
console.log(`Title: ${r.title}`);
|
||||||
|
console.log(`Link: ${r.link}`);
|
||||||
|
if (r.age) {
|
||||||
|
console.log(`Age: ${r.age}`);
|
||||||
|
}
|
||||||
|
console.log(`Snippet: ${r.snippet}`);
|
||||||
|
if (r.content) {
|
||||||
|
console.log(`Content:\n${r.content}`);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error: ${e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
---
|
||||||
|
name: browser
|
||||||
|
description: Use cmux browser automation for browser access. Validate real user-facing behavior in browser surfaces, including navigation, DOM interaction, inspection, storage, tabs, dialogs, frames, downloads, and browser logs.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Browser automation with cmux
|
||||||
|
|
||||||
|
Use this skill for browser-driven validation through `cmux browser`.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Verify real user-facing behavior.
|
||||||
|
- Access and inspect web pages.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
Use this skill when the task involves any of these:
|
||||||
|
|
||||||
|
- Opening or navigating a page
|
||||||
|
- Testing a UI flow end to end
|
||||||
|
- Filling forms or clicking controls
|
||||||
|
- Verifying visible text, URL, title, values, attributes, or counts
|
||||||
|
- Capturing snapshots or screenshots
|
||||||
|
- Checking console logs or browser errors
|
||||||
|
- Working with cookies, storage, or saved browser state
|
||||||
|
- Handling tabs, dialogs, iframes, or downloads
|
||||||
|
|
||||||
|
Do not use this skill for unit tests, static analysis, or API-only checks unless browser behavior is part of the task.
|
||||||
|
|
||||||
|
## Operating rules
|
||||||
|
|
||||||
|
1. Start by identifying or opening the browser surface you will use.
|
||||||
|
2. Wait for a stable state before interacting.
|
||||||
|
3. Prefer stable selectors and structured getters over visual guesswork.
|
||||||
|
4. After each mutating action, verify the result with URL, text, visibility, or value checks.
|
||||||
|
5. Treat `console` and `errors` output as evidence, not optional noise.
|
||||||
|
6. On failure, collect artifacts before concluding root cause.
|
||||||
|
7. Use `eval` only when the browser commands cannot express the check directly.
|
||||||
|
8. Keep credentials and secrets out of logs, commands, screenshots, and saved state when possible.
|
||||||
|
|
||||||
|
## Verified command surface
|
||||||
|
|
||||||
|
The commands below are verified against the official cmux browser automation docs.
|
||||||
|
|
||||||
|
### Targeting a browser surface
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser open https://example.com
|
||||||
|
cmux browser open-split https://example.com
|
||||||
|
cmux browser identify
|
||||||
|
cmux browser identify --surface surface:2
|
||||||
|
cmux browser surface:2 url
|
||||||
|
cmux browser --surface surface:2 url
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation and focus
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:2 navigate https://example.com/docs --snapshot-after
|
||||||
|
cmux browser surface:2 back
|
||||||
|
cmux browser surface:2 forward
|
||||||
|
cmux browser surface:2 reload --snapshot-after
|
||||||
|
cmux browser surface:2 focus-webview
|
||||||
|
cmux browser surface:2 is-webview-focused
|
||||||
|
```
|
||||||
|
|
||||||
|
### Waiting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:2 wait --load-state complete --timeout-ms 15000
|
||||||
|
cmux browser surface:2 wait --selector "#checkout" --timeout-ms 10000
|
||||||
|
cmux browser surface:2 wait --text "Order confirmed"
|
||||||
|
cmux browser surface:2 wait --url-contains "/dashboard"
|
||||||
|
cmux browser surface:2 wait --function "window.__appReady === true"
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer this order of readiness checks:
|
||||||
|
|
||||||
|
1. `--selector` when a specific element gates the next action
|
||||||
|
2. `--text` for user-visible confirmation
|
||||||
|
3. `--url-contains` for navigation assertions
|
||||||
|
4. `--function` only when the app exposes a reliable readiness flag
|
||||||
|
5. `--load-state complete` for initial page load or simple pages
|
||||||
|
|
||||||
|
Avoid fixed sleeps unless there is no reliable signal.
|
||||||
|
|
||||||
|
### DOM interaction
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:2 click "button[type='submit']" --snapshot-after
|
||||||
|
cmux browser surface:2 dblclick ".item-row"
|
||||||
|
cmux browser surface:2 hover "#menu"
|
||||||
|
cmux browser surface:2 focus "#email"
|
||||||
|
cmux browser surface:2 check "#terms"
|
||||||
|
cmux browser surface:2 uncheck "#newsletter"
|
||||||
|
cmux browser surface:2 scroll-into-view "#pricing"
|
||||||
|
cmux browser surface:2 type "#search" "cmux"
|
||||||
|
cmux browser surface:2 fill "#email" --text "ops@example.com"
|
||||||
|
cmux browser surface:2 press Enter
|
||||||
|
cmux browser surface:2 keydown Shift
|
||||||
|
cmux browser surface:2 keyup Shift
|
||||||
|
cmux browser surface:2 select "#region" "us-east"
|
||||||
|
cmux browser surface:2 scroll --dy 800 --snapshot-after
|
||||||
|
cmux browser surface:2 scroll --selector "#log-view" --dx 0 --dy 400
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer `fill` when setting a known final value.
|
||||||
|
|
||||||
|
Prefer `type` when testing keystroke-driven behavior such as debouncing, masking, shortcuts, or suggestions.
|
||||||
|
|
||||||
|
### Inspection and assertions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:2 snapshot --interactive --compact
|
||||||
|
cmux browser surface:2 snapshot --selector "main" --max-depth 5
|
||||||
|
cmux browser surface:2 screenshot --out /tmp/cmux-page.png
|
||||||
|
|
||||||
|
cmux browser surface:2 get title
|
||||||
|
cmux browser surface:2 get url
|
||||||
|
cmux browser surface:2 get text "h1"
|
||||||
|
cmux browser surface:2 get html "main"
|
||||||
|
cmux browser surface:2 get value "#email"
|
||||||
|
cmux browser surface:2 get attr "a.primary" --attr href
|
||||||
|
cmux browser surface:2 get count ".row"
|
||||||
|
cmux browser surface:2 get box "#checkout"
|
||||||
|
cmux browser surface:2 get styles "#total" --property color
|
||||||
|
|
||||||
|
cmux browser surface:2 is visible "#checkout"
|
||||||
|
cmux browser surface:2 is enabled "button[type='submit']"
|
||||||
|
cmux browser surface:2 is checked "#terms"
|
||||||
|
|
||||||
|
cmux browser surface:2 find role button --name "Continue"
|
||||||
|
cmux browser surface:2 find text "Order confirmed"
|
||||||
|
cmux browser surface:2 find label "Email"
|
||||||
|
cmux browser surface:2 find placeholder "Search"
|
||||||
|
cmux browser surface:2 find alt "Product image"
|
||||||
|
cmux browser surface:2 find title "Open settings"
|
||||||
|
cmux browser surface:2 find testid "save-btn"
|
||||||
|
cmux browser surface:2 find first ".row"
|
||||||
|
cmux browser surface:2 find last ".row"
|
||||||
|
cmux browser surface:2 find nth 2 ".row"
|
||||||
|
|
||||||
|
cmux browser surface:2 highlight "#checkout"
|
||||||
|
```
|
||||||
|
|
||||||
|
Preferred selector order:
|
||||||
|
|
||||||
|
1. Stable test IDs
|
||||||
|
2. Accessible role and name
|
||||||
|
3. Labels and placeholders
|
||||||
|
4. Semantic CSS selectors
|
||||||
|
5. Text selectors
|
||||||
|
|
||||||
|
Avoid brittle selectors like deeply nested `nth-child` chains unless no better option exists.
|
||||||
|
|
||||||
|
### JavaScript and injection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:2 eval "document.title"
|
||||||
|
cmux browser surface:2 eval --script "window.location.href"
|
||||||
|
cmux browser surface:2 addinitscript "window.__cmuxReady = true;"
|
||||||
|
cmux browser surface:2 addscript "document.querySelector('#name')?.focus()"
|
||||||
|
cmux browser surface:2 addstyle "#debug-banner { display: none !important; }"
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `eval` sparingly. Do not bypass the UI path if the task is to validate user behavior.
|
||||||
|
|
||||||
|
### State and session data
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:2 cookies get
|
||||||
|
cmux browser surface:2 cookies get --name session_id
|
||||||
|
cmux browser surface:2 cookies set session_id abc123 --domain example.com --path /
|
||||||
|
cmux browser surface:2 cookies clear --name session_id
|
||||||
|
cmux browser surface:2 cookies clear --all
|
||||||
|
|
||||||
|
cmux browser surface:2 storage local set theme dark
|
||||||
|
cmux browser surface:2 storage local get theme
|
||||||
|
cmux browser surface:2 storage local clear
|
||||||
|
cmux browser surface:2 storage session set flow onboarding
|
||||||
|
cmux browser surface:2 storage session get flow
|
||||||
|
|
||||||
|
cmux browser surface:2 state save /tmp/cmux-browser-state.json
|
||||||
|
cmux browser surface:2 state load /tmp/cmux-browser-state.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Use saved browser state for authenticated sessions, long setup flows, and repeatable bug repros.
|
||||||
|
|
||||||
|
Do not use saved state when validating fresh-session behavior like login, onboarding, logout, or first-run UX.
|
||||||
|
|
||||||
|
### Tabs, logs, dialogs, frames, downloads
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:2 tab list
|
||||||
|
cmux browser surface:2 tab new https://example.com/pricing
|
||||||
|
cmux browser surface:2 tab switch 1
|
||||||
|
cmux browser surface:2 tab switch surface:7
|
||||||
|
cmux browser surface:2 tab close
|
||||||
|
cmux browser surface:2 tab close surface:7
|
||||||
|
|
||||||
|
cmux browser surface:2 console list
|
||||||
|
cmux browser surface:2 console clear
|
||||||
|
cmux browser surface:2 errors list
|
||||||
|
cmux browser surface:2 errors clear
|
||||||
|
|
||||||
|
cmux browser surface:2 dialog accept
|
||||||
|
cmux browser surface:2 dialog accept "Confirmed by automation"
|
||||||
|
cmux browser surface:2 dialog dismiss
|
||||||
|
|
||||||
|
cmux browser surface:2 frame "iframe[name='checkout']"
|
||||||
|
cmux browser surface:2 frame main
|
||||||
|
|
||||||
|
cmux browser surface:2 download --path /tmp/report.csv --timeout-ms 30000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Execution playbooks
|
||||||
|
|
||||||
|
### Default flow
|
||||||
|
|
||||||
|
Use this decision sequence unless the task clearly needs a different one.
|
||||||
|
|
||||||
|
1. Acquire a surface.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser open <URL>
|
||||||
|
cmux browser identify
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Establish readiness.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:<ID> wait --load-state complete --timeout-ms 15000
|
||||||
|
cmux browser surface:<ID> snapshot --interactive --compact
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Choose the next action type.
|
||||||
|
|
||||||
|
- Navigation task: use `navigate`, `back`, `forward`, or `reload`, then wait again.
|
||||||
|
- Form task: use the form playbook below.
|
||||||
|
- Inspection task: use `get`, `is`, `find`, `snapshot`, or `screenshot`.
|
||||||
|
- State setup task: use `cookies`, `storage`, or `state` before continuing.
|
||||||
|
- Download, dialog, tab, or frame task: switch to the relevant specialized playbook.
|
||||||
|
|
||||||
|
4. After every mutating action, verify with at least one explicit assertion.
|
||||||
|
|
||||||
|
Preferred assertion order:
|
||||||
|
|
||||||
|
1. `wait --url-contains`
|
||||||
|
2. `wait --text`
|
||||||
|
3. `is visible`
|
||||||
|
4. `get value`
|
||||||
|
5. `get count`
|
||||||
|
6. `errors list` when debugging or validating stability
|
||||||
|
|
||||||
|
7. If the assertion fails, run the failure protocol before making claims.
|
||||||
|
|
||||||
|
### Form playbook
|
||||||
|
|
||||||
|
Use this for login, signup, checkout, search, and settings forms.
|
||||||
|
|
||||||
|
1. Wait for the first required field.
|
||||||
|
2. Populate fields with `fill` unless the task is specifically about typing behavior.
|
||||||
|
3. Submit with `click`, `press Enter`, or the control the user would actually use.
|
||||||
|
4. Verify success using URL, text, and visibility checks.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:<ID> wait --selector "#email" --timeout-ms 10000
|
||||||
|
cmux browser surface:<ID> fill "#email" --text "$TEST_EMAIL"
|
||||||
|
cmux browser surface:<ID> fill "#password" --text "$TEST_PASSWORD"
|
||||||
|
cmux browser surface:<ID> click "button[type='submit']" --snapshot-after
|
||||||
|
cmux browser surface:<ID> wait --url-contains "/dashboard" --timeout-ms 10000
|
||||||
|
cmux browser surface:<ID> is visible "#dashboard"
|
||||||
|
```
|
||||||
|
|
||||||
|
If submission should fail, verify the expected error text or blocked state instead of forcing success assertions.
|
||||||
|
|
||||||
|
### Navigation playbook
|
||||||
|
|
||||||
|
Use this when the task is about routing, links, history, or page transitions.
|
||||||
|
|
||||||
|
1. Trigger navigation.
|
||||||
|
2. Wait for URL or page content to settle.
|
||||||
|
3. Verify the destination.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:<ID> navigate https://example.com/docs --snapshot-after
|
||||||
|
cmux browser surface:<ID> wait --url-contains "/docs" --timeout-ms 10000
|
||||||
|
cmux browser surface:<ID> get title
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inspection playbook
|
||||||
|
|
||||||
|
Use this when the task is observational rather than interactive.
|
||||||
|
|
||||||
|
1. Prefer `find` to discover the right selector.
|
||||||
|
2. Use `get` or `is` for structured assertions.
|
||||||
|
3. Use `snapshot` or `screenshot` only when human review is useful.
|
||||||
|
|
||||||
|
### State playbook
|
||||||
|
|
||||||
|
Use this when the task depends on auth, persisted preferences, or reproducible setup.
|
||||||
|
|
||||||
|
1. Decide whether the task should start fresh or with persisted state.
|
||||||
|
2. If fresh-session behavior matters, do not load saved state.
|
||||||
|
3. If setup reuse is justified, use `cookies`, `storage`, or `state load`.
|
||||||
|
4. After state changes, reload or navigate as needed and verify the expected state is visible.
|
||||||
|
|
||||||
|
### Tabs, dialogs, frames, and downloads playbook
|
||||||
|
|
||||||
|
- Tabs: `tab list`, `tab new`, `tab switch`, then verify with `get url` or visible text.
|
||||||
|
- Dialogs: trigger the dialog, then immediately `dialog accept` or `dialog dismiss`.
|
||||||
|
- Frames: enter with `frame <selector>`, complete the work, then return with `frame main`.
|
||||||
|
- Downloads: trigger the download, run `download --path ...`, then verify the file with shell tools if needed.
|
||||||
|
|
||||||
|
## Failure protocol
|
||||||
|
|
||||||
|
Run this whenever a flow fails or results are ambiguous:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cmux browser surface:<ID> console list
|
||||||
|
cmux browser surface:<ID> errors list
|
||||||
|
cmux browser surface:<ID> screenshot --out /tmp/cmux-failure.png
|
||||||
|
cmux browser surface:<ID> snapshot --interactive --compact
|
||||||
|
cmux browser surface:<ID> get url
|
||||||
|
cmux browser surface:<ID> get title
|
||||||
|
```
|
||||||
|
|
||||||
|
Report all of these if available:
|
||||||
|
|
||||||
|
- Failed action
|
||||||
|
- Expected behavior
|
||||||
|
- Actual behavior
|
||||||
|
- Current URL
|
||||||
|
- Current title
|
||||||
|
- Relevant visible text
|
||||||
|
- Console findings
|
||||||
|
- Browser error findings
|
||||||
|
- Artifact paths
|
||||||
|
|
||||||
|
## Flaky triage
|
||||||
|
|
||||||
|
If an interaction fails, use this exact order:
|
||||||
|
|
||||||
|
1. Check existence: `cmux browser surface:<ID> get count "<selector>"`
|
||||||
|
2. Check visibility: `cmux browser surface:<ID> is visible "<selector>"`
|
||||||
|
3. Check enabled state: `cmux browser surface:<ID> is enabled "<selector>"`
|
||||||
|
4. Scroll into view: `cmux browser surface:<ID> scroll-into-view "<selector>"`
|
||||||
|
5. Retry the action once
|
||||||
|
6. If it still fails, stop retrying and run the failure protocol
|
||||||
|
|
||||||
|
## Reporting format
|
||||||
|
|
||||||
|
Use this concise result format:
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Browser Test Result
|
||||||
|
|
||||||
|
Status: PASS | FAIL | BLOCKED
|
||||||
|
Tested URL: <url>
|
||||||
|
Scenario: <what was tested>
|
||||||
|
Result: <one-sentence outcome>
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
|
||||||
|
- <assertion or command result>
|
||||||
|
- <assertion or command result>
|
||||||
|
|
||||||
|
Artifacts:
|
||||||
|
|
||||||
|
- Screenshot: <path or none>
|
||||||
|
- Snapshot: <brief summary or none>
|
||||||
|
- Console/errors: <brief summary>
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
<caveats, blockers, or likely cause if supported by evidence>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common mistakes
|
||||||
|
|
||||||
|
- Interacting before the page or element is ready
|
||||||
|
- Assuming navigation succeeded without verifying URL or text
|
||||||
|
- Using brittle selectors when stable ones exist
|
||||||
|
- Treating `eval` as a substitute for user behavior
|
||||||
|
- Ignoring `console` or `errors` output during failures
|
||||||
|
- Reusing saved state in tests that should start fresh
|
||||||
|
- Reporting success without at least one explicit assertion
|
||||||
|
|
||||||
|
## Gotcha: implicit Enter form submission
|
||||||
|
|
||||||
|
Some forms rely on the browser’s default behavior where pressing Enter in an input submits the form. In `cmux`, `fill`, `focus`, and `press Enter` may all appear to succeed without actually triggering submission.
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Never assume Enter submitted the form just because `press Enter` returned `OK`.
|
||||||
|
- Always verify submission through URL change, visible success state, or expected content.
|
||||||
|
- If Enter does not submit and there are no console/browser errors, suspect an automation limitation.
|
||||||
|
- Retry once with `focus`, `focus-webview`, and `type "...\\n"`.
|
||||||
|
- If still unsuccessful, validate the feature through the expected destination state and report that implicit Enter submission could not be reliably proven in `cmux`.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: chromy
|
||||||
|
description: This skill provides access to a RAG-like context enhancer that uses Chromadb locally.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Chromy
|
||||||
|
|
||||||
|
Whenever the user asks to "use chromy", you should invoke `chromy`, which is a cli tool to perform RAG search.
|
||||||
|
The tool should be available in the `$PATH` as `chromy`.
|
||||||
|
|
||||||
|
You have access to these commands:
|
||||||
|
|
||||||
|
- `$ chromy list-collections` -> Lists the existing collections.
|
||||||
|
- `$ chromy query <collection> <query>` -> Performs a query. Be sure to quote the `<query>` if this is composed by multiple words.
|
||||||
|
|
||||||
|
Then use the response from Chromy to enhance the context and give the user a refined response.
|
||||||
|
|
||||||
|
## A note on file sources
|
||||||
|
|
||||||
|
The Chromy response returns the metadatas for the chunks it finds. Among these metadatas, there is `file_name`, which refers to the original file that was chunked and imported. **DO NOT ATTEMPT** to find or fetch these files. They most likely do not exist in the filesystem. You **SHOULD ALWAYS** however cite correctly from which files (**ONLY** from Chromy's metadatas) the information is coming.
|
||||||
|
|
||||||
|
## Example use case
|
||||||
|
|
||||||
|
**START**
|
||||||
|
|
||||||
|
User query:
|
||||||
|
|
||||||
|
> Search in Chromy information about lovecraft's Dunwich horror.
|
||||||
|
|
||||||
|
Step 1: Get the available collections with `chromy list-collections`. The output is:
|
||||||
|
|
||||||
|
```
|
||||||
|
lovecraft
|
||||||
|
documents
|
||||||
|
```
|
||||||
|
|
||||||
|
Most likely our information is in the `lovecraft` collection. We will use that for the query.
|
||||||
|
|
||||||
|
Step 2: Query using `chromy query lovecraft <query>`. The query _is up to you_, create one keeping into account that this is a raw query on a vector DB. Be concise, extract keywords, avoid noise.
|
||||||
|
|
||||||
|
Step 3: Get the results, enhance the context, and respond to the user.
|
||||||
|
|
||||||
|
**END**
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: codebase-analysis
|
||||||
|
description: Analyze a codebase to understand architecture, dependencies, runnable tasks and runtime information.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Codebase Analysis Skill
|
||||||
|
|
||||||
|
Use this skill when asked to inspect, explain, refactor, extend, debug, or assess an unfamiliar codebase.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Build a concise mental model of the codebase.
|
||||||
|
- Identify entry points, core modules, data flow, and external dependencies.
|
||||||
|
- Identify all existing runnable scripts (ex: run the dev server, run in production, etc.)
|
||||||
|
- Surface risks, conventions, and likely impact areas.
|
||||||
|
- Produce actionable findings before proposing code changes.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. Inspect the codebase structure.
|
||||||
|
2. Read top-level docs: `README`, `AGENTS`, architecture notes, contributing docs, and setup files.
|
||||||
|
3. Identify languages, frameworks, package managers, build tools, test and run commands.
|
||||||
|
4. Locate application entry points and configuration files.
|
||||||
|
5. Trace the relevant feature path from input to output.
|
||||||
|
6. Check tests, fixtures, CI workflows, and lint/type settings.
|
||||||
|
7. Summarize findings with file references and uncertainty.
|
||||||
|
|
||||||
|
## Heuristics
|
||||||
|
|
||||||
|
- Prefer reading existing conventions over inventing new patterns.
|
||||||
|
- Do not assume framework behavior when config or code contradicts defaults.
|
||||||
|
- Treat generated, vendored, minified, and lock files as supporting evidence only.
|
||||||
|
- Search for similar implementations before designing a new one.
|
||||||
|
- Verify public APIs, side effects, and persistence boundaries before editing.
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
Provide:
|
||||||
|
|
||||||
|
- `Overview`: what the codebase does.
|
||||||
|
- `Architecture`: main components and how they interact.
|
||||||
|
- `Relevant files`: files inspected and why they matter.
|
||||||
|
- `Execution path`: request/event/job flow, if applicable.
|
||||||
|
- `Dependencies`: important internal and external dependencies.
|
||||||
|
- `Risks`: fragile areas, missing tests, unclear behavior.
|
||||||
|
- `Next steps`: recommended investigation or implementation plan.
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
- Do not make broad claims without file evidence.
|
||||||
|
- Mark guesses explicitly as assumptions.
|
||||||
|
- Do not edit code until the impact area is understood.
|
||||||
|
- Keep the summary concise and focused on the user’s task.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: context7
|
||||||
|
description: Get code snippet examples, get inline help about any library or language, find documentation using Context7 API.
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
Simply invoke the `main.py` script using `uv` and passing as command line arguments the `<library>` and the `<query>`. Make sure to use quotations if the query is composed by more than one word. The script will return useful code snippets about what has been requested.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ uv run main.py django "how to run django migrations?"
|
||||||
|
|
||||||
|
[OUTPUT]
|
||||||
|
```
|
||||||
|
|
||||||
|
This skill uses Python and `uv` to run: before using the skill, the virtual environment must be ready and the libraries must be installed.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv venv --allow-existing
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
This skill must be used whenever there is the need to get additional context for the specific library or language that is in use in the project. If the project is using, for example, React, it might be useful to use it with `react` and `"how to use the the useEffect hook?"`, or if using Pydantic with `pydantic` and `"how to define an email field?"`.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
CONTEXT7_API_KEY = os.environ.get("CONTEXT7_API_KEY")
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {CONTEXT7_API_KEY}"}
|
||||||
|
|
||||||
|
|
||||||
|
def search_library(library: str, query: str) -> str:
|
||||||
|
# Step 1: Search for the library
|
||||||
|
search_response = requests.get(
|
||||||
|
"https://context7.com/api/v2/libs/search",
|
||||||
|
headers=headers,
|
||||||
|
params={"libraryName": library, "query": query},
|
||||||
|
)
|
||||||
|
search_response.raise_for_status()
|
||||||
|
data = search_response.json()
|
||||||
|
best_match = data["results"][0]
|
||||||
|
print(f"Found: {best_match['title']} ({best_match['id']})")
|
||||||
|
return best_match["id"]
|
||||||
|
|
||||||
|
|
||||||
|
def search_snippet(library: str, query: str) -> None:
|
||||||
|
# Step 2: Get documentation context
|
||||||
|
context_response = requests.get(
|
||||||
|
"https://context7.com/api/v2/context",
|
||||||
|
headers=headers,
|
||||||
|
params={
|
||||||
|
"libraryId": library,
|
||||||
|
"query": query,
|
||||||
|
"type": "json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
context_response.raise_for_status()
|
||||||
|
docs = context_response.json()
|
||||||
|
|
||||||
|
for snippet in docs["codeSnippets"]:
|
||||||
|
print(f"Title: {snippet['codeTitle']}")
|
||||||
|
for code in snippet["codeList"]:
|
||||||
|
print(f"Code: {code['code']}")
|
||||||
|
|
||||||
|
for info in docs["infoSnippets"]:
|
||||||
|
print(f"Content: {info['content']}")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("library", help="Library name to search for")
|
||||||
|
parser.add_argument("query", help="Documentation query to run")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
library_id = search_library(args.library, args.query)
|
||||||
|
search_snippet(library_id, args.query)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[project]
|
||||||
|
name = "context7"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"requests>=2.33.1",
|
||||||
|
]
|
||||||
Generated
+129
@@ -0,0 +1,129 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.4.22"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "context7"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "requests", specifier = ">=2.33.1" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.13"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.33.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.6.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||||
|
]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: dev-server
|
||||||
|
description: Instructions on how to run correctly development / preview servers or background processes in new panes in cmux.
|
||||||
|
---
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
|
||||||
|
This skill is active when the user asks directly or the agent wants to run a local dev/preview server or any other long running command that ideally needs to run in the background.
|
||||||
|
The idea is to provide a dedicated cmux pane / split where to execute this task, and skip entirely the background execution in the coding agent.
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
1. Identify what is the task to run. Examples are `npm run dev`, `uv run manage.py runserver`, `python -m http.server` and so on.
|
||||||
|
2. Create a new split in cmux. By default we want to split right: `cmux new-split right`.
|
||||||
|
3. After the split is created, it will return some information like so: `OK surface:23 workspace:5`.
|
||||||
|
4. We want to rename the split to give it a meaningful name, for example "Django dev server" or "npm dev server": `cmux rename-tab --surface surface:23 "npm dev server"`
|
||||||
|
5. Now we need to send the actual command to the split: `cmux send --surface surface:23 "npm run dev"`.
|
||||||
|
6. Lastly, we need to send the <enter> key to trigger the command execution: `cmux send-key --surface surface:23 enter`.
|
||||||
|
|
||||||
|
The external process now is up and running. It will be up to the user to terminate or close the split.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: frontend-design
|
||||||
|
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||||
|
license: Complete terms in LICENSE.txt
|
||||||
|
---
|
||||||
|
|
||||||
|
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||||
|
|
||||||
|
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||||
|
|
||||||
|
## Design Thinking
|
||||||
|
|
||||||
|
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||||
|
|
||||||
|
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||||
|
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||||
|
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||||
|
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||||
|
|
||||||
|
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||||
|
|
||||||
|
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||||
|
|
||||||
|
- Production-grade and functional
|
||||||
|
- Visually striking and memorable
|
||||||
|
- Cohesive with a clear aesthetic point-of-view
|
||||||
|
- Meticulously refined in every detail
|
||||||
|
|
||||||
|
## Frontend Aesthetics Guidelines
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
|
||||||
|
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||||
|
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||||
|
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||||
|
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||||
|
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||||
|
|
||||||
|
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||||
|
|
||||||
|
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||||
|
|
||||||
|
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||||
|
|
||||||
|
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Python Reference
|
||||||
|
|
||||||
|
Use this file only when deeper guidance is needed. `SKILL.md` is the default operating contract.
|
||||||
|
|
||||||
|
## When To Consult This File
|
||||||
|
|
||||||
|
- You are choosing between multiple Python design options.
|
||||||
|
- You need stronger guidance for typing, validation, async, or testing.
|
||||||
|
- You are creating new modules or new project structure rather than making a local edit.
|
||||||
|
- You are reviewing code and want a more complete checklist.
|
||||||
|
|
||||||
|
## Stronger Typing Guidance
|
||||||
|
|
||||||
|
- Prefer `collections.abc` and `typing` abstractions such as `Sequence`, `Mapping`, `Callable`, and `Iterator`.
|
||||||
|
- Use `Protocol` for behavioral contracts when inheritance is not required.
|
||||||
|
- Use `TypedDict` for structured dictionary payloads when a dataclass or Pydantic model is not appropriate.
|
||||||
|
- Use `Final` for constants and `ClassVar` for class-level attributes when helpful.
|
||||||
|
- Keep `Any` rare, local, and justified.
|
||||||
|
- If the project supports it, use `@override` on overriding methods.
|
||||||
|
|
||||||
|
## Architecture Heuristics
|
||||||
|
|
||||||
|
- Domain layer: pure business rules, no framework or persistence code.
|
||||||
|
- Repository or data layer: database and external service access.
|
||||||
|
- Service or use-case layer: orchestration across domain rules and collaborators.
|
||||||
|
- Interface layer: HTTP, CLI, events, serialization, request and response shaping.
|
||||||
|
- Infrastructure layer: configuration, logging setup, DB sessions, clients.
|
||||||
|
|
||||||
|
Use this split when it fits the repository. Do not impose it mechanically on smaller scripts or simpler codebases.
|
||||||
|
|
||||||
|
## Data Modeling
|
||||||
|
|
||||||
|
- Use Pydantic for request payloads, config, and data from untrusted sources.
|
||||||
|
- Use dataclasses for internal value objects and simple structured state.
|
||||||
|
- Use `frozen=True` for values that should not change after creation.
|
||||||
|
- Use `slots=True` where the codebase already prefers it or where many instances are created.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Prefer project-specific exception types over generic `ValueError` or `RuntimeError` when a domain concept exists.
|
||||||
|
- Wrap lower-level exceptions with domain context when crossing layers.
|
||||||
|
- Keep exception handling near boundaries unless recovery is local and explicit.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
- Prefer structured logging when the project supports it.
|
||||||
|
- Log important operational events at service or interface boundaries.
|
||||||
|
- Do not log secrets, credentials, or sensitive user data.
|
||||||
|
- Avoid `print()` for operational behavior unless the program is explicitly a simple script or CLI output path.
|
||||||
|
|
||||||
|
## Async Guidance
|
||||||
|
|
||||||
|
- Use async for I/O-bound workflows, not CPU-bound work.
|
||||||
|
- Keep blocking calls out of coroutines; use thread or process offloading where appropriate.
|
||||||
|
- Prefer `asyncio.TaskGroup` over ad hoc task orchestration when available.
|
||||||
|
- Be explicit about sync-to-async boundaries.
|
||||||
|
|
||||||
|
## Security Guidance
|
||||||
|
|
||||||
|
- Never hardcode secrets or credentials.
|
||||||
|
- Validate all external input before it enters core logic.
|
||||||
|
- Avoid dangerous calls unless explicitly justified and safely constrained:
|
||||||
|
- `eval`, `exec`
|
||||||
|
- `pickle.loads`
|
||||||
|
- `yaml.load`
|
||||||
|
- `subprocess` with `shell=True`
|
||||||
|
- `os.system`
|
||||||
|
- Do not interpolate raw user input into SQL, shell commands, or file-system-sensitive operations.
|
||||||
|
|
||||||
|
## Testing Guidance
|
||||||
|
|
||||||
|
- Update tests whenever behavior changes.
|
||||||
|
- Prefer unit tests for pure logic and integration tests for boundaries.
|
||||||
|
- Use parametrization to improve coverage without duplicating setup.
|
||||||
|
- Test behavior and observable outcomes rather than implementation details.
|
||||||
|
- Mock external boundaries selectively; avoid mocking the core logic you actually want to verify.
|
||||||
|
|
||||||
|
## Review Heuristics
|
||||||
|
|
||||||
|
When reviewing Python changes, prioritize:
|
||||||
|
|
||||||
|
- correctness and regressions
|
||||||
|
- incomplete or misleading typing
|
||||||
|
- invalid assumptions at trust boundaries
|
||||||
|
- misplaced logic between layers
|
||||||
|
- unnecessary abstraction or indirection
|
||||||
|
- missing tests for changed behavior
|
||||||
|
- risky calls, secret handling, or unsafe subprocess use
|
||||||
|
|
||||||
|
## Common Verification Flow
|
||||||
|
|
||||||
|
If the repository uses `uv`, the usual flow is:
|
||||||
|
|
||||||
|
1. `uv run ruff format <paths>`
|
||||||
|
2. `uv run ruff check --fix <paths>`
|
||||||
|
3. `uv run ruff check <paths>`
|
||||||
|
4. `uv run mypy <paths-or-package>`
|
||||||
|
5. `uv run pytest <relevant tests>`
|
||||||
|
|
||||||
|
If the repository uses different commands or tools, follow the repository.
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
name: python-dev
|
||||||
|
description: |
|
||||||
|
Apply this skill whenever writing, editing, generating, or reviewing Python code. Favor small, typed, idiomatic changes that fit the existing codebase and finish with lint, type checks, and relevant tests passing. Use `REFERENCE.md` only when deeper guidance is needed.
|
||||||
|
version: 3.1.0
|
||||||
|
user-invocable: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Python Development
|
||||||
|
|
||||||
|
Use this skill for any task that creates or changes Python code.
|
||||||
|
|
||||||
|
## Operating Mode
|
||||||
|
|
||||||
|
- Match the repository's existing style and architecture before introducing new patterns.
|
||||||
|
- Prefer the smallest correct change over broad refactors.
|
||||||
|
- Keep code easy to read: flat control flow, clear names, limited indirection.
|
||||||
|
- Do not force framework choices or house style upgrades into unrelated work.
|
||||||
|
- Do not add compatibility layers, feature flags, or speculative abstractions unless required.
|
||||||
|
- If an API or library detail is uncertain, check current docs before coding.
|
||||||
|
|
||||||
|
## Defaults
|
||||||
|
|
||||||
|
- Add `from __future__ import annotations` to new Python modules.
|
||||||
|
- Fully annotate public functions and important variables.
|
||||||
|
- Prefer precise types over broad ones.
|
||||||
|
- Use `X | Y` and `X | None` syntax.
|
||||||
|
- Avoid `Any`; if unavoidable, keep it narrow and explain why.
|
||||||
|
- Use `TYPE_CHECKING` for type-only imports when it improves runtime imports or avoids cycles.
|
||||||
|
- Prefer simple functions and dataclasses unless the codebase clearly wants a heavier OO design.
|
||||||
|
- Use classes for durable domain concepts or stateful collaborators, not by default.
|
||||||
|
- Prefer composition over inheritance.
|
||||||
|
- Keep responsibilities separated: domain logic, data access, and interface code should stay distinct.
|
||||||
|
- Do not place business logic directly in route handlers, CLI commands, or persistence code.
|
||||||
|
- Validate untrusted input at the boundary.
|
||||||
|
- Prefer Pydantic for external input, config, or data crossing trust boundaries.
|
||||||
|
- Prefer `@dataclass(slots=True)` for internal structured data.
|
||||||
|
- Use `frozen=True` when immutability is a natural fit.
|
||||||
|
- Prefer `pathlib` over `os.path`.
|
||||||
|
- Prefer f-strings.
|
||||||
|
- Prefer standard idioms like comprehensions, `enumerate`, `zip`, and context managers where appropriate.
|
||||||
|
- Use guard clauses to reduce nesting.
|
||||||
|
- Avoid mutable default arguments, bare `except:`, and `type(x) == T` checks.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Raise specific exceptions with useful messages.
|
||||||
|
- Keep `try` blocks narrow.
|
||||||
|
- Preserve context with `raise ... from exc` when re-raising.
|
||||||
|
- Do not swallow exceptions silently.
|
||||||
|
- Log at system boundaries or orchestration layers, not deep inside pure domain logic.
|
||||||
|
|
||||||
|
## Async
|
||||||
|
|
||||||
|
- Use `async` only for real I/O-bound workflows.
|
||||||
|
- Do not call blocking I/O directly from coroutines.
|
||||||
|
- Keep sync and async boundaries explicit.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Never hardcode secrets, tokens, or credentials.
|
||||||
|
- Validate external input before using it.
|
||||||
|
- Avoid dangerous calls unless explicitly justified: `eval`, `exec`, `pickle.loads`, `yaml.load`, `shell=True`, `os.system`.
|
||||||
|
- Do not build shell commands from raw user input.
|
||||||
|
- Avoid logging secrets or sensitive user data.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Add or update tests for behavior you change.
|
||||||
|
- Prefer unit tests for pure logic and integration tests for persistence or external boundaries.
|
||||||
|
- Use parametrization to cover multiple cases succinctly.
|
||||||
|
- Mock at external boundaries, not inside the core logic under test, unless the repo uses a different testing style.
|
||||||
|
|
||||||
|
## Tooling
|
||||||
|
|
||||||
|
- Use `uv` for Python dependency and tool execution when the project uses it.
|
||||||
|
- Run formatters, linters, and type checks after Python changes.
|
||||||
|
- Default verification flow: format, lint, type-check, then run relevant tests.
|
||||||
|
- If the repo uses different commands, follow the repo.
|
||||||
|
|
||||||
|
## Completion Checklist
|
||||||
|
|
||||||
|
Before finishing Python work, make sure:
|
||||||
|
|
||||||
|
- the change matches existing project patterns
|
||||||
|
- code is fully typed to the repo's standard
|
||||||
|
- untrusted input is validated at the boundary
|
||||||
|
- logic is kept in the right layer
|
||||||
|
- `ruff` is clean
|
||||||
|
- `mypy` is clean
|
||||||
|
- relevant tests pass
|
||||||
|
- no secrets or risky calls were introduced without justification
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
|
||||||
|
See `REFERENCE.md` for optional deeper guidance on typing, architecture, async patterns, testing, security, and review heuristics.
|
||||||
Reference in New Issue
Block a user