pi-code 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +51 -0
- package/extensions/claude-rules.ts +136 -0
- package/extensions/commands.ts +45 -0
- package/extensions/context-imports.ts +118 -0
- package/extensions/git-checkpoint.ts +199 -0
- package/extensions/hooks.ts +178 -0
- package/extensions/mcp.ts +268 -0
- package/extensions/memory.ts +127 -0
- package/extensions/notify.ts +52 -0
- package/extensions/output-styles.ts +159 -0
- package/extensions/plan-mode/README.md +65 -0
- package/extensions/plan-mode/index.ts +352 -0
- package/extensions/plan-mode/utils.ts +175 -0
- package/extensions/question.ts +258 -0
- package/extensions/skills.ts +41 -0
- package/extensions/status-line.ts +59 -0
- package/extensions/subagent/README.md +172 -0
- package/extensions/subagent/agents/planner.md +37 -0
- package/extensions/subagent/agents/reviewer.md +35 -0
- package/extensions/subagent/agents/scout.md +50 -0
- package/extensions/subagent/agents/worker.md +24 -0
- package/extensions/subagent/agents.ts +147 -0
- package/extensions/subagent/background.ts +89 -0
- package/extensions/subagent/index.ts +953 -0
- package/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/extensions/subagent/prompts/implement.md +10 -0
- package/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/extensions/todo.ts +504 -0
- package/extensions/web.ts +195 -0
- package/package.json +59 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planner
|
|
3
|
+
description: Creates implementation plans from context and requirements
|
|
4
|
+
tools: read, grep, find, ls
|
|
5
|
+
model: claude-sonnet-4-5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a planning specialist. You receive context (from a scout) and requirements, then produce a clear implementation plan.
|
|
9
|
+
|
|
10
|
+
You must NOT make any changes. Only read, analyze, and plan.
|
|
11
|
+
|
|
12
|
+
Input format you'll receive:
|
|
13
|
+
- Context/findings from a scout agent
|
|
14
|
+
- Original query or requirements
|
|
15
|
+
|
|
16
|
+
Output format:
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
One sentence summary of what needs to be done.
|
|
20
|
+
|
|
21
|
+
## Plan
|
|
22
|
+
Numbered steps, each small and actionable:
|
|
23
|
+
1. Step one - specific file/function to modify
|
|
24
|
+
2. Step two - what to add/change
|
|
25
|
+
3. ...
|
|
26
|
+
|
|
27
|
+
## Files to Modify
|
|
28
|
+
- `path/to/file.ts` - what changes
|
|
29
|
+
- `path/to/other.ts` - what changes
|
|
30
|
+
|
|
31
|
+
## New Files (if any)
|
|
32
|
+
- `path/to/new.ts` - purpose
|
|
33
|
+
|
|
34
|
+
## Risks
|
|
35
|
+
Anything to watch out for.
|
|
36
|
+
|
|
37
|
+
Keep the plan concrete. The worker agent will execute it verbatim.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Code review specialist for quality and security analysis
|
|
4
|
+
tools: read, grep, find, ls, bash
|
|
5
|
+
model: claude-sonnet-4-5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a senior code reviewer. Analyze code for quality, security, and maintainability.
|
|
9
|
+
|
|
10
|
+
Bash is for read-only commands only: `git diff`, `git log`, `git show`. Do NOT modify files or run builds.
|
|
11
|
+
Assume tool permissions are not perfectly enforceable; keep all bash usage strictly read-only.
|
|
12
|
+
|
|
13
|
+
Strategy:
|
|
14
|
+
1. Run `git diff` to see recent changes (if applicable)
|
|
15
|
+
2. Read the modified files
|
|
16
|
+
3. Check for bugs, security issues, code smells
|
|
17
|
+
|
|
18
|
+
Output format:
|
|
19
|
+
|
|
20
|
+
## Files Reviewed
|
|
21
|
+
- `path/to/file.ts` (lines X-Y)
|
|
22
|
+
|
|
23
|
+
## Critical (must fix)
|
|
24
|
+
- `file.ts:42` - Issue description
|
|
25
|
+
|
|
26
|
+
## Warnings (should fix)
|
|
27
|
+
- `file.ts:100` - Issue description
|
|
28
|
+
|
|
29
|
+
## Suggestions (consider)
|
|
30
|
+
- `file.ts:150` - Improvement idea
|
|
31
|
+
|
|
32
|
+
## Summary
|
|
33
|
+
Overall assessment in 2-3 sentences.
|
|
34
|
+
|
|
35
|
+
Be specific with file paths and line numbers.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scout
|
|
3
|
+
description: Fast codebase recon that returns compressed context for handoff to other agents
|
|
4
|
+
tools: read, grep, find, ls, bash
|
|
5
|
+
model: claude-haiku-4-5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
|
|
9
|
+
|
|
10
|
+
Your output will be passed to an agent who has NOT seen the files you explored.
|
|
11
|
+
|
|
12
|
+
Thoroughness (infer from task, default medium):
|
|
13
|
+
- Quick: Targeted lookups, key files only
|
|
14
|
+
- Medium: Follow imports, read critical sections
|
|
15
|
+
- Thorough: Trace all dependencies, check tests/types
|
|
16
|
+
|
|
17
|
+
Strategy:
|
|
18
|
+
1. grep/find to locate relevant code
|
|
19
|
+
2. Read key sections (not entire files)
|
|
20
|
+
3. Identify types, interfaces, key functions
|
|
21
|
+
4. Note dependencies between files
|
|
22
|
+
|
|
23
|
+
Output format:
|
|
24
|
+
|
|
25
|
+
## Files Retrieved
|
|
26
|
+
List with exact line ranges:
|
|
27
|
+
1. `path/to/file.ts` (lines 10-50) - Description of what's here
|
|
28
|
+
2. `path/to/other.ts` (lines 100-150) - Description
|
|
29
|
+
3. ...
|
|
30
|
+
|
|
31
|
+
## Key Code
|
|
32
|
+
Critical types, interfaces, or functions:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
interface Example {
|
|
36
|
+
// actual code from the files
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
function keyFunction() {
|
|
42
|
+
// actual implementation
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Architecture
|
|
47
|
+
Brief explanation of how the pieces connect.
|
|
48
|
+
|
|
49
|
+
## Start Here
|
|
50
|
+
Which file to look at first and why.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: worker
|
|
3
|
+
description: General-purpose subagent with full capabilities, isolated context
|
|
4
|
+
model: claude-sonnet-4-5
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are a worker agent with full capabilities. You operate in an isolated context window to handle delegated tasks without polluting the main conversation.
|
|
8
|
+
|
|
9
|
+
Work autonomously to complete the assigned task. Use all available tools as needed.
|
|
10
|
+
|
|
11
|
+
Output format when finished:
|
|
12
|
+
|
|
13
|
+
## Completed
|
|
14
|
+
What was done.
|
|
15
|
+
|
|
16
|
+
## Files Changed
|
|
17
|
+
- `path/to/file.ts` - what changed
|
|
18
|
+
|
|
19
|
+
## Notes (if any)
|
|
20
|
+
Anything the main agent should know.
|
|
21
|
+
|
|
22
|
+
If handing off to another agent (e.g. reviewer), include:
|
|
23
|
+
- Exact file paths changed
|
|
24
|
+
- Key functions/types touched (short list)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery and configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs'
|
|
6
|
+
import * as os from 'node:os'
|
|
7
|
+
import * as path from 'node:path'
|
|
8
|
+
import { getAgentDir, parseFrontmatter } from '@earendil-works/pi-coding-agent'
|
|
9
|
+
|
|
10
|
+
// Claude Code tool names -> pi tool names; unmapped names pass through lowercased
|
|
11
|
+
const CLAUDE_TOOL_MAP: Record<string, string> = {
|
|
12
|
+
read: 'read',
|
|
13
|
+
write: 'write',
|
|
14
|
+
edit: 'edit',
|
|
15
|
+
bash: 'bash',
|
|
16
|
+
grep: 'grep',
|
|
17
|
+
glob: 'find',
|
|
18
|
+
ls: 'ls',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeToolName(tool: string): string {
|
|
22
|
+
const lower = tool.toLowerCase()
|
|
23
|
+
return CLAUDE_TOOL_MAP[lower] ?? lower
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type AgentScope = 'user' | 'project' | 'both'
|
|
27
|
+
|
|
28
|
+
export interface AgentConfig {
|
|
29
|
+
name: string
|
|
30
|
+
description: string
|
|
31
|
+
tools?: string[]
|
|
32
|
+
model?: string
|
|
33
|
+
systemPrompt: string
|
|
34
|
+
source: 'user' | 'project'
|
|
35
|
+
filePath: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AgentDiscoveryResult {
|
|
39
|
+
agents: AgentConfig[]
|
|
40
|
+
projectAgentsDir: string | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadAgentsFromDir(dir: string, source: 'user' | 'project'): AgentConfig[] {
|
|
44
|
+
const agents: AgentConfig[] = []
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(dir)) {
|
|
47
|
+
return agents
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let entries: fs.Dirent[]
|
|
51
|
+
try {
|
|
52
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
53
|
+
} catch {
|
|
54
|
+
return agents
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (!entry.name.endsWith('.md')) continue
|
|
59
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue
|
|
60
|
+
|
|
61
|
+
const filePath = path.join(dir, entry.name)
|
|
62
|
+
let content: string
|
|
63
|
+
try {
|
|
64
|
+
content = fs.readFileSync(filePath, 'utf-8')
|
|
65
|
+
} catch {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content)
|
|
70
|
+
|
|
71
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const tools = frontmatter.tools
|
|
76
|
+
?.split(',')
|
|
77
|
+
.map((t: string) => normalizeToolName(t.trim()))
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
|
|
80
|
+
agents.push({
|
|
81
|
+
name: frontmatter.name,
|
|
82
|
+
description: frontmatter.description,
|
|
83
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
84
|
+
model: frontmatter.model,
|
|
85
|
+
systemPrompt: body,
|
|
86
|
+
source,
|
|
87
|
+
filePath,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return agents
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isDirectory(p: string): boolean {
|
|
95
|
+
try {
|
|
96
|
+
return fs.statSync(p).isDirectory()
|
|
97
|
+
} catch {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findNearestDir(cwd: string, relative: string): string | null {
|
|
103
|
+
let currentDir = cwd
|
|
104
|
+
while (true) {
|
|
105
|
+
const candidate = path.join(currentDir, relative)
|
|
106
|
+
if (isDirectory(candidate)) return candidate
|
|
107
|
+
|
|
108
|
+
const parentDir = path.dirname(currentDir)
|
|
109
|
+
if (parentDir === currentDir) return null
|
|
110
|
+
currentDir = parentDir
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
115
|
+
const userDir = path.join(getAgentDir(), 'agents')
|
|
116
|
+
const claudeUserDir = path.join(os.homedir(), '.claude', 'agents')
|
|
117
|
+
const projectPiDir = findNearestDir(cwd, path.join('.pi', 'agents'))
|
|
118
|
+
const projectClaudeDir = findNearestDir(cwd, path.join('.claude', 'agents'))
|
|
119
|
+
|
|
120
|
+
// ~/.claude/agents loads first so ~/.pi/agent/agents wins on name conflicts
|
|
121
|
+
const userAgents = scope === 'project' ? [] : [...loadAgentsFromDir(claudeUserDir, 'user'), ...loadAgentsFromDir(userDir, 'user')]
|
|
122
|
+
// project .claude/agents loads first so project .pi/agents wins on name conflicts
|
|
123
|
+
const projectAgents = scope === 'user' ? [] : [...(projectClaudeDir ? loadAgentsFromDir(projectClaudeDir, 'project') : []), ...(projectPiDir ? loadAgentsFromDir(projectPiDir, 'project') : [])]
|
|
124
|
+
|
|
125
|
+
const agentMap = new Map<string, AgentConfig>()
|
|
126
|
+
|
|
127
|
+
if (scope === 'both') {
|
|
128
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent)
|
|
129
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent)
|
|
130
|
+
} else if (scope === 'user') {
|
|
131
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent)
|
|
132
|
+
} else {
|
|
133
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { agents: Array.from(agentMap.values()), projectAgentsDir: projectPiDir }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {
|
|
140
|
+
if (agents.length === 0) return { text: 'none', remaining: 0 }
|
|
141
|
+
const listed = agents.slice(0, maxItems)
|
|
142
|
+
const remaining = agents.length - listed.length
|
|
143
|
+
return {
|
|
144
|
+
text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join('; '),
|
|
145
|
+
remaining,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background subagent runs: fire-and-forget children whose completion wakes
|
|
3
|
+
* the parent agent via a notification message. Session-scoped (children die
|
|
4
|
+
* with pi); state lives in an in-memory registry queried via {action:"status"}.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'node:child_process'
|
|
8
|
+
import { randomUUID } from 'node:crypto'
|
|
9
|
+
|
|
10
|
+
export interface BackgroundRun {
|
|
11
|
+
id: string
|
|
12
|
+
agent: string
|
|
13
|
+
task: string
|
|
14
|
+
state: 'running' | 'done' | 'failed'
|
|
15
|
+
exitCode?: number
|
|
16
|
+
output?: string
|
|
17
|
+
turns: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BackgroundSpawn {
|
|
21
|
+
command: string
|
|
22
|
+
args: string[]
|
|
23
|
+
cwd: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const runs = new Map<string, BackgroundRun>()
|
|
27
|
+
|
|
28
|
+
/** Extract the final assistant text and turn count from a pi --mode json stdout stream. */
|
|
29
|
+
export function parseFinalOutputFromJsonl(jsonl: string): { text: string; turns: number } {
|
|
30
|
+
let text = ''
|
|
31
|
+
let turns = 0
|
|
32
|
+
for (const line of jsonl.split('\n')) {
|
|
33
|
+
if (!line.trim()) continue
|
|
34
|
+
let event: { type?: string; message?: { role?: string; content?: Array<{ type: string; text?: string }> } }
|
|
35
|
+
try {
|
|
36
|
+
event = JSON.parse(line)
|
|
37
|
+
} catch {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if (event.type !== 'message_end' || event.message?.role !== 'assistant') continue
|
|
41
|
+
turns++
|
|
42
|
+
for (const part of event.message.content ?? []) {
|
|
43
|
+
if (part.type === 'text' && part.text) text = part.text
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { text, turns }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatStatus(all: Iterable<BackgroundRun>): string {
|
|
50
|
+
const lines = [...all].map((run) => {
|
|
51
|
+
const label = run.state === 'running' ? 'running' : `${run.state} (exit ${run.exitCode ?? '?'}, ${run.turns} turns)`
|
|
52
|
+
return `${run.id} ${run.agent}: ${label} - ${run.task.slice(0, 60)}`
|
|
53
|
+
})
|
|
54
|
+
return lines.length > 0 ? lines.join('\n') : 'No background runs in this session.'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function backgroundStatusText(): string {
|
|
58
|
+
return formatStatus(runs.values())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function startBackgroundRun(agent: string, task: string, invocation: BackgroundSpawn, onComplete: (run: BackgroundRun) => void): string {
|
|
62
|
+
const id = `bg-${randomUUID().slice(0, 8)}`
|
|
63
|
+
const run: BackgroundRun = { id, agent, task, state: 'running', turns: 0 }
|
|
64
|
+
runs.set(id, run)
|
|
65
|
+
|
|
66
|
+
const proc = spawn(invocation.command, invocation.args, {
|
|
67
|
+
cwd: invocation.cwd,
|
|
68
|
+
shell: false,
|
|
69
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
70
|
+
})
|
|
71
|
+
let stdout = ''
|
|
72
|
+
proc.stdout.on('data', (data) => {
|
|
73
|
+
stdout += data.toString()
|
|
74
|
+
})
|
|
75
|
+
proc.on('close', (code) => {
|
|
76
|
+
const { text, turns } = parseFinalOutputFromJsonl(stdout)
|
|
77
|
+
run.state = code === 0 ? 'done' : 'failed'
|
|
78
|
+
run.exitCode = code ?? 0
|
|
79
|
+
run.output = text
|
|
80
|
+
run.turns = turns
|
|
81
|
+
onComplete(run)
|
|
82
|
+
})
|
|
83
|
+
proc.on('error', () => {
|
|
84
|
+
run.state = 'failed'
|
|
85
|
+
run.exitCode = 1
|
|
86
|
+
onComplete(run)
|
|
87
|
+
})
|
|
88
|
+
return id
|
|
89
|
+
}
|