pi-code 1.0.34 → 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.
|
@@ -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
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -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",
|