pi-code 1.0.37 → 1.0.39
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/extensions/commands.ts +1 -1
- package/extensions/internal/agent-run.ts +6 -0
- package/extensions/internal/command-file.ts +17 -12
- package/extensions/internal/plugins.ts +18 -3
- package/extensions/mcp/index.ts +28 -0
- package/extensions/skills.ts +53 -3
- package/extensions/subagent/index.ts +18 -1
- package/package.json +1 -1
package/extensions/commands.ts
CHANGED
|
@@ -236,7 +236,7 @@ export async function expandCommand(runner: SpanRunner, parsed: ParsedCommand, a
|
|
|
236
236
|
return { stdout, stderr: result.stderr, code: result.code }
|
|
237
237
|
}
|
|
238
238
|
: async () => ({ stdout: SHELL_DISABLED_PLACEHOLDER, stderr: '', code: 0 })
|
|
239
|
-
let expanded = await expandDynamicContent(withVars, ctx.cwd, exec)
|
|
239
|
+
let expanded = await expandDynamicContent(withVars, ctx.cwd, exec, parsed.shell === 'powershell' ? 'powershell' : 'bash')
|
|
240
240
|
|
|
241
241
|
// Claude appends the raw arguments when the command never read them, so what
|
|
242
242
|
// the user typed still reaches the model.
|
|
@@ -16,6 +16,12 @@ export interface AgentRunRequest {
|
|
|
16
16
|
model?: string
|
|
17
17
|
/** Optional extra system prompt (Claude's experimental `systemPrompt` field). */
|
|
18
18
|
systemPrompt?: string
|
|
19
|
+
/** A discovered agent to run as (Claude's `agent` field on context: fork
|
|
20
|
+
* skills); unknown names fall back per fullTools. */
|
|
21
|
+
agent?: string
|
|
22
|
+
/** Run with the full toolset instead of the read-only hook shape, for
|
|
23
|
+
* context: fork skills. */
|
|
24
|
+
fullTools?: boolean
|
|
19
25
|
/** Aborts the run at the hook's deadline. */
|
|
20
26
|
signal?: AbortSignal
|
|
21
27
|
}
|
|
@@ -49,7 +49,7 @@ export interface ParsedCommand {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export interface DiscoveredCommand {
|
|
52
|
-
/** Claude
|
|
52
|
+
/** Claude names a command by its file name alone; a plugin's is `plugin:name`. */
|
|
53
53
|
name: string
|
|
54
54
|
filePath: string
|
|
55
55
|
/** Set for plugin commands, carrying the ${CLAUDE_PLUGIN_*} and ${user_config.*} substitution sources. */
|
|
@@ -539,16 +539,21 @@ function fencedRanges(body: string): Array<[number, number]> {
|
|
|
539
539
|
}
|
|
540
540
|
|
|
541
541
|
/** Exit 1 is a normal result for Claude's documented search and comparison commands
|
|
542
|
-
* (no matches, files differ); exit 2 and up fails even for these.
|
|
542
|
+
* (no matches, files differ); exit 2 and up fails even for these. The PowerShell
|
|
543
|
+
* shell uses a different set, which "includes grep and git diff but not find or
|
|
544
|
+
* diff" (test/[ are bash builtins and do not apply there either). */
|
|
543
545
|
const EXIT_ONE_OK = new Set(['grep', 'rg', 'egrep', 'fgrep', 'find', 'diff', 'test', '['])
|
|
546
|
+
const EXIT_ONE_OK_POWERSHELL = new Set(['grep', 'rg', 'egrep', 'fgrep'])
|
|
544
547
|
|
|
545
|
-
|
|
548
|
+
export type SpanShell = 'bash' | 'powershell'
|
|
549
|
+
|
|
550
|
+
const isCarveoutSegment = (segment: string, shell: SpanShell): boolean => {
|
|
546
551
|
const words = segment.trim().split(/\s+/)
|
|
547
552
|
if (words[0] === 'git') return words[1] === 'diff' || words[1] === 'grep'
|
|
548
|
-
return EXIT_ONE_OK.has(words[0])
|
|
553
|
+
return (shell === 'powershell' ? EXIT_ONE_OK_POWERSHELL : EXIT_ONE_OK).has(words[0])
|
|
549
554
|
}
|
|
550
555
|
|
|
551
|
-
function benignExitOne(command: string): boolean {
|
|
556
|
+
export function benignExitOne(command: string, shell: SpanShell = 'bash'): boolean {
|
|
552
557
|
const segments = splitSegments(command)
|
|
553
558
|
if (segments.length === 0) return false
|
|
554
559
|
// A `&&`/`||` chain can short-circuit, so an earlier segment's exit 1 becomes the
|
|
@@ -557,15 +562,15 @@ function benignExitOne(command: string): boolean {
|
|
|
557
562
|
// the exit benign whichever ran last. Without short-circuit operators the exit is
|
|
558
563
|
// the last segment's (a `|` pipeline exits with its final command, `;`/newline with
|
|
559
564
|
// the last statement), so the last segment decides.
|
|
560
|
-
if (/&&|\|\|/.test(command)) return segments.every(isCarveoutSegment)
|
|
561
|
-
return isCarveoutSegment(segments.at(-1) ?? '')
|
|
565
|
+
if (/&&|\|\|/.test(command)) return segments.every((segment) => isCarveoutSegment(segment, shell))
|
|
566
|
+
return isCarveoutSegment(segments.at(-1) ?? '', shell)
|
|
562
567
|
}
|
|
563
568
|
|
|
564
569
|
/** Run one injected span. A failure aborts the whole invocation, as Claude
|
|
565
570
|
* documents: the model never sees a half-expanded body. */
|
|
566
|
-
async function runSpan(exec: CommandExec, command: string, pattern: string): Promise<string> {
|
|
571
|
+
async function runSpan(exec: CommandExec, command: string, pattern: string, shell: SpanShell): Promise<string> {
|
|
567
572
|
const result = await exec(command)
|
|
568
|
-
if (result.code !== 0 && !(result.code === 1 && benignExitOne(command))) {
|
|
573
|
+
if (result.code !== 0 && !(result.code === 1 && benignExitOne(command, shell))) {
|
|
569
574
|
throw new Error(`Shell command failed for pattern "${pattern}"\n[stderr]\n${(result.stderr || result.stdout).trim()}`)
|
|
570
575
|
}
|
|
571
576
|
return result.stdout.trimEnd()
|
|
@@ -608,7 +613,7 @@ interface DynamicSpan {
|
|
|
608
613
|
* never re-scanned for further placeholders. Re-scanning was both a parity break
|
|
609
614
|
* (Claude expands once) and a command-injection path: output of a `` ```! `` block
|
|
610
615
|
* such as a commit message could smuggle its own `` !`cmd` `` for a later pass. */
|
|
611
|
-
export async function expandDynamicContent(body: string, cwd: string, exec: CommandExec): Promise<string> {
|
|
616
|
+
export async function expandDynamicContent(body: string, cwd: string, exec: CommandExec, shell: SpanShell = 'bash'): Promise<string> {
|
|
612
617
|
const blocks = fenceBlocks(body)
|
|
613
618
|
const protectedRanges = blocks.filter((block) => !block.exec).map((block): [number, number] => [block.start, block.end])
|
|
614
619
|
const execRanges = blocks.filter((block) => block.exec).map((block): [number, number] => [block.start, block.end])
|
|
@@ -618,14 +623,14 @@ export async function expandDynamicContent(body: string, cwd: string, exec: Comm
|
|
|
618
623
|
|
|
619
624
|
const spans: DynamicSpan[] = []
|
|
620
625
|
for (const block of blocks) {
|
|
621
|
-
if (block.exec) spans.push({ start: block.start, end: block.end, run: () => runSpan(exec, block.content, '```!') })
|
|
626
|
+
if (block.exec) spans.push({ start: block.start, end: block.end, run: () => runSpan(exec, block.content, '```!', shell) })
|
|
622
627
|
}
|
|
623
628
|
// `!` counts only at the start of a line or after whitespace; `KEY=!`cmd`` is literal.
|
|
624
629
|
const bashPattern = /(^|\s)!`([^`]+)`/g
|
|
625
630
|
for (let m = bashPattern.exec(body); m !== null; m = bashPattern.exec(body)) {
|
|
626
631
|
if (literal(m.index)) continue
|
|
627
632
|
const [span, lead, command] = m
|
|
628
|
-
spans.push({ start: m.index, end: m.index + span.length, run: async () => lead + (await runSpan(exec, command, `!\`${command}
|
|
633
|
+
spans.push({ start: m.index, end: m.index + span.length, run: async () => lead + (await runSpan(exec, command, `!\`${command}\``, shell)) })
|
|
629
634
|
}
|
|
630
635
|
const atPattern = /(^|\s)@(\S+)/g
|
|
631
636
|
for (let m = atPattern.exec(body); m !== null; m = atPattern.exec(body)) {
|
|
@@ -16,6 +16,7 @@ import * as fs from 'node:fs'
|
|
|
16
16
|
import * as path from 'node:path'
|
|
17
17
|
|
|
18
18
|
import { claudeConfigDir } from './config-dir.js'
|
|
19
|
+
import { readManagedSettings } from './managed-settings.js'
|
|
19
20
|
|
|
20
21
|
export interface InstalledPlugin {
|
|
21
22
|
name: string
|
|
@@ -169,16 +170,30 @@ export function installedPlugins(home: string, extraSettingsFiles: string[] = []
|
|
|
169
170
|
return plugins
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
/** The plugin's effective enablement per Claude's precedence: a managed
|
|
174
|
+
* enabledPlugins entry force-enables or blocks, then the user's setting, then the
|
|
175
|
+
* manifest's defaultEnabled, which defaults to true ("starts in an enabled state
|
|
176
|
+
* when the user has not set one"). */
|
|
177
|
+
function pluginEnabled(qualified: string, pluginDir: string, enabled: Record<string, boolean>, manifest: Record<string, unknown>): boolean {
|
|
178
|
+
const managedEntry = readManagedSettings().enabledPlugins
|
|
179
|
+
if (managedEntry !== null && typeof managedEntry === 'object') {
|
|
180
|
+
const managedState = (managedEntry as Record<string, unknown>)[qualified] ?? (managedEntry as Record<string, unknown>)[pluginDir]
|
|
181
|
+
if (typeof managedState === 'boolean') return managedState
|
|
182
|
+
}
|
|
183
|
+
const userState = enabled[qualified] ?? enabled[pluginDir]
|
|
184
|
+
if (typeof userState === 'boolean') return userState
|
|
185
|
+
return manifest.defaultEnabled !== false
|
|
186
|
+
}
|
|
187
|
+
|
|
172
188
|
/** Resolve one cached plugin directory into an enabled InstalledPlugin, or null to skip
|
|
173
|
-
* it:
|
|
189
|
+
* it: turned off by managed/user settings or defaultEnabled, or no version yet. */
|
|
174
190
|
function resolvePlugin(home: string, cacheDir: string, marketplace: string, pluginDir: string, enabled: Record<string, boolean>, configs: Record<string, Record<string, string>>): InstalledPlugin | null {
|
|
175
191
|
const qualified = `${pluginDir}@${marketplace}`
|
|
176
|
-
const state = enabled[qualified] ?? enabled[pluginDir]
|
|
177
|
-
if (state !== true) return null
|
|
178
192
|
const version = newestVersion(listDirs(path.join(cacheDir, marketplace, pluginDir)))
|
|
179
193
|
if (!version) return null
|
|
180
194
|
const root = path.join(cacheDir, marketplace, pluginDir, version)
|
|
181
195
|
const manifest = readJson(path.join(root, '.claude-plugin', 'plugin.json'))
|
|
196
|
+
if (!pluginEnabled(qualified, pluginDir, enabled, manifest)) return null
|
|
182
197
|
const name = typeof manifest.name === 'string' && manifest.name.length > 0 ? manifest.name : pluginDir
|
|
183
198
|
// Claude: "{id} is the plugin identifier with characters outside a-z, A-Z, 0-9,
|
|
184
199
|
// _, and - replaced by -", one dash per character, underscores kept.
|
package/extensions/mcp/index.ts
CHANGED
|
@@ -595,6 +595,34 @@ export default async function mcpExtension(pi: ExtensionAPI) {
|
|
|
595
595
|
status.clear()
|
|
596
596
|
})
|
|
597
597
|
|
|
598
|
+
// Claude references MCP resources with @server:uri mentions, fetched into the
|
|
599
|
+
// conversation when referenced. Only mentions naming a connected server expand;
|
|
600
|
+
// anything else (an email, a handle) stays untouched.
|
|
601
|
+
pi.on('input', async (event) => {
|
|
602
|
+
if (event.source === 'extension') return
|
|
603
|
+
const mentions = [...event.text.matchAll(/@([A-Za-z0-9_-]+):(\S+)/g)].filter((match) => clients.has(match[1]))
|
|
604
|
+
if (mentions.length === 0) return
|
|
605
|
+
const sections: string[] = []
|
|
606
|
+
for (const match of mentions) {
|
|
607
|
+
const [, server, uri] = match
|
|
608
|
+
try {
|
|
609
|
+
const client = clients.get(server)
|
|
610
|
+
if (!client) continue
|
|
611
|
+
const wall = callTimeoutMs()
|
|
612
|
+
const result = await withTimeout(client.readResource({ uri }, callRequestOptions(wall, callTuning(server))), wall, `read ${uri}`)
|
|
613
|
+
const text = (result.contents as Array<{ text?: string }>)
|
|
614
|
+
.map((entry) => entry.text)
|
|
615
|
+
.filter((value): value is string => typeof value === 'string')
|
|
616
|
+
.join('\n')
|
|
617
|
+
if (text) sections.push(capForContext(`<mcp-resource server="${server}" uri="${uri}">\n${text}\n</mcp-resource>`))
|
|
618
|
+
} catch {
|
|
619
|
+
// An unreadable resource leaves the mention as plain text.
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (sections.length === 0) return
|
|
623
|
+
return { action: 'transform', text: `${event.text}\n\n${sections.join('\n\n')}` }
|
|
624
|
+
})
|
|
625
|
+
|
|
598
626
|
pi.registerCommand('mcp', {
|
|
599
627
|
description: 'Show MCP server status and tools',
|
|
600
628
|
handler: async (_args, ctx) => {
|
package/extensions/skills.ts
CHANGED
|
@@ -25,11 +25,14 @@ import * as path from 'node:path'
|
|
|
25
25
|
import { type ExtensionAPI, type ExtensionContext, parseFrontmatter } from '@earendil-works/pi-coding-agent'
|
|
26
26
|
|
|
27
27
|
import { expandCommand, shellExecutionDisabled } from './commands.js'
|
|
28
|
+
import { runAgent } from './internal/agent-run.js'
|
|
28
29
|
import { parseCommandFile } from './internal/command-file.js'
|
|
29
30
|
import { claudeConfigDir } from './internal/config-dir.js'
|
|
31
|
+
import { managedSettingsFile } from './internal/managed-settings.js'
|
|
30
32
|
import { installedPlugins } from './internal/plugins.js'
|
|
31
33
|
import { isProjectApprovedSilently } from './internal/project-approval.js'
|
|
32
34
|
import { ancestorDirs } from './internal/project-root.js'
|
|
35
|
+
import { claudeSettingsChain } from './internal/settings-chain.js'
|
|
33
36
|
import { SKILL_HOOKS_CHANNEL } from './internal/skill-hooks.js'
|
|
34
37
|
|
|
35
38
|
function isDirectory(target: string): boolean {
|
|
@@ -45,7 +48,10 @@ function isDirectory(target: string): boolean {
|
|
|
45
48
|
* name and description to the model, so an untrusted repository would otherwise get
|
|
46
49
|
* text into the prompt without the user ever agreeing to load its config. */
|
|
47
50
|
export function skillDirs(cwd: string, home: string, trusted: boolean): string[] {
|
|
48
|
-
|
|
51
|
+
// Claude's precedence: enterprise (the skills directory beside the managed
|
|
52
|
+
// settings file) overrides personal, and personal overrides project; discovery
|
|
53
|
+
// here is first-match, so higher precedence goes first.
|
|
54
|
+
const candidates = [path.join(path.dirname(managedSettingsFile()), '.claude', 'skills'), path.join(claudeConfigDir(home), 'skills')]
|
|
49
55
|
// Enabled plugins contribute their skills directories. pi's loader names a
|
|
50
56
|
// skill by its directory, so a plugin skill registers without Claude's
|
|
51
57
|
// /plugin: prefix; a rename-free approximation, disclosed in the README.
|
|
@@ -125,11 +131,49 @@ export default function skillsExtension(pi: ExtensionAPI) {
|
|
|
125
131
|
})
|
|
126
132
|
}
|
|
127
133
|
|
|
134
|
+
/** Claude's `skillOverrides` value for one skill from the settings chain, later
|
|
135
|
+
* files winning: "off" hides the skill entirely, "name-only" trims its listing
|
|
136
|
+
* (a pi-loader surface, noted in docs). */
|
|
137
|
+
function skillOverrideFor(name: string, cwd: string, trusted: boolean): string | undefined {
|
|
138
|
+
let value: string | undefined
|
|
139
|
+
for (const file of claudeSettingsChain(cwd, os.homedir(), trusted)) {
|
|
140
|
+
try {
|
|
141
|
+
const overrides = JSON.parse(fs.readFileSync(file, 'utf-8')).skillOverrides
|
|
142
|
+
if (overrides !== null && typeof overrides === 'object' && typeof overrides[name] === 'string') value = overrides[name]
|
|
143
|
+
} catch {
|
|
144
|
+
// missing or invalid file: skip
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return value
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Claude's skillOverrides "off": the skill is hidden and does not run; the
|
|
151
|
+
* invocation is swallowed with a notice. Undefined lets the invocation proceed. */
|
|
152
|
+
function refusedByOverride(name: string, ctx: ExtensionContext, trusted: boolean): { action: 'handled' } | undefined {
|
|
153
|
+
if (skillOverrideFor(name, ctx.cwd, trusted) !== 'off') return undefined
|
|
154
|
+
if (ctx.hasUI) ctx.ui.notify(`Skill "${name}" is turned off by skillOverrides in settings.`, 'info')
|
|
155
|
+
return { action: 'handled' }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Claude's context: fork run: the expanded skill content becomes the prompt that
|
|
159
|
+
* drives a subagent, without the conversation history. Divergence: Claude
|
|
160
|
+
* backgrounds the fork by default; pi-code waits for the result in the invoking
|
|
161
|
+
* turn (Claude's background: false behavior, which is also what Claude itself
|
|
162
|
+
* does in -p and SDK runs). */
|
|
163
|
+
async function runForkedSkill(name: string, filePath: string, expanded: string, agentName: string | undefined): Promise<{ action: 'transform'; text: string }> {
|
|
164
|
+
try {
|
|
165
|
+
const output = await runAgent({ prompt: expanded, fullTools: true, ...(agentName ? { agent: agentName } : {}) })
|
|
166
|
+
return { action: 'transform', text: `<skill name="${name}" location="${filePath}">\nThe skill ran in a forked subagent (no conversation history shared). Its result:\n\n${output}\n</skill>` }
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return { action: 'transform', text: `<skill name="${name}">\nThe forked subagent run failed: ${error instanceof Error ? error.message : String(error)}\n</skill>` }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
128
172
|
/** A `/skill:name args` invocation into its expanded skill block, or undefined to
|
|
129
173
|
* pass the input through to pi untouched. The expanded body is wrapped in pi's
|
|
130
174
|
* skill-block format so downstream behavior (the baseDir note for relative
|
|
131
175
|
* references) matches an untouched invocation. */
|
|
132
|
-
async function expandSkillInvocation(pi: ExtensionAPI, rawText: string, ctx: ExtensionContext): Promise<{ action: 'transform'; text: string } | undefined> {
|
|
176
|
+
async function expandSkillInvocation(pi: ExtensionAPI, rawText: string, ctx: ExtensionContext): Promise<{ action: 'transform'; text: string } | { action: 'handled' } | undefined> {
|
|
133
177
|
const text = rawText.trimStart()
|
|
134
178
|
if (!text.startsWith('/skill:')) return
|
|
135
179
|
const space = text.indexOf(' ')
|
|
@@ -139,6 +183,8 @@ async function expandSkillInvocation(pi: ExtensionAPI, rawText: string, ctx: Ext
|
|
|
139
183
|
const trusted = isProjectApprovedSilently(ctx)
|
|
140
184
|
const found = findClaudeSkill(name, skillDirs(ctx.cwd, os.homedir(), trusted))
|
|
141
185
|
if (!found) return
|
|
186
|
+
const refused = refusedByOverride(name, ctx, trusted)
|
|
187
|
+
if (refused) return refused
|
|
142
188
|
let parsed: ReturnType<typeof parseCommandFile>
|
|
143
189
|
let content: string
|
|
144
190
|
try {
|
|
@@ -153,10 +199,14 @@ async function expandSkillInvocation(pi: ExtensionAPI, rawText: string, ctx: Ext
|
|
|
153
199
|
// Claude registers hooks a skill's frontmatter declares when the skill is
|
|
154
200
|
// invoked, for the rest of the session; the hooks extension owns running them,
|
|
155
201
|
// so the declaration is announced over the shared bus.
|
|
156
|
-
const
|
|
202
|
+
const frontmatter = parseFrontmatter<Record<string, unknown>>(content).frontmatter
|
|
203
|
+
const declaredHooks = frontmatter.hooks
|
|
157
204
|
if (declaredHooks !== null && typeof declaredHooks === 'object' && !Array.isArray(declaredHooks)) {
|
|
158
205
|
pi.events?.emit(SKILL_HOOKS_CHANNEL, { skillName: name, hooks: declaredHooks })
|
|
159
206
|
}
|
|
160
207
|
const expanded = await expandCommand(pi, parsed, args, { cwd: ctx.cwd }, found.filePath, undefined, { allowShell: !shellExecutionDisabled(ctx.cwd, os.homedir(), trusted) })
|
|
208
|
+
if (typeof frontmatter.context === 'string' && frontmatter.context.trim().toLowerCase() === 'fork') {
|
|
209
|
+
return runForkedSkill(name, found.filePath, expanded, typeof frontmatter.agent === 'string' ? frontmatter.agent.trim() : undefined)
|
|
210
|
+
}
|
|
161
211
|
return { action: 'transform', text: `<skill name="${name}" location="${found.filePath}">\nReferences are relative to ${found.baseDir}.\n\n${expanded}\n</skill>` }
|
|
162
212
|
}
|
|
@@ -650,6 +650,20 @@ export const AGENT_HOOK_SYSTEM = [
|
|
|
650
650
|
|
|
651
651
|
/** A throwaway agent config for one agent-hook run: read-only inspection tools, the
|
|
652
652
|
* hook's model (a fast default when unset), and the decision-returning system prompt. */
|
|
653
|
+
/** The agent a context: fork skill runs as when it names none: full toolset, no
|
|
654
|
+
* extra system prompt (the child keeps pi's default), the skill content as the
|
|
655
|
+
* task. */
|
|
656
|
+
function forkAgent(request: Pick<AgentRunRequest, 'model' | 'systemPrompt'>): AgentConfig {
|
|
657
|
+
return {
|
|
658
|
+
name: 'fork',
|
|
659
|
+
description: 'forked skill run',
|
|
660
|
+
systemPrompt: request.systemPrompt ?? '',
|
|
661
|
+
...(request.model ? { model: request.model } : {}),
|
|
662
|
+
source: 'builtin',
|
|
663
|
+
filePath: '',
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
653
667
|
export function buildHookAgent(request: Pick<AgentRunRequest, 'model' | 'systemPrompt'>): AgentConfig {
|
|
654
668
|
return {
|
|
655
669
|
name: 'agent-hook',
|
|
@@ -1467,7 +1481,10 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
1467
1481
|
// A subagent session must not spawn further agents; agent hooks inside one are
|
|
1468
1482
|
// skipped (the seam rejection is non-blocking in runAgentHook).
|
|
1469
1483
|
if (process.env.PI_CODE_SUBAGENT) throw new Error('agent hooks do not run inside a subagent')
|
|
1470
|
-
|
|
1484
|
+
// A context: fork skill names its agent, or runs with the full toolset;
|
|
1485
|
+
// agent hooks keep the read-only hook shape.
|
|
1486
|
+
const named = request.agent ? discoverAgents(hookCwd, 'both').agents.find((a) => a.name === request.agent) : undefined
|
|
1487
|
+
const agent = named ?? (request.fullTools ? forkAgent(request) : buildHookAgent(request))
|
|
1471
1488
|
const result = await runSingleAgent({
|
|
1472
1489
|
defaultCwd: hookCwd,
|
|
1473
1490
|
agents: [agent],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.39",
|
|
4
4
|
"description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, and subagents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|