pi-code 1.0.54 → 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.
@@ -325,7 +325,11 @@ export default function commandsExtension(pi: ExtensionAPI) {
325
325
  // escape as unhandled; surface it as a no-op instead of leaving the session silently
326
326
  // on the command's override model.
327
327
  set: (model) => {
328
- void pi.setModel(model as Parameters<typeof pi.setModel>[0]).catch(() => {})
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
+ })
329
333
  },
330
334
  })
331
335
  /** The thinking level to restore after a command's `effort:` override drove its run,
@@ -505,8 +509,12 @@ export default function commandsExtension(pi: ExtensionAPI) {
505
509
  let parsed: ParsedCommand
506
510
  try {
507
511
  parsed = parseCommandFile(fs.readFileSync(command.filePath, 'utf-8'))
508
- } catch {
509
- continue // an unreadable command file must not take down session start
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
510
518
  }
511
519
  discovered.set(command.name, command)
512
520
  // A user-only command stays off the tool description; it is still in the
@@ -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
- // An invalid regex matcher falls back to exact-name matching, as before.
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
- const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'))
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
  }
@@ -345,8 +345,20 @@ export function runHeadersHelper(command: string, env: NodeJS.ProcessEnv, resolv
345
345
  resolve({})
346
346
  return
347
347
  }
348
+ const server = env.CLAUDE_CODE_MCP_SERVER_NAME ?? 'the server'
348
349
  execFile(shell.file, shell.argsFor(command), { timeout: 10_000, env }, (error, stdout) => {
349
- resolve(error ? {} : parseHelperHeaders(stdout))
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)
350
362
  })
351
363
  })
352
364
  }
@@ -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; the store stays put.
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
- return null // malformed YAML must not abort discovery for the whole directory
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.54",
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",