pi-code 1.0.33 → 1.0.35
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.
- package/extensions/context-imports.ts +24 -21
- package/extensions/hooks/runners.ts +7 -1
- package/extensions/internal/builtin-styles/concise.md +13 -0
- package/extensions/internal/managed-settings.ts +37 -2
- package/extensions/internal/path-rules.ts +5 -0
- package/extensions/mcp/transport.ts +3 -1
- package/extensions/memory.ts +43 -11
- package/extensions/output-styles.ts +25 -4
- package/extensions/status-line.ts +31 -6
- package/package.json +1 -1
|
@@ -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.
|
|
@@ -99,8 +99,14 @@ export const runHookCommand: HookCommandRunner = (command, payload, timeoutMs, p
|
|
|
99
99
|
// the descendants too. CLAUDE_PROJECT_DIR is Claude's documented way for a hook to
|
|
100
100
|
// reference project files regardless of the shell's cwd. CLAUDECODE=1 marks every
|
|
101
101
|
// subprocess Claude spawns, so it is set on the child unconditionally.
|
|
102
|
-
|
|
102
|
+
// CLAUDE_CODE_CHILD_SESSION marks per-call children (hook and status line
|
|
103
|
+
// commands), never long-lived stdio MCP servers, as Claude documents; COLUMNS
|
|
104
|
+
// and LINES carry the terminal dimensions since the script's own width
|
|
105
|
+
// detection cannot see the captured terminal.
|
|
106
|
+
const env: NodeJS.ProcessEnv = { ...process.env, CLAUDECODE: '1', CLAUDE_CODE_CHILD_SESSION: '1' }
|
|
103
107
|
if (projectDir) env.CLAUDE_PROJECT_DIR = projectDir
|
|
108
|
+
if (process.stdout.columns) env.COLUMNS = String(process.stdout.columns)
|
|
109
|
+
if (process.stdout.rows) env.LINES = String(process.stdout.rows)
|
|
104
110
|
// An exec-form hook (an `args` array) spawns the executable directly with those args
|
|
105
111
|
// and no shell, so shell metacharacters in the args arrive literally; $ARGUMENTS in
|
|
106
112
|
// each arg is replaced with the event JSON by a replacer function (so $$/$& in the
|
|
@@ -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.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import * as fs from 'node:fs'
|
|
13
|
+
import * as path from 'node:path'
|
|
13
14
|
|
|
14
15
|
/** The OS managed-settings.json path Claude Code documents per platform. */
|
|
15
16
|
export function managedSettingsPath(platform: NodeJS.Platform = process.platform): string {
|
|
@@ -31,8 +32,7 @@ export function managedSettingsFile(): string {
|
|
|
31
32
|
return managedSettingsFileOverride ?? managedSettingsPath()
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
export function readManagedSettings(file: string = managedSettingsFileOverride ?? managedSettingsPath()): Record<string, unknown> {
|
|
35
|
+
function readOneSettingsFile(file: string): Record<string, unknown> {
|
|
36
36
|
try {
|
|
37
37
|
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'))
|
|
38
38
|
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as Record<string, unknown>
|
|
@@ -41,3 +41,38 @@ export function readManagedSettings(file: string = managedSettingsFileOverride ?
|
|
|
41
41
|
}
|
|
42
42
|
return {}
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
function isRecordValue(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Claude's managed-settings.d merge rules: a later single value replaces, lists
|
|
50
|
+
* combine with duplicates removed, and nested blocks merge key by key with each
|
|
51
|
+
* key following these same rules. */
|
|
52
|
+
function mergeManagedKey(base: unknown, next: unknown): unknown {
|
|
53
|
+
if (Array.isArray(base) && Array.isArray(next)) return [...new Set([...base, ...next])]
|
|
54
|
+
if (isRecordValue(base) && isRecordValue(next)) {
|
|
55
|
+
const merged: Record<string, unknown> = { ...base }
|
|
56
|
+
for (const [key, value] of Object.entries(next)) merged[key] = key in merged ? mergeManagedKey(merged[key], value) : value
|
|
57
|
+
return merged
|
|
58
|
+
}
|
|
59
|
+
return next
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The parsed managed settings object, or {} when absent or malformed. Claude also
|
|
63
|
+
* merges an optional managed-settings.d/ directory next to the file: every *.json
|
|
64
|
+
* in alphabetical order after the base file, hidden files and non-json ignored. */
|
|
65
|
+
export function readManagedSettings(file: string = managedSettingsFileOverride ?? managedSettingsPath()): Record<string, unknown> {
|
|
66
|
+
let merged = readOneSettingsFile(file)
|
|
67
|
+
const dropInDir = path.join(path.dirname(file), 'managed-settings.d')
|
|
68
|
+
let entries: string[]
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(dropInDir)
|
|
71
|
+
} catch {
|
|
72
|
+
return merged
|
|
73
|
+
}
|
|
74
|
+
for (const entry of entries.filter((name) => name.endsWith('.json') && !name.startsWith('.')).sort((a, b) => a.localeCompare(b, 'en'))) {
|
|
75
|
+
merged = mergeManagedKey(merged, readOneSettingsFile(path.join(dropInDir, entry))) as Record<string, unknown>
|
|
76
|
+
}
|
|
77
|
+
return merged
|
|
78
|
+
}
|
|
@@ -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)
|
|
@@ -184,7 +184,9 @@ function helperEnv(name: string, config: HttpServerConfig): NodeJS.ProcessEnv {
|
|
|
184
184
|
* env block, and Claude's path variables (CLAUDE_PROJECT_DIR, and CLAUDE_PLUGIN_ROOT
|
|
185
185
|
* for a plugin's server). */
|
|
186
186
|
function stdioEnv(config: StdioServerConfig, fill: (value: string) => string, session?: SessionDirs): Record<string, string> {
|
|
187
|
-
|
|
187
|
+
// CLAUDECODE marks every subprocess; the long-lived server deliberately gets no
|
|
188
|
+
// CLAUDE_CODE_CHILD_SESSION, which Claude reserves for per-call children.
|
|
189
|
+
const env: Record<string, string> = { ...getDefaultEnvironment(), CLAUDECODE: '1' }
|
|
188
190
|
for (const [key, value] of Object.entries(config.env ?? {})) env[key] = fill(value)
|
|
189
191
|
if (session) env.CLAUDE_PROJECT_DIR = session.projectDir
|
|
190
192
|
if (config.pluginRoot !== undefined) env.CLAUDE_PLUGIN_ROOT = config.pluginRoot
|
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
|
})
|
|
@@ -66,7 +66,8 @@ function formatCost(cost: number): string {
|
|
|
66
66
|
|
|
67
67
|
interface RateLimitWindow {
|
|
68
68
|
used_percentage: number
|
|
69
|
-
|
|
69
|
+
/** Unix epoch seconds when the window resets, per Claude's documented field. */
|
|
70
|
+
resets_at?: number
|
|
70
71
|
}
|
|
71
72
|
interface RateLimitSnapshot {
|
|
72
73
|
five_hour?: RateLimitWindow
|
|
@@ -109,10 +110,26 @@ function readRateLimitWindow(headers: Record<string, string>, prefix: string): R
|
|
|
109
110
|
// the computed value negative); clamp so the payload never carries a nonsense percentage.
|
|
110
111
|
const window: RateLimitWindow = { used_percentage: Math.max(0, Math.min(100, usedPercentage)) }
|
|
111
112
|
const resetsAt = headers[`${base}-reset`] ?? headers[`${base}-resets-at`]
|
|
112
|
-
if (resetsAt)
|
|
113
|
+
if (resetsAt) {
|
|
114
|
+
// Claude documents resets_at as Unix epoch seconds; the header carries an ISO
|
|
115
|
+
// timestamp (or, from some providers, a bare epoch number already).
|
|
116
|
+
const epoch = /^\d+$/.test(resetsAt.trim()) ? Number(resetsAt.trim()) : Math.floor(Date.parse(resetsAt) / 1000)
|
|
117
|
+
if (Number.isFinite(epoch)) window.resets_at = epoch
|
|
118
|
+
}
|
|
113
119
|
return window
|
|
114
120
|
}
|
|
115
121
|
|
|
122
|
+
/** The snapshot with expired windows dropped, so a window whose reset time has
|
|
123
|
+
* passed never lingers in the payload; undefined when nothing remains. */
|
|
124
|
+
function liveRateLimits(snapshot: RateLimitSnapshot): RateLimitSnapshot | undefined {
|
|
125
|
+
const nowSeconds = Date.now() / 1000
|
|
126
|
+
const keep = (window?: RateLimitWindow): RateLimitWindow | undefined => (window && (window.resets_at === undefined || window.resets_at > nowSeconds) ? window : undefined)
|
|
127
|
+
const fiveHour = keep(snapshot.five_hour)
|
|
128
|
+
const sevenDay = keep(snapshot.seven_day)
|
|
129
|
+
if (!fiveHour && !sevenDay) return undefined
|
|
130
|
+
return { ...(fiveHour ? { five_hour: fiveHour } : {}), ...(sevenDay ? { seven_day: sevenDay } : {}) }
|
|
131
|
+
}
|
|
132
|
+
|
|
116
133
|
/** The five-hour and seven-day utilization windows Claude's statusline reports,
|
|
117
134
|
* from the unified rate-limit response headers. Undefined when neither is present
|
|
118
135
|
* so a response without them never clobbers an earlier snapshot. */
|
|
@@ -216,7 +233,9 @@ export default function statusLine(pi: ExtensionAPI) {
|
|
|
216
233
|
session_id: ctx.sessionManager.getSessionId(),
|
|
217
234
|
cwd: ctx.cwd,
|
|
218
235
|
version: PACKAGE_VERSION,
|
|
219
|
-
|
|
236
|
+
// added_dirs is always empty: pi has no /add-dir; the field stays present
|
|
237
|
+
// because Claude documents "Empty array if none have been added".
|
|
238
|
+
workspace: { current_dir: ctx.cwd, project_dir: ctx.cwd, added_dirs: [] },
|
|
220
239
|
// Both fields, per Claude's documented contract: published statusline scripts
|
|
221
240
|
// read .model.display_name and render the literal "null" when it is missing.
|
|
222
241
|
model: { id: model?.id ?? '', display_name: model?.name ?? model?.id ?? '' },
|
|
@@ -255,13 +274,19 @@ export default function statusLine(pi: ExtensionAPI) {
|
|
|
255
274
|
const sessionName = ctx.sessionManager.getSessionName?.()
|
|
256
275
|
if (sessionName) payload.session_name = sessionName
|
|
257
276
|
if (ctx.thinkingLevel) {
|
|
258
|
-
|
|
277
|
+
// pi's off/minimal are outside Claude's effort vocabulary: minimal maps to
|
|
278
|
+
// low, and off omits effort entirely (thinking disabled says the rest).
|
|
259
279
|
payload.thinking = { enabled: ctx.thinkingLevel !== 'off' }
|
|
280
|
+
if (ctx.thinkingLevel !== 'off') payload.effort = { level: ctx.thinkingLevel === 'minimal' ? 'low' : ctx.thinkingLevel }
|
|
260
281
|
}
|
|
261
282
|
if (styleName) payload.output_style = { name: styleName }
|
|
262
283
|
// The current utilization of the account's rate-limit windows, when the
|
|
263
|
-
// provider reported them; omitted
|
|
264
|
-
|
|
284
|
+
// provider reported them; omitted until a response has carried them, and an
|
|
285
|
+
// expired window is dropped rather than left stale.
|
|
286
|
+
if (rateLimits) {
|
|
287
|
+
const live = liveRateLimits(rateLimits)
|
|
288
|
+
if (live) payload.rate_limits = live
|
|
289
|
+
}
|
|
265
290
|
return payload
|
|
266
291
|
}
|
|
267
292
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.35",
|
|
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",
|