pi-code 1.0.67 → 1.0.68

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.
@@ -7,16 +7,41 @@
7
7
  * context-imports and the command path-scope guard all go through it).
8
8
  */
9
9
 
10
+ import * as os from 'node:os'
11
+ import * as path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+
10
14
  /** The tools that name a file pi-code acts on. */
11
15
  const FILE_TOOLS: ReadonlySet<string> = new Set(['read', 'edit', 'write'])
12
16
 
17
+ /** The spaces pi folds to a plain one before resolving (pi dist/utils/paths). */
18
+ const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g
19
+
20
+ /** `target` as pi's file tools read it before resolving against cwd: a leading @ (which
21
+ * some models add) stripped, ~ expanded, a file:// URL decoded, unicode spaces folded.
22
+ * pi does this in resolveToCwd (dist/core/tools/path-utils), which the package does not
23
+ * export. A reader that skips it judges a different file than the one pi opens: a
24
+ * command scoped to `Read(*.md)` allowed `~/secret/notes.md`, read as <cwd>/~/secret. */
25
+ function asPiReadsIt(target: string): string {
26
+ const folded = target.replace(UNICODE_SPACES, ' ')
27
+ const bare = folded.startsWith('@') ? folded.slice(1) : folded
28
+ if (bare === '~') return os.homedir()
29
+ if (bare.startsWith('~/') || (process.platform === 'win32' && bare.startsWith('~\\'))) return path.join(os.homedir(), bare.slice(2))
30
+ if (!bare.startsWith('file://')) return bare
31
+ try {
32
+ return fileURLToPath(bare)
33
+ } catch {
34
+ return bare
35
+ }
36
+ }
37
+
13
38
  /** The path a file tool call named, or undefined for any other tool, an errored call,
14
- * or a call that named none. The path is as the tool gave it: callers resolve it
15
- * against their own cwd. */
39
+ * or a call that named none. Normalised as pi normalises it, and still relative when
40
+ * the model gave a relative path: callers resolve it against their own cwd. */
16
41
  export function fileToolTarget(event: { toolName: string; input?: unknown; isError?: boolean }): string | undefined {
17
42
  if (event.isError === true) return undefined
18
43
  if (!FILE_TOOLS.has(event.toolName)) return undefined
19
44
  const input = event.input as { path?: unknown; file_path?: unknown } | undefined
20
45
  const target = typeof input?.path === 'string' ? input.path : input?.file_path
21
- return typeof target === 'string' && target.length > 0 ? target : undefined
46
+ return typeof target === 'string' && target.length > 0 ? asPiReadsIt(target) : undefined
22
47
  }
@@ -167,11 +167,37 @@ function refusedByOverride(name: string, ctx: ExtensionContext, trusted: boolean
167
167
  return { action: 'handled' }
168
168
  }
169
169
 
170
+ /** Forked skills still running in the background, by name. */
171
+ const runningForks = new Set<string>()
172
+
173
+ /** Whether a context: fork skill holds the invoking turn for its result. Claude: "The
174
+ * forked subagent runs in the background: you keep working while it runs, and its result
175
+ * arrives in your conversation when it completes. Set background: false in the frontmatter
176
+ * to instead wait for the result in the turn that invoked the skill." It also waits in
177
+ * non-interactive mode, when CLAUDE_CODE_DISABLE_BACKGROUND_TASKS is 1, and "when you
178
+ * invoke a forked skill while an earlier invocation of the same skill is still running". */
179
+ function forkWaits(name: string, frontmatter: Record<string, unknown>, ctx: ExtensionContext): boolean {
180
+ if (String(frontmatter.background).trim().toLowerCase() === 'false') return true
181
+ return !ctx.hasUI || process.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS === '1' || runningForks.has(name)
182
+ }
183
+
184
+ /** Start a forked skill in the background and hand its result to the conversation when it
185
+ * completes. The promise has no awaiter, and sendMessage on a pi whose session was
186
+ * replaced meanwhile throws: the catch keeps that from exiting pi, at the cost of the
187
+ * delivery, as a background subagent run's completion notice is lost the same way. */
188
+ function startForkedSkill(pi: ExtensionAPI, ctx: ExtensionContext, fork: { name: string; filePath: string; expanded: string; agentName: string | undefined }): { action: 'handled' } {
189
+ runningForks.add(fork.name)
190
+ ctx.ui.notify(`Skill ${fork.name} is running in a forked subagent in the background; its result arrives here when it completes.`, 'info')
191
+ void runForkedSkill(fork.name, fork.filePath, fork.expanded, fork.agentName)
192
+ .then((result) => pi.sendMessage({ customType: 'skill-fork', content: result.text, display: true }, { triggerTurn: true }))
193
+ .catch(() => {})
194
+ .finally(() => runningForks.delete(fork.name))
195
+ return { action: 'handled' }
196
+ }
197
+
170
198
  /** Claude's context: fork run: the expanded skill content becomes the prompt that
171
- * drives a subagent, without the conversation history. Divergence: Claude
172
- * backgrounds the fork by default; pi-code waits for the result in the invoking
173
- * turn (Claude's background: false behavior, which is also what Claude itself
174
- * does in -p and SDK runs). */
199
+ * drives a subagent, without the conversation history. Never rejects: a failed run
200
+ * is reported in the returned block. */
175
201
  async function runForkedSkill(name: string, filePath: string, expanded: string, agentName: string | undefined): Promise<{ action: 'transform'; text: string }> {
176
202
  try {
177
203
  const output = await runAgent({ prompt: expanded, fullTools: true, ...(agentName ? { agent: agentName } : {}) })
@@ -218,7 +244,9 @@ async function expandSkillInvocation(pi: ExtensionAPI, rawText: string, ctx: Ext
218
244
  }
219
245
  const expanded = await expandCommand(pi, parsed, args, { cwd: ctx.cwd }, found.filePath, undefined, { allowShell: !shellExecutionDisabled(ctx.cwd, os.homedir(), trusted) })
220
246
  if (typeof frontmatter.context === 'string' && frontmatter.context.trim().toLowerCase() === 'fork') {
221
- return runForkedSkill(name, found.filePath, expanded, typeof frontmatter.agent === 'string' ? frontmatter.agent.trim() : undefined)
247
+ const agentName = typeof frontmatter.agent === 'string' ? frontmatter.agent.trim() : undefined
248
+ if (forkWaits(name, frontmatter, ctx)) return runForkedSkill(name, found.filePath, expanded, agentName)
249
+ return startForkedSkill(pi, ctx, { name, filePath: found.filePath, expanded, agentName })
222
250
  }
223
251
  return { action: 'transform', text: `<skill name="${name}" location="${found.filePath}">\nReferences are relative to ${found.baseDir}.\n\n${expanded}\n</skill>` }
224
252
  }
@@ -114,12 +114,15 @@ function toNumber(value: string | undefined): number | undefined {
114
114
  }
115
115
 
116
116
  /** One rate-limit window from the `anthropic-ratelimit-<prefix>-*` header family,
117
- * taking a direct `-utilization` percentage when present, else computing it from
118
- * `-limit` and `-remaining`. resets_at comes from `-reset` when the header is set.
119
- * Header names vary, so only what is present is read and a bare number is enough. */
117
+ * taking `-utilization` when present, else computing the percentage from `-limit` and
118
+ * `-remaining`. `-utilization` is a 0 to 1 fraction, while Claude documents
119
+ * used_percentage as "from 0 to 100": it is scaled and rounded to one decimal, as
120
+ * Claude Code does (Math.round(utilization * 1000) / 10). resets_at comes from `-reset`
121
+ * when the header is set. Header names vary, so only what is present is read. */
120
122
  function readRateLimitWindow(headers: Record<string, string>, prefix: string): RateLimitWindow | undefined {
121
123
  const base = `anthropic-ratelimit-${prefix}`
122
- let usedPercentage = toNumber(headers[`${base}-utilization`])
124
+ const utilization = toNumber(headers[`${base}-utilization`])
125
+ let usedPercentage = utilization === undefined ? undefined : Math.round(utilization * 1000) / 10
123
126
  if (usedPercentage === undefined) {
124
127
  const limit = toNumber(headers[`${base}-limit`])
125
128
  const remaining = toNumber(headers[`${base}-remaining`])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-code",
3
- "version": "1.0.67",
3
+ "version": "1.0.68",
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",