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,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions for plan mode.
|
|
3
|
+
* Extracted for testability.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Destructive commands blocked in plan mode
|
|
7
|
+
const DESTRUCTIVE_PATTERNS = [
|
|
8
|
+
/\brm\b/i,
|
|
9
|
+
/\brmdir\b/i,
|
|
10
|
+
/\bmv\b/i,
|
|
11
|
+
/\bcp\b/i,
|
|
12
|
+
/\bmkdir\b/i,
|
|
13
|
+
/\btouch\b/i,
|
|
14
|
+
/\bchmod\b/i,
|
|
15
|
+
/\bchown\b/i,
|
|
16
|
+
/\bchgrp\b/i,
|
|
17
|
+
/\bln\b/i,
|
|
18
|
+
/\btee\b/i,
|
|
19
|
+
/\btruncate\b/i,
|
|
20
|
+
/\bdd\b/i,
|
|
21
|
+
/\bshred\b/i,
|
|
22
|
+
/(^|[^<])>(?!>)/,
|
|
23
|
+
/>>/,
|
|
24
|
+
/\bnpm\s+(install|uninstall|update|ci|link|publish)/i,
|
|
25
|
+
/\byarn\s+(add|remove|install|publish)/i,
|
|
26
|
+
/\bpnpm\s+(add|remove|install|publish)/i,
|
|
27
|
+
/\bpip\s+(install|uninstall)/i,
|
|
28
|
+
/\bapt(-get)?\s+(install|remove|purge|update|upgrade)/i,
|
|
29
|
+
/\bbrew\s+(install|uninstall|upgrade)/i,
|
|
30
|
+
/\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|branch\s+-[dD]|stash|cherry-pick|revert|tag|init|clone)/i,
|
|
31
|
+
/\bsudo\b/i,
|
|
32
|
+
/\bsu\b/i,
|
|
33
|
+
/\bkill\b/i,
|
|
34
|
+
/\bpkill\b/i,
|
|
35
|
+
/\bkillall\b/i,
|
|
36
|
+
/\breboot\b/i,
|
|
37
|
+
/\bshutdown\b/i,
|
|
38
|
+
/\bsystemctl\s+(start|stop|restart|enable|disable)/i,
|
|
39
|
+
/\bservice\s+\S+\s+(start|stop|restart)/i,
|
|
40
|
+
/\b(vim?|nano|emacs|code|subl)\b/i,
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
// Safe read-only commands allowed in plan mode
|
|
44
|
+
const SAFE_PATTERNS = [
|
|
45
|
+
/^\s*cat\b/,
|
|
46
|
+
/^\s*head\b/,
|
|
47
|
+
/^\s*tail\b/,
|
|
48
|
+
/^\s*less\b/,
|
|
49
|
+
/^\s*more\b/,
|
|
50
|
+
/^\s*grep\b/,
|
|
51
|
+
/^\s*find\b/,
|
|
52
|
+
/^\s*ls\b/,
|
|
53
|
+
/^\s*pwd\b/,
|
|
54
|
+
/^\s*echo\b/,
|
|
55
|
+
/^\s*printf\b/,
|
|
56
|
+
/^\s*wc\b/,
|
|
57
|
+
/^\s*sort\b/,
|
|
58
|
+
/^\s*uniq\b/,
|
|
59
|
+
/^\s*diff\b/,
|
|
60
|
+
/^\s*file\b/,
|
|
61
|
+
/^\s*stat\b/,
|
|
62
|
+
/^\s*du\b/,
|
|
63
|
+
/^\s*df\b/,
|
|
64
|
+
/^\s*tree\b/,
|
|
65
|
+
/^\s*which\b/,
|
|
66
|
+
/^\s*whereis\b/,
|
|
67
|
+
/^\s*type\b/,
|
|
68
|
+
/^\s*env\b/,
|
|
69
|
+
/^\s*printenv\b/,
|
|
70
|
+
/^\s*uname\b/,
|
|
71
|
+
/^\s*whoami\b/,
|
|
72
|
+
/^\s*id\b/,
|
|
73
|
+
/^\s*date\b/,
|
|
74
|
+
/^\s*cal\b/,
|
|
75
|
+
/^\s*uptime\b/,
|
|
76
|
+
/^\s*ps\b/,
|
|
77
|
+
/^\s*top\b/,
|
|
78
|
+
/^\s*htop\b/,
|
|
79
|
+
/^\s*free\b/,
|
|
80
|
+
/^\s*git\s+(status|log|diff|show|branch|remote|config\s+--get)/i,
|
|
81
|
+
/^\s*git\s+ls-/i,
|
|
82
|
+
/^\s*npm\s+(list|ls|view|info|search|outdated|audit)/i,
|
|
83
|
+
/^\s*yarn\s+(list|info|why|audit)/i,
|
|
84
|
+
/^\s*node\s+--version/i,
|
|
85
|
+
/^\s*python\s+--version/i,
|
|
86
|
+
/^\s*curl\s/i,
|
|
87
|
+
/^\s*wget\s+-O\s*-/i,
|
|
88
|
+
/^\s*jq\b/,
|
|
89
|
+
/^\s*sed\s+-n/i,
|
|
90
|
+
/^\s*awk\b/,
|
|
91
|
+
/^\s*rg\b/,
|
|
92
|
+
/^\s*fd\b/,
|
|
93
|
+
/^\s*bat\b/,
|
|
94
|
+
/^\s*eza\b/,
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
export function isSafeCommand(command: string): boolean {
|
|
98
|
+
const isDestructive = DESTRUCTIVE_PATTERNS.some((p) => p.test(command))
|
|
99
|
+
const isSafe = SAFE_PATTERNS.some((p) => p.test(command))
|
|
100
|
+
return !isDestructive && isSafe
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface TodoItem {
|
|
104
|
+
step: number
|
|
105
|
+
text: string
|
|
106
|
+
completed: boolean
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function cleanStepText(text: string): string {
|
|
110
|
+
let cleaned = text
|
|
111
|
+
.replace(/\*{1,2}([^*]+)\*{1,2}/g, '$1') // Remove bold/italic
|
|
112
|
+
.replace(/`([^`]+)`/g, '$1') // Remove code
|
|
113
|
+
.replace(/^(Use|Run|Execute|Create|Write|Read|Check|Verify|Update|Modify|Add|Remove|Delete|Install)\s+(the\s+)?/i, '')
|
|
114
|
+
.replace(/\s+/g, ' ')
|
|
115
|
+
.trim()
|
|
116
|
+
|
|
117
|
+
if (cleaned.length > 0) {
|
|
118
|
+
cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1)
|
|
119
|
+
}
|
|
120
|
+
if (cleaned.length > 50) {
|
|
121
|
+
cleaned = `${cleaned.slice(0, 47)}...`
|
|
122
|
+
}
|
|
123
|
+
return cleaned
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function extractTodoItems(message: string): TodoItem[] {
|
|
127
|
+
const items: TodoItem[] = []
|
|
128
|
+
const headerMatch = message.match(/\*{0,2}Plan:\*{0,2}\s*\n/i)
|
|
129
|
+
if (!headerMatch) return items
|
|
130
|
+
|
|
131
|
+
const planSection = message.slice(message.indexOf(headerMatch[0]) + headerMatch[0].length)
|
|
132
|
+
const numberedPattern = /^\s*(\d+)[.)]\s+\*{0,2}([^*\n]+)/gm
|
|
133
|
+
|
|
134
|
+
for (const match of planSection.matchAll(numberedPattern)) {
|
|
135
|
+
const text = match[2]
|
|
136
|
+
.trim()
|
|
137
|
+
.replace(/\*{1,2}$/, '')
|
|
138
|
+
.trim()
|
|
139
|
+
if (text.length > 5 && !text.startsWith('`') && !text.startsWith('/') && !text.startsWith('-')) {
|
|
140
|
+
const cleaned = cleanStepText(text)
|
|
141
|
+
if (cleaned.length > 3) {
|
|
142
|
+
items.push({ step: items.length + 1, text: cleaned, completed: false })
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return items
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function extractDoneSteps(message: string): number[] {
|
|
150
|
+
const steps: number[] = []
|
|
151
|
+
for (const match of message.matchAll(/\[DONE:(\d+)\]/gi)) {
|
|
152
|
+
const step = Number(match[1])
|
|
153
|
+
if (Number.isFinite(step)) steps.push(step)
|
|
154
|
+
}
|
|
155
|
+
return steps
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function markCompletedSteps(text: string, items: TodoItem[]): number {
|
|
159
|
+
const doneSteps = extractDoneSteps(text)
|
|
160
|
+
for (const step of doneSteps) {
|
|
161
|
+
const item = items.find((t) => t.step === step)
|
|
162
|
+
if (item) item.completed = true
|
|
163
|
+
}
|
|
164
|
+
return doneSteps.length
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parse an explicitly submitted plan (from the plan_mode_complete tool) into
|
|
169
|
+
* todo items. Unlike extractTodoItems, the Plan: header is optional because
|
|
170
|
+
* the tool input is already known to be the plan itself.
|
|
171
|
+
*/
|
|
172
|
+
export function planToTodos(plan: string): TodoItem[] {
|
|
173
|
+
const withHeader = /\*{0,2}Plan:\*{0,2}\s*\n/i.test(plan) ? plan : `Plan:\n${plan}`
|
|
174
|
+
return extractTodoItems(withHeader)
|
|
175
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Question Tool - Single question with options
|
|
3
|
+
* Full custom UI: options list + inline editor for "Type something..."
|
|
4
|
+
* Escape in editor returns to options, Escape in options cancels
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
8
|
+
import { Editor, type EditorTheme, Key, matchesKey, Text, truncateToWidth } from '@earendil-works/pi-tui'
|
|
9
|
+
import { Type } from 'typebox'
|
|
10
|
+
|
|
11
|
+
interface OptionWithDesc {
|
|
12
|
+
label: string
|
|
13
|
+
description?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type DisplayOption = OptionWithDesc & { isOther?: boolean }
|
|
17
|
+
|
|
18
|
+
interface QuestionDetails {
|
|
19
|
+
question: string
|
|
20
|
+
options: string[]
|
|
21
|
+
answer: string | null
|
|
22
|
+
wasCustom?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Options with labels and optional descriptions
|
|
26
|
+
const OptionSchema = Type.Object({
|
|
27
|
+
label: Type.String({ description: 'Display label for the option' }),
|
|
28
|
+
description: Type.Optional(Type.String({ description: 'Optional description shown below label' })),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const QuestionParams = Type.Object({
|
|
32
|
+
question: Type.String({ description: 'The question to ask the user' }),
|
|
33
|
+
options: Type.Array(OptionSchema, { description: 'Options for the user to choose from' }),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export default function question(pi: ExtensionAPI) {
|
|
37
|
+
pi.registerTool({
|
|
38
|
+
name: 'question',
|
|
39
|
+
label: 'Question',
|
|
40
|
+
description: 'Ask the user a question and let them pick from options. Use when you need user input to proceed.',
|
|
41
|
+
parameters: QuestionParams,
|
|
42
|
+
|
|
43
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
44
|
+
if (!ctx.hasUI) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: 'text', text: 'Error: UI not available (running in non-interactive mode)' }],
|
|
47
|
+
details: {
|
|
48
|
+
question: params.question,
|
|
49
|
+
options: params.options.map((o) => o.label),
|
|
50
|
+
answer: null,
|
|
51
|
+
} as QuestionDetails,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (params.options.length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: 'text', text: 'Error: No options provided' }],
|
|
58
|
+
details: { question: params.question, options: [], answer: null } as QuestionDetails,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const allOptions: DisplayOption[] = [...params.options, { label: 'Type something.', isOther: true }]
|
|
63
|
+
|
|
64
|
+
const result = await ctx.ui.custom<{ answer: string; wasCustom: boolean; index?: number } | null>((tui, theme, _kb, done) => {
|
|
65
|
+
let optionIndex = 0
|
|
66
|
+
let editMode = false
|
|
67
|
+
let cachedLines: string[] | undefined
|
|
68
|
+
|
|
69
|
+
const editorTheme: EditorTheme = {
|
|
70
|
+
borderColor: (s) => theme.fg('accent', s),
|
|
71
|
+
selectList: {
|
|
72
|
+
selectedPrefix: (t) => theme.fg('accent', t),
|
|
73
|
+
selectedText: (t) => theme.fg('accent', t),
|
|
74
|
+
description: (t) => theme.fg('muted', t),
|
|
75
|
+
scrollInfo: (t) => theme.fg('dim', t),
|
|
76
|
+
noMatch: (t) => theme.fg('warning', t),
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
const editor = new Editor(tui, editorTheme)
|
|
80
|
+
|
|
81
|
+
editor.onSubmit = (value) => {
|
|
82
|
+
const trimmed = value.trim()
|
|
83
|
+
if (trimmed) {
|
|
84
|
+
done({ answer: trimmed, wasCustom: true })
|
|
85
|
+
} else {
|
|
86
|
+
editMode = false
|
|
87
|
+
editor.setText('')
|
|
88
|
+
refresh()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function refresh() {
|
|
93
|
+
cachedLines = undefined
|
|
94
|
+
tui.requestRender()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleInput(data: string) {
|
|
98
|
+
if (editMode) {
|
|
99
|
+
if (matchesKey(data, Key.escape)) {
|
|
100
|
+
editMode = false
|
|
101
|
+
editor.setText('')
|
|
102
|
+
refresh()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
editor.handleInput(data)
|
|
106
|
+
refresh()
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (matchesKey(data, Key.up)) {
|
|
111
|
+
optionIndex = Math.max(0, optionIndex - 1)
|
|
112
|
+
refresh()
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
if (matchesKey(data, Key.down)) {
|
|
116
|
+
optionIndex = Math.min(allOptions.length - 1, optionIndex + 1)
|
|
117
|
+
refresh()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (matchesKey(data, Key.enter)) {
|
|
122
|
+
const selected = allOptions[optionIndex]
|
|
123
|
+
if (selected.isOther) {
|
|
124
|
+
editMode = true
|
|
125
|
+
refresh()
|
|
126
|
+
} else {
|
|
127
|
+
done({ answer: selected.label, wasCustom: false, index: optionIndex + 1 })
|
|
128
|
+
}
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (matchesKey(data, Key.escape)) {
|
|
133
|
+
done(null)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function render(width: number): string[] {
|
|
138
|
+
if (cachedLines) return cachedLines
|
|
139
|
+
|
|
140
|
+
const lines: string[] = []
|
|
141
|
+
const add = (s: string) => lines.push(truncateToWidth(s, width))
|
|
142
|
+
|
|
143
|
+
add(theme.fg('accent', '─'.repeat(width)))
|
|
144
|
+
add(theme.fg('text', ` ${params.question}`))
|
|
145
|
+
lines.push('')
|
|
146
|
+
|
|
147
|
+
for (let i = 0; i < allOptions.length; i++) {
|
|
148
|
+
const opt = allOptions[i]
|
|
149
|
+
const selected = i === optionIndex
|
|
150
|
+
const isOther = opt.isOther === true
|
|
151
|
+
const prefix = selected ? theme.fg('accent', '> ') : ' '
|
|
152
|
+
|
|
153
|
+
if (isOther && editMode) {
|
|
154
|
+
add(prefix + theme.fg('accent', `${i + 1}. ${opt.label} ✎`))
|
|
155
|
+
} else if (selected) {
|
|
156
|
+
add(prefix + theme.fg('accent', `${i + 1}. ${opt.label}`))
|
|
157
|
+
} else {
|
|
158
|
+
add(` ${theme.fg('text', `${i + 1}. ${opt.label}`)}`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Show description if present
|
|
162
|
+
if (opt.description) {
|
|
163
|
+
add(` ${theme.fg('muted', opt.description)}`)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (editMode) {
|
|
168
|
+
lines.push('')
|
|
169
|
+
add(theme.fg('muted', ' Your answer:'))
|
|
170
|
+
for (const line of editor.render(width - 2)) {
|
|
171
|
+
add(` ${line}`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
lines.push('')
|
|
176
|
+
if (editMode) {
|
|
177
|
+
add(theme.fg('dim', ' Enter to submit • Esc to go back'))
|
|
178
|
+
} else {
|
|
179
|
+
add(theme.fg('dim', ' ↑↓ navigate • Enter to select • Esc to cancel'))
|
|
180
|
+
}
|
|
181
|
+
add(theme.fg('accent', '─'.repeat(width)))
|
|
182
|
+
|
|
183
|
+
cachedLines = lines
|
|
184
|
+
return lines
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
render,
|
|
189
|
+
invalidate: () => {
|
|
190
|
+
cachedLines = undefined
|
|
191
|
+
},
|
|
192
|
+
handleInput,
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Build simple options list for details
|
|
197
|
+
const simpleOptions = params.options.map((o) => o.label)
|
|
198
|
+
|
|
199
|
+
if (!result) {
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: 'text', text: 'User cancelled the selection' }],
|
|
202
|
+
details: { question: params.question, options: simpleOptions, answer: null } as QuestionDetails,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (result.wasCustom) {
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: 'text', text: `User wrote: ${result.answer}` }],
|
|
209
|
+
details: {
|
|
210
|
+
question: params.question,
|
|
211
|
+
options: simpleOptions,
|
|
212
|
+
answer: result.answer,
|
|
213
|
+
wasCustom: true,
|
|
214
|
+
} as QuestionDetails,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
content: [{ type: 'text', text: `User selected: ${result.index}. ${result.answer}` }],
|
|
219
|
+
details: {
|
|
220
|
+
question: params.question,
|
|
221
|
+
options: simpleOptions,
|
|
222
|
+
answer: result.answer,
|
|
223
|
+
wasCustom: false,
|
|
224
|
+
} as QuestionDetails,
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
renderCall(args, theme, _context) {
|
|
229
|
+
let text = theme.fg('toolTitle', theme.bold('question ')) + theme.fg('muted', args.question)
|
|
230
|
+
const opts = Array.isArray(args.options) ? args.options : []
|
|
231
|
+
if (opts.length) {
|
|
232
|
+
const labels = opts.map((o: OptionWithDesc) => o.label)
|
|
233
|
+
const numbered = [...labels, 'Type something.'].map((o, i) => `${i + 1}. ${o}`)
|
|
234
|
+
text += `\n${theme.fg('dim', ` Options: ${numbered.join(', ')}`)}`
|
|
235
|
+
}
|
|
236
|
+
return new Text(text, 0, 0)
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
renderResult(result, _options, theme, _context) {
|
|
240
|
+
const details = result.details as QuestionDetails | undefined
|
|
241
|
+
if (!details) {
|
|
242
|
+
const text = result.content[0]
|
|
243
|
+
return new Text(text?.type === 'text' ? text.text : '', 0, 0)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (details.answer === null) {
|
|
247
|
+
return new Text(theme.fg('warning', 'Cancelled'), 0, 0)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (details.wasCustom) {
|
|
251
|
+
return new Text(theme.fg('success', '✓ ') + theme.fg('muted', '(wrote) ') + theme.fg('accent', details.answer), 0, 0)
|
|
252
|
+
}
|
|
253
|
+
const idx = details.options.indexOf(details.answer) + 1
|
|
254
|
+
const display = idx > 0 ? `${idx}. ${details.answer}` : details.answer
|
|
255
|
+
return new Text(theme.fg('success', '✓ ') + theme.fg('accent', display), 0, 0)
|
|
256
|
+
},
|
|
257
|
+
})
|
|
258
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Skills Extension
|
|
3
|
+
*
|
|
4
|
+
* Bridges Claude Code's skills into pi. On resources_discover it hands pi the
|
|
5
|
+
* existing `.claude/skills` directories (user then project) as skill paths, so
|
|
6
|
+
* pi discovers Claude Code skills the same way it loads its own `.pi/skills`.
|
|
7
|
+
* pi implements the Agent Skills standard, so `SKILL.md` directories work
|
|
8
|
+
* unchanged and register as `/skill:name`.
|
|
9
|
+
*
|
|
10
|
+
* Docs: https://code.claude.com/docs/en/skills.md, https://agentskills.io
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'node:fs'
|
|
14
|
+
import * as os from 'node:os'
|
|
15
|
+
import * as path from 'node:path'
|
|
16
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
17
|
+
|
|
18
|
+
function isDirectory(target: string): boolean {
|
|
19
|
+
try {
|
|
20
|
+
return fs.statSync(target).isDirectory()
|
|
21
|
+
} catch {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Existing `.claude/skills` directories, user first then project. */
|
|
27
|
+
export function skillDirs(cwd: string, home: string): string[] {
|
|
28
|
+
const candidates = [path.join(home, '.claude', 'skills'), path.join(cwd, '.claude', 'skills')]
|
|
29
|
+
const dirs: string[] = []
|
|
30
|
+
for (const dir of candidates) {
|
|
31
|
+
if (!dirs.includes(dir) && isDirectory(dir)) dirs.push(dir)
|
|
32
|
+
}
|
|
33
|
+
return dirs
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function skillsExtension(pi: ExtensionAPI) {
|
|
37
|
+
pi.on('resources_discover', async (_event, ctx) => {
|
|
38
|
+
const skillPaths = skillDirs(ctx.cwd, os.homedir())
|
|
39
|
+
return skillPaths.length > 0 ? { skillPaths } : undefined
|
|
40
|
+
})
|
|
41
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Line Extension
|
|
3
|
+
*
|
|
4
|
+
* Adds a Claude Code style status segment to pi's footer: turn state plus
|
|
5
|
+
* running session cost. Cost is summed from per-message usage on the current
|
|
6
|
+
* branch, so it stays correct across /tree navigation and forks.
|
|
7
|
+
*
|
|
8
|
+
* pi's built-in footer already shows path, branch, context, and model;
|
|
9
|
+
* this extension only adds what is missing instead of replacing the footer.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
|
|
13
|
+
|
|
14
|
+
interface UsageEntry {
|
|
15
|
+
type: string
|
|
16
|
+
message?: { usage?: { cost?: { total?: number } } }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sessionCost(ctx: ExtensionContext): number {
|
|
20
|
+
let total = 0
|
|
21
|
+
for (const entry of ctx.sessionManager.getBranch() as UsageEntry[]) {
|
|
22
|
+
total += entry.message?.usage?.cost?.total ?? 0
|
|
23
|
+
}
|
|
24
|
+
return total
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatCost(cost: number): string {
|
|
28
|
+
return cost >= 0.01 ? `$${cost.toFixed(2)}` : `$${cost.toFixed(4)}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function statusLine(pi: ExtensionAPI) {
|
|
32
|
+
let turnCount = 0
|
|
33
|
+
|
|
34
|
+
function showIdle(ctx: ExtensionContext, symbol: string): void {
|
|
35
|
+
const theme = ctx.ui.theme
|
|
36
|
+
const cost = sessionCost(ctx)
|
|
37
|
+
const costText = cost > 0 ? theme.fg('muted', ` ${formatCost(cost)}`) : ''
|
|
38
|
+
const turnText = turnCount > 0 ? theme.fg('dim', ` turn ${turnCount}`) : theme.fg('dim', ' ready')
|
|
39
|
+
ctx.ui.setStatus('pi-code-status', symbol + turnText + costText)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
43
|
+
showIdle(ctx, ctx.ui.theme.fg('dim', '○'))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
pi.on('turn_start', async (_event, ctx) => {
|
|
47
|
+
turnCount++
|
|
48
|
+
const theme = ctx.ui.theme
|
|
49
|
+
ctx.ui.setStatus('pi-code-status', theme.fg('accent', '●') + theme.fg('dim', ` turn ${turnCount}...`))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
pi.on('turn_end', async (_event, ctx) => {
|
|
53
|
+
showIdle(ctx, ctx.ui.theme.fg('success', '✓'))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
pi.on('agent_end', async (_event, ctx) => {
|
|
57
|
+
showIdle(ctx, ctx.ui.theme.fg('success', '✓'))
|
|
58
|
+
})
|
|
59
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Subagent Example
|
|
2
|
+
|
|
3
|
+
Delegate tasks to specialized subagents with isolated context windows.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Isolated context**: Each subagent runs in a separate `pi` process
|
|
8
|
+
- **Streaming output**: See tool calls and progress as they happen
|
|
9
|
+
- **Parallel streaming**: All parallel tasks stream updates simultaneously
|
|
10
|
+
- **Markdown rendering**: Final output rendered with proper formatting (expanded view)
|
|
11
|
+
- **Usage tracking**: Shows turns, tokens, cost, and context usage per agent
|
|
12
|
+
- **Abort support**: Ctrl+C propagates to kill subagent processes
|
|
13
|
+
|
|
14
|
+
## Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
subagent/
|
|
18
|
+
├── README.md # This file
|
|
19
|
+
├── index.ts # The extension (entry point)
|
|
20
|
+
├── agents.ts # Agent discovery logic
|
|
21
|
+
├── agents/ # Sample agent definitions
|
|
22
|
+
│ ├── scout.md # Fast recon, returns compressed context
|
|
23
|
+
│ ├── planner.md # Creates implementation plans
|
|
24
|
+
│ ├── reviewer.md # Code review
|
|
25
|
+
│ └── worker.md # General-purpose (full capabilities)
|
|
26
|
+
└── prompts/ # Workflow presets (prompt templates)
|
|
27
|
+
├── implement.md # scout -> planner -> worker
|
|
28
|
+
├── scout-and-plan.md # scout -> planner (no implementation)
|
|
29
|
+
└── implement-and-review.md # worker -> reviewer -> worker
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
From the repository root, symlink the files:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Symlink the extension (must be in a subdirectory with index.ts)
|
|
38
|
+
mkdir -p ~/.pi/agent/extensions/subagent
|
|
39
|
+
ln -sf "$(pwd)/packages/coding-agent/examples/extensions/subagent/index.ts" ~/.pi/agent/extensions/subagent/index.ts
|
|
40
|
+
ln -sf "$(pwd)/packages/coding-agent/examples/extensions/subagent/agents.ts" ~/.pi/agent/extensions/subagent/agents.ts
|
|
41
|
+
|
|
42
|
+
# Symlink agents
|
|
43
|
+
mkdir -p ~/.pi/agent/agents
|
|
44
|
+
for f in packages/coding-agent/examples/extensions/subagent/agents/*.md; do
|
|
45
|
+
ln -sf "$(pwd)/$f" ~/.pi/agent/agents/$(basename "$f")
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
# Symlink workflow prompts
|
|
49
|
+
mkdir -p ~/.pi/agent/prompts
|
|
50
|
+
for f in packages/coding-agent/examples/extensions/subagent/prompts/*.md; do
|
|
51
|
+
ln -sf "$(pwd)/$f" ~/.pi/agent/prompts/$(basename "$f")
|
|
52
|
+
done
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Security Model
|
|
56
|
+
|
|
57
|
+
This tool executes a separate `pi` subprocess with a delegated system prompt and tool/model configuration.
|
|
58
|
+
|
|
59
|
+
**Project-local agents** (`.pi/agents/*.md`) are repo-controlled prompts that can instruct the model to read files, run bash commands, etc.
|
|
60
|
+
|
|
61
|
+
**Default behavior:** Only loads **user-level agents** from `~/.pi/agent/agents`.
|
|
62
|
+
|
|
63
|
+
To enable project-local agents, pass `agentScope: "both"` (or `"project"`). Only do this for repositories you trust.
|
|
64
|
+
|
|
65
|
+
When running interactively, the tool prompts for confirmation before running project-local agents. Set `confirmProjectAgents: false` to disable.
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
### Single agent
|
|
70
|
+
```
|
|
71
|
+
Use scout to find all authentication code
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Parallel execution
|
|
75
|
+
```
|
|
76
|
+
Run 2 scouts in parallel: one to find models, one to find providers
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Chained workflow
|
|
80
|
+
```
|
|
81
|
+
Use a chain: first have scout find the read tool, then have planner suggest improvements
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Workflow prompts
|
|
85
|
+
```
|
|
86
|
+
/implement add Redis caching to the session store
|
|
87
|
+
/scout-and-plan refactor auth to support OAuth
|
|
88
|
+
/implement-and-review add input validation to API endpoints
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Tool Modes
|
|
92
|
+
|
|
93
|
+
| Mode | Parameter | Description |
|
|
94
|
+
|------|-----------|-------------|
|
|
95
|
+
| Single | `{ agent, task }` | One agent, one task |
|
|
96
|
+
| Parallel | `{ tasks: [...] }` | Multiple agents run concurrently (max 8, 4 concurrent) |
|
|
97
|
+
| Chain | `{ chain: [...] }` | Sequential with `{previous}` placeholder |
|
|
98
|
+
|
|
99
|
+
## Output Display
|
|
100
|
+
|
|
101
|
+
**Collapsed view** (default):
|
|
102
|
+
- Status icon (✓/✗/⏳) and agent name
|
|
103
|
+
- Last 5-10 items (tool calls and text)
|
|
104
|
+
- Usage stats: `3 turns ↑input ↓output RcacheRead WcacheWrite $cost ctx:contextTokens model`
|
|
105
|
+
|
|
106
|
+
**Expanded view** (Ctrl+O):
|
|
107
|
+
- Full task text
|
|
108
|
+
- All tool calls with formatted arguments
|
|
109
|
+
- Final output rendered as Markdown
|
|
110
|
+
- Per-task usage (for chain/parallel)
|
|
111
|
+
|
|
112
|
+
**Parallel mode streaming**:
|
|
113
|
+
- Shows all tasks with live status (⏳ running, ✓ done, ✗ failed)
|
|
114
|
+
- Updates as each task makes progress
|
|
115
|
+
- Shows "2/3 done, 1 running" status
|
|
116
|
+
|
|
117
|
+
**Tool call formatting** (mimics built-in tools):
|
|
118
|
+
- `$ command` for bash
|
|
119
|
+
- `read ~/path:1-10` for read
|
|
120
|
+
- `grep /pattern/ in ~/path` for grep
|
|
121
|
+
- etc.
|
|
122
|
+
|
|
123
|
+
## Agent Definitions
|
|
124
|
+
|
|
125
|
+
Agents are markdown files with YAML frontmatter:
|
|
126
|
+
|
|
127
|
+
```markdown
|
|
128
|
+
---
|
|
129
|
+
name: my-agent
|
|
130
|
+
description: What this agent does
|
|
131
|
+
tools: read, grep, find, ls
|
|
132
|
+
model: claude-haiku-4-5
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
System prompt for the agent goes here.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Locations:**
|
|
139
|
+
- `~/.pi/agent/agents/*.md` - User-level (always loaded)
|
|
140
|
+
- `.pi/agents/*.md` - Project-level (only with `agentScope: "project"` or `"both"`)
|
|
141
|
+
|
|
142
|
+
Project agents override user agents with the same name when `agentScope: "both"`.
|
|
143
|
+
|
|
144
|
+
## Sample Agents
|
|
145
|
+
|
|
146
|
+
| Agent | Purpose | Model | Tools |
|
|
147
|
+
|-------|---------|-------|-------|
|
|
148
|
+
| `scout` | Fast codebase recon | Haiku | read, grep, find, ls, bash |
|
|
149
|
+
| `planner` | Implementation plans | Sonnet | read, grep, find, ls |
|
|
150
|
+
| `reviewer` | Code review | Sonnet | read, grep, find, ls, bash |
|
|
151
|
+
| `worker` | General-purpose | Sonnet | (all default) |
|
|
152
|
+
|
|
153
|
+
## Workflow Prompts
|
|
154
|
+
|
|
155
|
+
| Prompt | Flow |
|
|
156
|
+
|--------|------|
|
|
157
|
+
| `/implement <query>` | scout → planner → worker |
|
|
158
|
+
| `/scout-and-plan <query>` | scout → planner |
|
|
159
|
+
| `/implement-and-review <query>` | worker → reviewer → worker |
|
|
160
|
+
|
|
161
|
+
## Error Handling
|
|
162
|
+
|
|
163
|
+
- **Exit code != 0**: Tool returns error with stderr/output
|
|
164
|
+
- **stopReason "error"**: LLM error propagated with error message
|
|
165
|
+
- **stopReason "aborted"**: User abort (Ctrl+C) kills subprocess, throws error
|
|
166
|
+
- **Chain mode**: Stops at first failing step, reports which step failed
|
|
167
|
+
|
|
168
|
+
## Limitations
|
|
169
|
+
|
|
170
|
+
- Output truncated to last 10 items in collapsed view (expand to see all)
|
|
171
|
+
- Agents discovered fresh on each invocation (allows editing mid-session)
|
|
172
|
+
- Parallel mode limited to 8 tasks, 4 concurrent
|