pi-code 1.0.53 → 1.0.55
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 +22 -5
- package/extensions/git-checkpoint.ts +7 -4
- package/extensions/hooks/config.ts +4 -1
- package/extensions/hooks/matcher.ts +4 -2
- package/extensions/internal/plugins.ts +11 -2
- package/extensions/mcp/oauth-flow.ts +6 -0
- package/extensions/mcp/policy.ts +10 -2
- package/extensions/mcp/transport.ts +17 -3
- package/extensions/memory.ts +10 -3
- package/extensions/subagent/agents.ts +5 -2
- package/extensions/subagent/background.ts +7 -4
- package/package.json +1 -1
package/extensions/commands.ts
CHANGED
|
@@ -174,10 +174,19 @@ export function shellExecutionDisabled(cwd: string, home: string, trusted: boole
|
|
|
174
174
|
if (readManagedSettings().disableSkillShellExecution === true) return true
|
|
175
175
|
const files = claudeSettingsChain(cwd, home, trusted)
|
|
176
176
|
return files.some((file) => {
|
|
177
|
+
let raw: string
|
|
177
178
|
try {
|
|
178
|
-
|
|
179
|
+
raw = fs.readFileSync(file, 'utf-8')
|
|
179
180
|
} catch {
|
|
180
|
-
return false //
|
|
181
|
+
return false // no such file: genuinely not a policy statement
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
return (JSON.parse(raw) as Record<string, unknown>).disableSkillShellExecution === true
|
|
185
|
+
} catch {
|
|
186
|
+
// Present but unreadable: the user may believe this file disables skill shells, so
|
|
187
|
+
// say so rather than silently leaving them enabled.
|
|
188
|
+
console.warn(`pi-code: ignoring ${file}: not valid JSON; skill shell execution stays as the other settings leave it`)
|
|
189
|
+
return false
|
|
181
190
|
}
|
|
182
191
|
})
|
|
183
192
|
}
|
|
@@ -316,7 +325,11 @@ export default function commandsExtension(pi: ExtensionAPI) {
|
|
|
316
325
|
// escape as unhandled; surface it as a no-op instead of leaving the session silently
|
|
317
326
|
// on the command's override model.
|
|
318
327
|
set: (model) => {
|
|
319
|
-
|
|
328
|
+
// A refused switch leaves the turn on the session model rather than the one the
|
|
329
|
+
// command named, and the reply gives no sign of it, so the refusal is reported.
|
|
330
|
+
void pi.setModel(model as Parameters<typeof pi.setModel>[0]).catch((error: unknown) => {
|
|
331
|
+
console.warn(`pi-code-commands: could not switch to ${typeof model === 'object' && model !== null && 'id' in model ? String((model as { id: unknown }).id) : String(model)}: ${error instanceof Error ? error.message : String(error)}`)
|
|
332
|
+
})
|
|
320
333
|
},
|
|
321
334
|
})
|
|
322
335
|
/** The thinking level to restore after a command's `effort:` override drove its run,
|
|
@@ -496,8 +509,12 @@ export default function commandsExtension(pi: ExtensionAPI) {
|
|
|
496
509
|
let parsed: ParsedCommand
|
|
497
510
|
try {
|
|
498
511
|
parsed = parseCommandFile(fs.readFileSync(command.filePath, 'utf-8'))
|
|
499
|
-
} catch {
|
|
500
|
-
|
|
512
|
+
} catch (error) {
|
|
513
|
+
// An unreadable file must not take down session start, but the command is then
|
|
514
|
+
// absent from /help and unresolvable by the model, which looks like one that was
|
|
515
|
+
// never written.
|
|
516
|
+
console.warn(`pi-code-commands: ignoring ${command.filePath}: ${error instanceof Error ? error.message : String(error)}`)
|
|
517
|
+
continue
|
|
501
518
|
}
|
|
502
519
|
discovered.set(command.name, command)
|
|
503
520
|
// A user-only command stays off the tool description; it is still in the
|
|
@@ -210,7 +210,7 @@ export default function gitCheckpointExtension(pi: ExtensionAPI) {
|
|
|
210
210
|
}
|
|
211
211
|
// Written on every start, so repos that predate the sidecar pick it up too.
|
|
212
212
|
rememberWorkTree(shadowDir, ctx.cwd)
|
|
213
|
-
await mirrorLocalExcludes(ctx
|
|
213
|
+
await mirrorLocalExcludes(ctx)
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
/** git reads ignore rules from the tree's .gitignore files, the user's global excludes,
|
|
@@ -219,7 +219,8 @@ export default function gitCheckpointExtension(pi: ExtensionAPI) {
|
|
|
219
219
|
* would be snapshotted and restored. Mirror it into the shadow on every start; the
|
|
220
220
|
* global excludes stay untouched (core.excludesFile is single-valued, so pointing it
|
|
221
221
|
* at the repo file would replace them). */
|
|
222
|
-
async function mirrorLocalExcludes(
|
|
222
|
+
async function mirrorLocalExcludes(ctx: ExtensionContext): Promise<void> {
|
|
223
|
+
const cwd = ctx.cwd
|
|
223
224
|
if (!shadowDir) return
|
|
224
225
|
// Resolved through git so a linked worktree maps to its common dir; outside a repo
|
|
225
226
|
// git exits 128 and there is nothing to mirror.
|
|
@@ -233,8 +234,10 @@ export default function gitCheckpointExtension(pi: ExtensionAPI) {
|
|
|
233
234
|
} else {
|
|
234
235
|
fs.rmSync(target, { force: true })
|
|
235
236
|
}
|
|
236
|
-
} catch {
|
|
237
|
-
//
|
|
237
|
+
} catch (error) {
|
|
238
|
+
// Without the mirror, files the user excluded locally are snapshotted into the
|
|
239
|
+
// checkpoint store and restored by /rewind, so this is not a silent fallback.
|
|
240
|
+
ctx.ui.notify(`Checkpoints cannot honor this repository's .git/info/exclude: ${error instanceof Error ? error.message : String(error)}`, 'warning')
|
|
238
241
|
}
|
|
239
242
|
}
|
|
240
243
|
|
|
@@ -200,7 +200,10 @@ function mergeHooksJson(config: HooksConfig, raw: string, source: string, source
|
|
|
200
200
|
let parsed: { hooks?: HooksConfig }
|
|
201
201
|
try {
|
|
202
202
|
parsed = JSON.parse(raw)
|
|
203
|
-
} catch {
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// Every hook this source declares is now absent, a policy hook among them, so the
|
|
205
|
+
// failure is named rather than left to look like a file with no hooks in it.
|
|
206
|
+
console.warn(`pi-code-hooks: ignoring the hooks in ${source}: ${error instanceof Error ? error.message : String(error)}`)
|
|
204
207
|
return
|
|
205
208
|
}
|
|
206
209
|
for (const [event, matchers] of Object.entries(parsed?.hooks ?? {})) {
|
|
@@ -59,8 +59,10 @@ function compileMatcher(matcher: string): CompiledMatcher {
|
|
|
59
59
|
} else {
|
|
60
60
|
try {
|
|
61
61
|
compiled = { regex: new RegExp(matcher, 'i') }
|
|
62
|
-
} catch {
|
|
63
|
-
//
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// The fallback matches the literal text, which almost never matches a tool name, so
|
|
64
|
+
// the hook simply never fires. Say so: the matcher reads as merely wrong otherwise.
|
|
65
|
+
console.warn(`pi-code-hooks: matcher ${matcher} is not a valid regular expression (${error instanceof Error ? error.message : String(error)}); it will only match a tool of that exact name`)
|
|
64
66
|
compiled = { tokens: exactTokens(matcher) }
|
|
65
67
|
}
|
|
66
68
|
}
|
|
@@ -31,10 +31,19 @@ export interface InstalledPlugin {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function readJson(file: string): Record<string, unknown> {
|
|
34
|
+
let raw: string
|
|
34
35
|
try {
|
|
35
|
-
|
|
36
|
-
return parsed !== null && typeof parsed === 'object' ? parsed : {}
|
|
36
|
+
raw = fs.readFileSync(file, 'utf-8')
|
|
37
37
|
} catch {
|
|
38
|
+
return {} // no such file: nothing to read, and most callers expect that
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(raw)
|
|
42
|
+
return parsed !== null && typeof parsed === 'object' ? parsed : {}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// A manifest that does not parse leaves the plugin with no components at all, and
|
|
45
|
+
// settings that do not parse drop the enablement or configuration they carried.
|
|
46
|
+
console.warn(`pi-code-plugins: ignoring ${file}: ${error instanceof Error ? error.message : String(error)}`)
|
|
38
47
|
return {}
|
|
39
48
|
}
|
|
40
49
|
}
|
|
@@ -28,6 +28,12 @@ function asOAuthRequiredError(name: string, error: unknown): OAuthRequiredError
|
|
|
28
28
|
* (stored-token) connects do not pass through here and stay fully parallel. */
|
|
29
29
|
let oauthQueue: Promise<unknown> = Promise.resolve()
|
|
30
30
|
|
|
31
|
+
/** Test seam: the queue is module state that outlives a test, so one login left pending
|
|
32
|
+
* would serialize every later login in the same file behind it. */
|
|
33
|
+
export function resetOAuthQueue(): void {
|
|
34
|
+
oauthQueue = Promise.resolve()
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
export function serializeInteractiveOAuth<T>(run: () => Promise<T>): Promise<T> {
|
|
32
38
|
const result = oauthQueue.then(run, run)
|
|
33
39
|
oauthQueue = result.then(
|
package/extensions/mcp/policy.ts
CHANGED
|
@@ -97,11 +97,19 @@ function parsePolicyEntry(entry: unknown): McpPolicyEntry | undefined {
|
|
|
97
97
|
* honored scope sets the key; an empty list is an explicit lockdown (deny all). */
|
|
98
98
|
export function mcpAllowDeny(scopeFiles: string[] = [], managedFile: string = managedSettingsFile()): McpPolicy {
|
|
99
99
|
const read = (file: string): Record<string, unknown> => {
|
|
100
|
+
let raw: string
|
|
100
101
|
try {
|
|
101
|
-
|
|
102
|
+
raw = fs.readFileSync(file, 'utf-8')
|
|
103
|
+
} catch {
|
|
104
|
+
return {} // no such file: genuinely no policy
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(raw)
|
|
102
108
|
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
103
109
|
} catch {
|
|
104
|
-
//
|
|
110
|
+
// Present but unreadable: its allow and deny lists are not in force, and a deny list
|
|
111
|
+
// silently dropped leaves every server allowed.
|
|
112
|
+
console.warn(`pi-code-mcp: ignoring ${file}: not valid JSON; its MCP allow and deny lists are not applied`)
|
|
105
113
|
return {}
|
|
106
114
|
}
|
|
107
115
|
}
|
|
@@ -336,15 +336,29 @@ export async function connectWithRetries(name: string, config: ServerConfig, aut
|
|
|
336
336
|
* environment helperEnv built. It runs through the platform shell (/bin/sh; Git Bash
|
|
337
337
|
* or PowerShell on Windows). A failure, a 10s timeout, or a machine with no shell
|
|
338
338
|
* yields no extra headers rather than blocking the connection. */
|
|
339
|
-
function runHeadersHelper(command: string, env: NodeJS.ProcessEnv): Promise<Record<string, string>> {
|
|
339
|
+
export function runHeadersHelper(command: string, env: NodeJS.ProcessEnv, resolve_ = resolveShell): Promise<Record<string, string>> {
|
|
340
340
|
return new Promise((resolve) => {
|
|
341
|
-
|
|
341
|
+
// The resolver is a parameter so a test can drive both outcomes on any platform:
|
|
342
|
+
// stubbing the platform cannot produce a shell-less host on a machine that has one.
|
|
343
|
+
const shell = resolve_(undefined)
|
|
342
344
|
if (!shell) {
|
|
343
345
|
resolve({})
|
|
344
346
|
return
|
|
345
347
|
}
|
|
348
|
+
const server = env.CLAUDE_CODE_MCP_SERVER_NAME ?? 'the server'
|
|
346
349
|
execFile(shell.file, shell.argsFor(command), { timeout: 10_000, env }, (error, stdout) => {
|
|
347
|
-
|
|
350
|
+
if (error) {
|
|
351
|
+
// The connect proceeds unauthenticated and the server answers 401, which reads as
|
|
352
|
+
// a login problem rather than a helper that never produced a header.
|
|
353
|
+
console.warn(`pi-code-mcp: the headersHelper for ${server} failed: ${error.message}; connecting without the headers it would have supplied`)
|
|
354
|
+
resolve({})
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
const headers = parseHelperHeaders(stdout)
|
|
358
|
+
if (Object.keys(headers).length === 0 && stdout.trim().length > 0) {
|
|
359
|
+
console.warn(`pi-code-mcp: the headersHelper for ${server} produced no usable headers; it must print a JSON object of header names to string values`)
|
|
360
|
+
}
|
|
361
|
+
resolve(headers)
|
|
348
362
|
})
|
|
349
363
|
})
|
|
350
364
|
}
|
package/extensions/memory.ts
CHANGED
|
@@ -149,8 +149,10 @@ export function migrateLegacyStore(cwd: string): void {
|
|
|
149
149
|
if (legacy === current || !fs.existsSync(legacy)) continue
|
|
150
150
|
try {
|
|
151
151
|
fs.renameSync(legacy, current)
|
|
152
|
-
} catch {
|
|
153
|
-
// A failed migration must not take down session start
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// A failed migration must not take down session start, but the session then has no
|
|
154
|
+
// memories while they sit under the old slug, which reads as having lost them.
|
|
155
|
+
console.warn(`pi-code-memory: could not move ${legacy} to ${current}: ${error instanceof Error ? error.message : String(error)}; this session starts without those memories`)
|
|
154
156
|
}
|
|
155
157
|
return
|
|
156
158
|
}
|
|
@@ -230,7 +232,12 @@ function readMemory(dir: string, name: string): MemoryToolResult {
|
|
|
230
232
|
try {
|
|
231
233
|
const body = fs.readFileSync(path.join(dir, `${name}.md`), 'utf-8')
|
|
232
234
|
return { content: [{ type: 'text', text: capForContext(body) }], details: {} }
|
|
233
|
-
} catch {
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Only a missing file is "no such memory"; anything else (a directory in its place, a
|
|
237
|
+
// permission problem) sends the model hunting for a name that is actually there.
|
|
238
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
239
|
+
return { content: [{ type: 'text', text: `Memory ${name} could not be read: ${error instanceof Error ? error.message : String(error)}` }], details: {} }
|
|
240
|
+
}
|
|
234
241
|
return { content: [{ type: 'text', text: `No memory named ${name}.` }], details: {} }
|
|
235
242
|
}
|
|
236
243
|
}
|
|
@@ -177,8 +177,11 @@ function parseAgentFile(content: string, source: AgentSource, filePath: string,
|
|
|
177
177
|
let parsed: { frontmatter: Record<string, unknown>; body: string }
|
|
178
178
|
try {
|
|
179
179
|
parsed = parseFrontmatter<Record<string, unknown>>(content)
|
|
180
|
-
} catch {
|
|
181
|
-
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// Malformed YAML must not abort discovery for the whole directory, but a silent drop
|
|
182
|
+
// reads as "that agent does not exist", so it is named like the other rejections here.
|
|
183
|
+
console.warn(`pi-code-subagent: ignoring agent ${filePath}: its frontmatter could not be parsed (${error instanceof Error ? error.message : String(error)})`)
|
|
184
|
+
return null
|
|
182
185
|
}
|
|
183
186
|
const { frontmatter, body } = parsed
|
|
184
187
|
const name = agentName(frontmatter, filePath, pluginName)
|
|
@@ -204,7 +204,7 @@ export function resumeBackgroundRun(id: string, task: string, onComplete: (run:
|
|
|
204
204
|
if (!fs.existsSync(run.spawn.cwd)) return 'cwd-gone'
|
|
205
205
|
// Persisted so the rebuild happens once: rebuilding per resume leaked one temp
|
|
206
206
|
// prompt dir every follow-up.
|
|
207
|
-
const rebuilt = withRebuiltPrompt(run.spawn)
|
|
207
|
+
const rebuilt = withRebuiltPrompt(run.spawn, run.agent)
|
|
208
208
|
run.spawn = { ...run.spawn, args: rebuilt.args }
|
|
209
209
|
if (rebuilt.dir) run.rebuiltPromptDir = rebuilt.dir
|
|
210
210
|
// The task prompt is always the final argument: both spawn paths push it last and the
|
|
@@ -228,7 +228,7 @@ export function resumeBackgroundRun(id: string, task: string, onComplete: (run:
|
|
|
228
228
|
|
|
229
229
|
/** Re-point --system-prompt at a fresh file when the original is gone; `dir` is the
|
|
230
230
|
* temp dir created for it, which the run then owns. */
|
|
231
|
-
function withRebuiltPrompt(spawnSpec: BackgroundSpawn): { args: string[]; dir?: string } {
|
|
231
|
+
function withRebuiltPrompt(spawnSpec: BackgroundSpawn, agent: string): { args: string[]; dir?: string } {
|
|
232
232
|
const flag = spawnSpec.args.indexOf('--system-prompt')
|
|
233
233
|
if (flag === -1 || !spawnSpec.promptBody) return { args: spawnSpec.args }
|
|
234
234
|
const current = spawnSpec.args[flag + 1]
|
|
@@ -240,9 +240,12 @@ function withRebuiltPrompt(spawnSpec: BackgroundSpawn): { args: string[]; dir?:
|
|
|
240
240
|
const rebuilt = [...spawnSpec.args]
|
|
241
241
|
rebuilt[flag + 1] = file
|
|
242
242
|
return { args: rebuilt, dir }
|
|
243
|
-
} catch {
|
|
243
|
+
} catch (error) {
|
|
244
244
|
// Cannot rewrite it: drop the pair rather than hand pi a path it will treat as
|
|
245
|
-
// prompt text, which would replace the agent persona with a temp path.
|
|
245
|
+
// prompt text, which would replace the agent persona with a temp path. The child then
|
|
246
|
+
// runs as a plain assistant instead of the agent asked for, and nothing in its output
|
|
247
|
+
// says so, hence the notice.
|
|
248
|
+
console.warn(`pi-code-subagent: resuming ${agent} without its agent prompt: ${error instanceof Error ? error.message : String(error)}`)
|
|
246
249
|
return { args: spawnSpec.args.filter((_arg, i) => i !== flag && i !== flag + 1) }
|
|
247
250
|
}
|
|
248
251
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.55",
|
|
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, subagents, and goals",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|