pi-code 1.0.33 → 1.0.34

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.
@@ -81,6 +81,21 @@ import { fenceMarker, stripBlockComments } from './internal/strip-comments.js'
81
81
 
82
82
  /** Claude documents "a maximum depth of four hops" for recursive imports. */
83
83
  const MAX_IMPORT_DEPTH = 4
84
+
85
+ /** Claude loads a context file (CLAUDE.md and friends) of up to 4 MiB in full and
86
+ * skips a larger one. */
87
+ const CONTEXT_FILE_MAX_BYTES = 4 * 1024 * 1024
88
+
89
+ /** One context file's content, or undefined when it is absent, unreadable, or over
90
+ * the 4 MiB limit Claude documents. */
91
+ function readContextFile(filePath: string): string | undefined {
92
+ try {
93
+ if (fs.statSync(filePath).size > CONTEXT_FILE_MAX_BYTES) return undefined
94
+ return fs.readFileSync(filePath, 'utf-8')
95
+ } catch {
96
+ return undefined
97
+ }
98
+ }
84
99
  export const MAX_IMPORT_FILES = 50
85
100
  export const MAX_IMPORT_BYTES = 256 * 1024
86
101
 
@@ -292,11 +307,8 @@ export function additionalDirContextFiles(dir: string, includeLocal: boolean): A
292
307
  if (includeLocal) candidates.push(path.join(dir, 'CLAUDE.local.md'))
293
308
  const files: Array<{ path: string; content: string }> = []
294
309
  for (const candidate of candidates) {
295
- try {
296
- files.push({ path: candidate, content: fs.readFileSync(candidate, 'utf-8') })
297
- } catch {
298
- // absent or unreadable: treat as not there
299
- }
310
+ const content = readContextFile(candidate)
311
+ if (content !== undefined) files.push({ path: candidate, content })
300
312
  }
301
313
  return files
302
314
  }
@@ -717,12 +729,9 @@ export default function contextImportsExtension(pi: ExtensionAPI) {
717
729
 
718
730
  // ~/.claude/CLAUDE.md, Claude's user-scope memory. The user's own file, so no
719
731
  // project approval is required; a missing file simply leaves it unset.
720
- try {
721
- const userClaudeMd = path.join(claudeConfigDir(os.homedir()), 'CLAUDE.md')
722
- userContext = { path: userClaudeMd, content: fs.readFileSync(userClaudeMd, 'utf-8') }
723
- } catch {
724
- // no user CLAUDE.md
725
- }
732
+ const userClaudeMd = path.join(claudeConfigDir(os.homedir()), 'CLAUDE.md')
733
+ const userContent = readContextFile(userClaudeMd)
734
+ if (userContent !== undefined) userContext = { path: userClaudeMd, content: userContent }
726
735
 
727
736
  // CLAUDE.local.md is Claude Code's personal sidecar of CLAUDE.md; pi's own loader
728
737
  // skips it. A cloned repo can ship one, so it is gated like other project config.
@@ -735,18 +744,12 @@ export default function contextImportsExtension(pi: ExtensionAPI) {
735
744
  const dotClaudeMd = findNearestFile(ctx.cwd, path.join('.claude', 'CLAUDE.md'))
736
745
  if ((candidates.length > 0 || dotClaudeMd !== null) && (await isProjectApproved(ctx))) {
737
746
  for (const candidate of candidates) {
738
- try {
739
- localContexts.push({ path: candidate, content: fs.readFileSync(candidate, 'utf-8') })
740
- } catch {
741
- // unreadable: treat as absent
742
- }
747
+ const content = readContextFile(candidate)
748
+ if (content !== undefined) localContexts.push({ path: candidate, content })
743
749
  }
744
750
  if (dotClaudeMd !== null) {
745
- try {
746
- projectDotClaude = { path: dotClaudeMd, content: fs.readFileSync(dotClaudeMd, 'utf-8') }
747
- } catch {
748
- // unreadable: treat as absent
749
- }
751
+ const content = readContextFile(dotClaudeMd)
752
+ if (content !== undefined) projectDotClaude = { path: dotClaudeMd, content }
750
753
  }
751
754
  }
752
755
  // Read after the local-context flow so an approval it just recorded is honored.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: Concise
3
+ description: Leads with the result and keeps responses short by default, without cutting the engineering work
4
+ keep-coding-instructions: true
5
+ ---
6
+
7
+ Lead with the result. Skip preamble, narration, and restating the request; answer first, support after.
8
+
9
+ Keep responses short by default while doing the engineering work as thoroughly as ever: brevity applies to the writing, never to the analysis, testing, or care taken.
10
+
11
+ When the user asks for an explanation or more detail, answer in full.
12
+
13
+ Always keep the complete content of error reports, security warnings, and confirmations for destructive actions; never shorten those.
@@ -198,9 +198,14 @@ export function globCompileStats(): { compiled: number; evaluated: number } {
198
198
  /** Rule `paths:` globs compiled once for repeated matching, with claude-rules'
199
199
  * pathMatchesGlobs semantics: `./` and leading `/` anchors are stripped, a trailing
200
200
  * slash scopes to the directory's contents, and blank entries drop out. */
201
+ /** Claude's shared list budget: rule patterns past ~1000 compiled entries are
202
+ * ignored rather than compiled without bound. */
203
+ const LIST_PATTERN_BUDGET = 1000
204
+
201
205
  export function compileGlobs(globs: string[]): CompiledGlob[] {
202
206
  const compiled: CompiledGlob[] = []
203
207
  for (const raw of globs) {
208
+ if (compiled.length >= LIST_PATTERN_BUDGET) break
204
209
  let glob = raw.trim()
205
210
  if (!glob) continue
206
211
  if (glob.startsWith('./')) glob = glob.slice(2)
@@ -15,6 +15,7 @@ import { StringEnum } from '@earendil-works/pi-ai'
15
15
  import { type ExtensionAPI, withFileMutationQueue } from '@earendil-works/pi-coding-agent'
16
16
  import { Type } from 'typebox'
17
17
  import { claudeConfigDir } from './internal/config-dir.js'
18
+ import { readManagedSettings } from './internal/managed-settings.js'
18
19
  import { capForContext } from './internal/output-guard.js'
19
20
  import { isProjectApprovedSilently } from './internal/project-approval.js'
20
21
  import { repoRoot } from './internal/project-root.js'
@@ -141,6 +142,18 @@ export function indexWouldOverflow(index: string, name: string, description: str
141
142
  return next.split('\n').length > INDEX_MAX_LINES || Buffer.byteLength(next, 'utf-8') > INDEX_MAX_BYTES
142
143
  }
143
144
 
145
+ /** Where the index stands against the read limits, measured on the loaded content
146
+ * (frontmatter and comments stripped): 'over' past either bound, 'near' within
147
+ * 10% of one, else 'ok'. Claude reminds near a limit and errors over it. */
148
+ export function indexReadState(index: string): 'ok' | 'near' | 'over' {
149
+ const loaded = stripNonLoaded(index)
150
+ const lines = loaded.split('\n').length
151
+ const bytes = Buffer.byteLength(loaded, 'utf-8')
152
+ if (lines > INDEX_MAX_LINES || bytes > INDEX_MAX_BYTES) return 'over'
153
+ if (lines > INDEX_MAX_LINES * 0.9 || bytes > INDEX_MAX_BYTES * 0.9) return 'near'
154
+ return 'ok'
155
+ }
156
+
144
157
  type MemoryToolResult = { content: Array<{ type: 'text'; text: string }>; details: Record<string, never> }
145
158
 
146
159
  /** Write a memory and its index line, or say why it cannot be written. The whole
@@ -156,18 +169,29 @@ async function saveMemory(dir: string, indexPath: string, name: string | undefin
156
169
  }
157
170
  return withFileMutationQueue(indexPath, async (): Promise<MemoryToolResult> => {
158
171
  const index = readIndex(dir)
159
- // Claude reports an explicit error rather than writing a memory the next session
160
- // would never load, and says what to do about it.
161
- if (indexWouldOverflow(index, name, description)) {
172
+ fs.mkdirSync(dir, { recursive: true })
173
+ // A memory with frontmatter records its write time; one without is left as-is.
174
+ fs.writeFileSync(path.join(dir, `${name}.md`), stampModified(content, now))
175
+ const nextIndex = upsertIndexLine(index, name, description)
176
+ writeIndex(indexPath, nextIndex)
177
+ // Claude measures the index after the write: over a read limit the write still
178
+ // succeeds, but an error tells Claude to rewrite the index (everything past
179
+ // the limit is dropped on the next load); near a limit, a reminder to shorten.
180
+ const state = indexReadState(nextIndex)
181
+ if (state === 'over') {
162
182
  return {
163
- content: [{ type: 'text', text: `Memory index is full (${INDEX_MAX_LINES} entries or ${INDEX_MAX_BYTES} bytes). Delete or consolidate memories before saving ${name}.` }],
183
+ content: [
184
+ {
185
+ type: 'text',
186
+ text: `Saved memory ${name}, but the memory index is over its read limit (${INDEX_MAX_LINES} lines / ${INDEX_MAX_BYTES} bytes): rewrite MEMORY.md now. Keep one line per entry, move detail into topic files, and merge or drop stale entries; everything past the limit is dropped on the next load.`,
187
+ },
188
+ ],
164
189
  details: {},
165
190
  }
166
191
  }
167
- fs.mkdirSync(dir, { recursive: true })
168
- // A memory with frontmatter records its write time; one without is left as-is.
169
- fs.writeFileSync(path.join(dir, `${name}.md`), stampModified(content, now))
170
- writeIndex(indexPath, upsertIndexLine(index, name, description))
192
+ if (state === 'near') {
193
+ return { content: [{ type: 'text', text: `Saved memory ${name}. The memory index is near its read limit; shorten it: keep one line per entry, move detail into topic files, and merge or drop stale entries.` }], details: {} }
194
+ }
171
195
  return { content: [{ type: 'text', text: `Saved memory ${name}.` }], details: {} }
172
196
  })
173
197
  }
@@ -294,8 +318,9 @@ export function memorySettingsFiles(cwd: string, home: string, approved: boolean
294
318
  return claudeSettingsChain(cwd, home, approved)
295
319
  }
296
320
 
297
- /** Merge the two memory settings across the chain, later files winning per key. */
298
- export function readMemorySettings(files: string[]): { autoMemoryEnabled?: unknown; autoMemoryDirectory?: unknown } {
321
+ /** Merge the two memory settings across the chain, later files winning per key;
322
+ * managed policy settings win over every file, per Claude's settings precedence. */
323
+ export function readMemorySettings(files: string[], managed: Record<string, unknown> = readManagedSettings()): { autoMemoryEnabled?: unknown; autoMemoryDirectory?: unknown } {
299
324
  const merged: { autoMemoryEnabled?: unknown; autoMemoryDirectory?: unknown } = {}
300
325
  for (const file of files) {
301
326
  try {
@@ -307,6 +332,8 @@ export function readMemorySettings(files: string[]): { autoMemoryEnabled?: unkno
307
332
  // missing or invalid settings file: skip
308
333
  }
309
334
  }
335
+ if ('autoMemoryEnabled' in managed) merged.autoMemoryEnabled = managed.autoMemoryEnabled
336
+ if ('autoMemoryDirectory' in managed) merged.autoMemoryDirectory = managed.autoMemoryDirectory
310
337
  return merged
311
338
  }
312
339
 
@@ -405,7 +432,8 @@ export default function memoryExtension(pi: ExtensionAPI) {
405
432
  pi.registerTool({
406
433
  name: 'memory',
407
434
  label: 'Memory',
408
- description: 'Persistent memory across sessions. Save durable facts, user preferences, corrections, and project decisions that are not derivable from the code. Actions: save (name + description + content), read (name), delete (name), list.',
435
+ description:
436
+ 'Persistent memory across sessions. Save durable facts, user preferences, corrections, and project decisions that are not derivable from the code. Give each saved memory `type` frontmatter from the documented vocabulary: user (who the user is), feedback (guidance on how to work), project (ongoing work and constraints), or reference (pointers to external resources). Actions: save (name + description + content), read (name), delete (name), list.',
409
437
  parameters: MemoryParams,
410
438
  async execute(_id, params) {
411
439
  if (inSubagent()) {
@@ -492,6 +520,10 @@ export default function memoryExtension(pi: ExtensionAPI) {
492
520
  ` Index: ${path.join(store, INDEX_FILE)}`,
493
521
  ` User memory (CLAUDE.md): ${path.join(home, '.claude', 'CLAUDE.md')}`,
494
522
  ` Project memory (CLAUDE.md): ${path.join(ctx.cwd, 'CLAUDE.md')}`,
523
+ // Claude's /memory lists every documented location, including files that
524
+ // do not exist yet.
525
+ ` Project memory (CLAUDE.local.md): ${path.join(ctx.cwd, 'CLAUDE.local.md')}`,
526
+ ` Project memory (alternate): ${path.join(ctx.cwd, '.claude', 'CLAUDE.md')}`,
495
527
  'Toggle with /memory on or /memory off.',
496
528
  ]
497
529
  ctx.ui.notify(lines.join('\n'), 'info')
@@ -25,6 +25,7 @@ import * as path from 'node:path'
25
25
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
26
26
 
27
27
  import { claudeConfigDir } from './internal/config-dir.js'
28
+ import { readManagedSettings } from './internal/managed-settings.js'
28
29
  import { installedPlugins } from './internal/plugins.js'
29
30
  import { isProjectApproved } from './internal/project-approval.js'
30
31
  import { ancestorDirs, findNearestDir, findNearestFile } from './internal/project-root.js'
@@ -35,6 +36,8 @@ export interface OutputStyle {
35
36
  description: string
36
37
  body: string
37
38
  keepCodingInstructions: boolean
39
+ /** Claude's `force-for-plugin`: a plugin style applying automatically. */
40
+ forceForPlugin: boolean
38
41
  }
39
42
 
40
43
  function field(frontmatter: string, key: string): string {
@@ -47,7 +50,20 @@ export function parseStyle(content: string, fallbackName: string): OutputStyle {
47
50
  const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content)
48
51
  const frontmatter = match ? match[1] : ''
49
52
  const body = match ? content.slice(match[0].length) : content
50
- return { name: field(frontmatter, 'name') || fallbackName, description: field(frontmatter, 'description'), body: body.trim(), keepCodingInstructions: field(frontmatter, 'keep-coding-instructions') === 'true' }
53
+ return {
54
+ name: field(frontmatter, 'name') || fallbackName,
55
+ description: field(frontmatter, 'description'),
56
+ body: body.trim(),
57
+ keepCodingInstructions: field(frontmatter, 'keep-coding-instructions') === 'true',
58
+ forceForPlugin: field(frontmatter, 'force-for-plugin') === 'true',
59
+ }
60
+ }
61
+
62
+ /** Claude's `force-for-plugin` (plugin output styles only): the first loaded style
63
+ * carrying it applies automatically, overriding the outputStyle setting. The
64
+ * caller passes only plugin-loaded styles. */
65
+ export function forcedPluginStyle(styles: OutputStyle[]): OutputStyle | undefined {
66
+ return styles.find((style) => style.forceForPlugin)
51
67
  }
52
68
 
53
69
  /** Equivalents of Claude's built-in styles, shipped with pi-code as the
@@ -137,8 +153,10 @@ export function settingsFiles(cwd: string, home: string, trusted: boolean): stri
137
153
  return claudeSettingsChain(cwd, home, trusted)
138
154
  }
139
155
 
140
- /** The `outputStyle` recorded in settings, last file winning. */
141
- export function readActiveStyleName(files: string[]): string | undefined {
156
+ /** The `outputStyle` recorded in settings, last file winning; a managed policy
157
+ * value wins over every file, per Claude's settings precedence. */
158
+ export function readActiveStyleName(files: string[], managed: Record<string, unknown> = readManagedSettings()): string | undefined {
159
+ if (typeof managed.outputStyle === 'string') return managed.outputStyle
142
160
  let name: string | undefined
143
161
  for (const file of files) {
144
162
  try {
@@ -186,7 +204,10 @@ export default function outputStylesExtension(pi: ExtensionAPI) {
186
204
  const nearestLocal = findNearestFile(ctx.cwd, path.join('.claude', 'settings.local.json'))
187
205
  const claudeDir = findNearestDir(ctx.cwd, '.claude') ?? path.join(ctx.cwd, '.claude')
188
206
  localSettingsPath = nearestLocal ?? path.join(claudeDir, 'settings.local.json')
189
- activeName = readActiveStyleName(settingsFiles(ctx.cwd, home, trusted))
207
+ // Claude's force-for-plugin: the first loaded forced plugin style applies
208
+ // automatically, overriding the outputStyle setting.
209
+ const forced = forcedPluginStyle(loadStyles(pluginStyleDirs(home)))
210
+ activeName = forced?.name ?? readActiveStyleName(settingsFiles(ctx.cwd, home, trusted))
190
211
  const active = styleForName(styles, activeName)
191
212
  if (active) ctx.ui.notify(`Output style: ${active.name}`, 'info')
192
213
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-code",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
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",