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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
739
|
-
|
|
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
|
-
|
|
746
|
-
|
|
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)
|
package/extensions/memory.ts
CHANGED
|
@@ -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
|
-
|
|
160
|
-
//
|
|
161
|
-
|
|
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: [
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|