pi-code 1.0.42 → 1.0.43

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.
@@ -121,6 +121,10 @@ function findMarkdownFiles(dir: string, basePath = '', visited = new Set<string>
121
121
  } catch {
122
122
  return results // a missing or unreadable directory must not take down session start
123
123
  }
124
+ // Raw readdir order is filesystem-dependent (hash order on ext4, sorted on
125
+ // APFS), so unsorted iteration injects rules into the prompt in a different
126
+ // order per machine. Pinned-locale sort makes the prompt reproducible.
127
+ entries.sort((a, b) => a.name.localeCompare(b.name, 'en'))
124
128
  for (const entry of entries) {
125
129
  const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name
126
130
  const fullPath = path.join(dir, entry.name)
@@ -299,7 +299,9 @@ export function additionalDirContextFiles(dir: string, includeLocal: boolean): A
299
299
  const names = fs
300
300
  .readdirSync(rulesDir)
301
301
  .filter((name) => name.endsWith('.md'))
302
- .sort((a, b) => a.localeCompare(b))
302
+ // Pinned locale: the default collator follows the host locale, which
303
+ // reorders names like ch/ci/h and makes prompt content machine-dependent.
304
+ .sort((a, b) => a.localeCompare(b, 'en'))
303
305
  candidates.push(...names.map((name) => path.join(rulesDir, name)))
304
306
  } catch {
305
307
  // no rules directory in this additional dir
@@ -262,11 +262,15 @@ export default function gitCheckpointExtension(pi: ExtensionAPI) {
262
262
  runNeedsSnapshot = true
263
263
  await ensureShadow(ctx)
264
264
  checkpoints.clear()
265
+ const stored: Checkpoint[] = []
265
266
  for (const entry of ctx.sessionManager.getEntries()) {
266
267
  if (entry.type !== 'custom' || entry.customType !== CUSTOM_TYPE) continue
267
268
  const checkpoint = entry.data as Checkpoint | undefined
268
- if (checkpoint?.entryId) checkpoints.set(checkpoint.entryId, checkpoint)
269
+ if (checkpoint?.entryId) stored.push(checkpoint)
269
270
  }
271
+ // The same cap the append path enforces: a resumed long session must not
272
+ // rebuild a rewind list beyond the per-session limit.
273
+ for (const checkpoint of capCheckpoints(stored)) checkpoints.set(checkpoint.entryId, checkpoint)
270
274
  })
271
275
 
272
276
  // A new agent loop starts a run: the next turn_start snapshots the pre-run tree.
@@ -531,13 +531,6 @@ function fenceBlocks(body: string): FenceBlock[] {
531
531
  return blocks
532
532
  }
533
533
 
534
- /** Spans of a body inside a protective fenced code block. */
535
- function fencedRanges(body: string): Array<[number, number]> {
536
- return fenceBlocks(body)
537
- .filter((block) => !block.exec)
538
- .map((block) => [block.start, block.end])
539
- }
540
-
541
534
  /** Exit 1 is a normal result for Claude's documented search and comparison commands
542
535
  * (no matches, files differ); exit 2 and up fails even for these. The PowerShell
543
536
  * shell uses a different set, which "includes grep and git diff but not find or
@@ -12,6 +12,7 @@
12
12
  * ~/.claude/plugins/data/<id>, id being the qualified name folded to dashes.
13
13
  */
14
14
 
15
+ import * as crypto from 'node:crypto'
15
16
  import * as fs from 'node:fs'
16
17
  import * as path from 'node:path'
17
18
 
@@ -49,9 +50,33 @@ function listDirs(dir: string): string[] {
49
50
  }
50
51
  }
51
52
 
52
- /** Version directories sort numerically segment-wise, so 1.10.0 beats 1.9.0. */
53
+ /** One version string split for comparison: optional v prefix dropped, numeric
54
+ * base segments, and whatever follows a dash as the prerelease tag. */
55
+ function parseVersion(version: string): { base: number[]; pre: string | undefined } {
56
+ const stripped = version.replace(/^v/i, '')
57
+ const dash = stripped.indexOf('-')
58
+ const base = (dash === -1 ? stripped : stripped.slice(0, dash)).split('.').map((segment) => Number.parseInt(segment, 10) || 0)
59
+ return { base, pre: dash === -1 ? undefined : stripped.slice(dash + 1) }
60
+ }
61
+
62
+ /** Semver ordering to the depth plugin cache dirs need: 1.10.0 beats 1.9.0,
63
+ * 10.0.0 beats v2.0.0, and a release outranks its own prerelease (a plain
64
+ * string sort got both of the latter wrong). */
65
+ function compareVersions(a: string, b: string): number {
66
+ const left = parseVersion(a)
67
+ const right = parseVersion(b)
68
+ for (let i = 0; i < Math.max(left.base.length, right.base.length); i++) {
69
+ const diff = (left.base[i] ?? 0) - (right.base[i] ?? 0)
70
+ if (diff !== 0) return diff
71
+ }
72
+ if (left.pre === right.pre) return 0
73
+ if (left.pre === undefined) return 1
74
+ if (right.pre === undefined) return -1
75
+ return left.pre.localeCompare(right.pre, 'en', { numeric: true })
76
+ }
77
+
53
78
  function newestVersion(versions: string[]): string | undefined {
54
- return [...versions].sort((a, b) => a.localeCompare(b, 'en', { numeric: true })).at(-1)
79
+ return [...versions].sort(compareVersions).at(-1)
55
80
  }
56
81
 
57
82
  /** The enablement map, later files winning per key, as settings scopes merge. */
@@ -101,7 +126,8 @@ export function resetInstalledPluginsCache(): void {
101
126
  pluginCache.clear()
102
127
  }
103
128
 
104
- /** mtime plus size, so a same-instant rewrite with different content still differs. */
129
+ /** mtime plus size; cheap, but blind to a same-size rewrite within one
130
+ * timestamp tick, so only directory-tree entries use it. */
105
131
  function statToken(target: string): string {
106
132
  try {
107
133
  const stat = fs.statSync(target)
@@ -111,15 +137,25 @@ function statToken(target: string): string {
111
137
  }
112
138
  }
113
139
 
140
+ /** Content hash for the small settings files: a same-size rewrite within one
141
+ * mtime tick (the settings-watch flake class) must still invalidate the cache. */
142
+ function contentToken(target: string): string {
143
+ try {
144
+ return crypto.createHash('sha256').update(fs.readFileSync(target)).digest('hex')
145
+ } catch {
146
+ return 'missing'
147
+ }
148
+ }
149
+
114
150
  /**
115
- * A cheap change signature for one home's plugin config: the settings files' stat
116
- * tokens plus the cache tree's directory names and mtimes down through each plugin's
151
+ * A cheap change signature for one home's plugin config: the settings files'
152
+ * content hashes plus the cache tree's directory names and mtimes down through each plugin's
117
153
  * version directories, and the stat token of the resolved (newest) version's manifest
118
154
  * so an in-place edit of it invalidates the cache. Costs a few stats where the full
119
155
  * walk reads and parses the settings and every manifest.
120
156
  */
121
157
  function pluginFingerprint(cacheDir: string, settingsFiles: string[]): string {
122
- const parts = settingsFiles.map(statToken)
158
+ const parts = settingsFiles.map(contentToken)
123
159
  for (const marketplace of listDirs(cacheDir)) {
124
160
  const marketplaceDir = path.join(cacheDir, marketplace)
125
161
  parts.push(`${marketplace}:${statToken(marketplaceDir)}`)
@@ -105,7 +105,9 @@ async function writePromptToTempFile(agentName: string, prompt: string): Promise
105
105
  return { dir: tmpDir, filePath }
106
106
  }
107
107
 
108
- function getPiInvocation(args: string[]): { command: string; args: string[] } {
108
+ /** Exported as a test seam: the fallbacks only fire in packaged distributions
109
+ * (bun single-file, compiled binary), which no CI run reaches naturally. */
110
+ export function getPiInvocation(args: string[]): { command: string; args: string[] } {
109
111
  const currentScript = process.argv[1]
110
112
  const isBunVirtualScript = currentScript?.startsWith('/$bunfs/root/')
111
113
  if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-code",
3
- "version": "1.0.42",
3
+ "version": "1.0.43",
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",