pi-code 1.0.36 → 1.0.37
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/commands.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* handing `.claude/commands` to pi's prompt-template loader. Owning registration
|
|
6
6
|
* is what makes the rest of Claude's command contract reachable: `$ARGUMENTS` and
|
|
7
7
|
* positional substitution, `` !`cmd` `` bash output, `@file` inlining, subdirectory
|
|
8
|
-
* commands (
|
|
9
|
-
*
|
|
8
|
+
* commands (named by file name alone, as Claude documents; subdirectories only
|
|
9
|
+
* organize the files), and the
|
|
10
10
|
* `allowed-tools`, `argument-hint` and `model` frontmatter (`model` switches the
|
|
11
11
|
* session model for the command's run via `pi.setModel`, restored on agent_end).
|
|
12
12
|
* `shell: powershell` runs a command's injected spans through PowerShell when a
|
|
@@ -47,7 +47,7 @@ import { Type } from 'typebox'
|
|
|
47
47
|
import { matchesBashRules } from './internal/bash-rules.js'
|
|
48
48
|
import { type CommandExec, type DiscoveredCommand, discoverCommandFiles, expandDynamicContent, type ParsedCommand, parseCommandFile, resolvePowershellBinary, spanExec, substituteArgsDetailed, substituteVars } from './internal/command-file.js'
|
|
49
49
|
import { claudeConfigDir } from './internal/config-dir.js'
|
|
50
|
-
import { readManagedSettings } from './internal/managed-settings.js'
|
|
50
|
+
import { managedSettingsFile, readManagedSettings } from './internal/managed-settings.js'
|
|
51
51
|
import { capForContext } from './internal/output-guard.js'
|
|
52
52
|
import { matchesPathRules } from './internal/path-rules.js'
|
|
53
53
|
import { type InstalledPlugin, installedPlugins } from './internal/plugins.js'
|
|
@@ -115,15 +115,17 @@ function isDirectory(target: string): boolean {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
/** Existing `.claude/commands` directories
|
|
119
|
-
*
|
|
120
|
-
*
|
|
118
|
+
/** Existing `.claude/commands` directories in Claude's precedence order (later
|
|
119
|
+
* directories win in collectCommands): project first, then personal, then the
|
|
120
|
+
* enterprise directory beside the managed settings file, per "enterprise
|
|
121
|
+
* overrides personal, and personal overrides project". The project directories
|
|
122
|
+
* are included only for approved projects. */
|
|
121
123
|
export function commandDirs(cwd: string, home: string, trusted: boolean): string[] {
|
|
122
|
-
const candidates = [
|
|
124
|
+
const candidates: string[] = []
|
|
123
125
|
// Claude scans every .claude/commands between cwd and the repository root, the
|
|
124
|
-
// nearest winning
|
|
125
|
-
// the project list goes root-first with the nearest last.
|
|
126
|
+
// nearest winning an intra-project name clash: root-first with the nearest last.
|
|
126
127
|
if (trusted) candidates.push(...ancestorDirs(cwd, path.join('.claude', 'commands')).reverse())
|
|
128
|
+
candidates.push(path.join(claudeConfigDir(home), 'commands'), path.join(path.dirname(managedSettingsFile()), '.claude', 'commands'))
|
|
127
129
|
const dirs: string[] = []
|
|
128
130
|
for (const dir of candidates) {
|
|
129
131
|
if (!dirs.includes(dir) && isDirectory(dir)) dirs.push(dir)
|
|
@@ -270,23 +272,22 @@ export function slashCommandBudget(contextWindow: number | undefined, env: Recor
|
|
|
270
272
|
/** The slash_command tool description: usage framing plus the budgeted command
|
|
271
273
|
* list, each entry `/name - description (argument-hint)`. */
|
|
272
274
|
export function slashCommandToolDescription(commands: SlashCommandEntry[], budget: number): string {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
// Claude: "The listing always contains every skill name"; the budget shortens
|
|
276
|
+
// descriptions (later entries lose theirs first), never drops a name, so an
|
|
277
|
+
// omitted-but-invocable command can no longer contradict the listing.
|
|
278
|
+
const lines = commands.map((command) => `/${command.name}`)
|
|
279
|
+
let used = lines.reduce((total, line) => total + line.length + 1, 0)
|
|
280
|
+
for (const [index, command] of commands.entries()) {
|
|
277
281
|
const hintSuffix = command.argumentHint ? ` (${command.argumentHint})` : ''
|
|
278
282
|
// when_to_use is model-facing trigger text, appended after the description and before
|
|
279
283
|
// the argument hint; it shares the per-entry cap and never reaches the user surface.
|
|
280
284
|
const whenSuffix = command.whenToUse ? ` ${command.whenToUse}` : ''
|
|
281
285
|
const entry = `/${command.name} - ${command.description}${whenSuffix}${hintSuffix}`.slice(0, ENTRY_CHAR_CAP)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
used += entry.length + 1
|
|
287
|
-
lines.push(entry)
|
|
286
|
+
const growth = entry.length - lines[index].length
|
|
287
|
+
if (used + growth > budget) continue
|
|
288
|
+
used += growth
|
|
289
|
+
lines[index] = entry
|
|
288
290
|
}
|
|
289
|
-
if (omitted > 0) lines.push(`(${omitted} more ${omitted === 1 ? 'command was' : 'commands were'} omitted: raise SLASH_COMMAND_TOOL_CHAR_BUDGET to list them)`)
|
|
290
291
|
return ["Execute a custom slash command on the user's behalf. The command expands to instructions for you to follow in this conversation.", '', 'Available commands:', ...lines].join('\n')
|
|
291
292
|
}
|
|
292
293
|
|
|
@@ -394,9 +394,11 @@ export function substituteVars(text: string, vars: Record<string, string | undef
|
|
|
394
394
|
return text.replaceAll(/\$\{(CLAUDE_[A-Z0-9_]+)\}/g, (token, name: string) => vars[name] ?? token)
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
/**
|
|
397
|
+
/** Claude: "You invoke a command file by its file name"; subdirectories organize
|
|
398
|
+
* files without namespacing, so `a/b/c.md` is `/c`. A same-name file in another
|
|
399
|
+
* subdirectory takes the name over (scan order decides), as with Claude. */
|
|
398
400
|
export function commandNameFor(relativePath: string): string {
|
|
399
|
-
return
|
|
401
|
+
return path.basename(relativePath, '.md')
|
|
400
402
|
}
|
|
401
403
|
|
|
402
404
|
/** Every `*.md` under a commands directory, including nested ones. */
|
|
@@ -7,20 +7,38 @@
|
|
|
7
7
|
|
|
8
8
|
import * as fs from 'node:fs'
|
|
9
9
|
|
|
10
|
+
// Captured at module load: the poll must run on real time even under a test's
|
|
11
|
+
// fake timers (the stat watcher it replaced lived in libuv and was immune too);
|
|
12
|
+
// otherwise fake-timer advances spin the poll and freeze real detection.
|
|
13
|
+
const realSetInterval = globalThis.setInterval
|
|
14
|
+
const realClearInterval = globalThis.clearInterval
|
|
15
|
+
|
|
16
|
+
/** One file's content, or undefined when absent or unreadable. */
|
|
17
|
+
function snapshot(file: string): string | undefined {
|
|
18
|
+
try {
|
|
19
|
+
return fs.readFileSync(file, 'utf-8')
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
/** Watch the given settings files, calling `reload` when any of them changes.
|
|
11
|
-
* Returns a dispose function. The poll
|
|
26
|
+
* Returns a dispose function. The poll compares content, not stats: a same-size
|
|
27
|
+
* rewrite within one timestamp tick is invisible to an mtime comparison on a
|
|
28
|
+
* coarse-granularity filesystem, which made detection flaky. Settings files are
|
|
29
|
+
* small, so re-reading them on the poll is negligible. The interval is
|
|
30
|
+
* env-tunable for tests. */
|
|
12
31
|
export function watchSettingsFiles(files: string[], reload: () => void): () => void {
|
|
13
32
|
const interval = Number(process.env.PI_CODE_SETTINGS_WATCH_INTERVAL_MS) || 2000
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
33
|
+
let last = files.map(snapshot)
|
|
34
|
+
const timer = realSetInterval(() => {
|
|
35
|
+
const next = files.map(snapshot)
|
|
36
|
+
if (next.some((content, index) => content !== last[index])) {
|
|
37
|
+
last = next
|
|
38
|
+
reload()
|
|
18
39
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return () => {
|
|
24
|
-
for (const [file, listener] of listeners) fs.unwatchFile(file, listener)
|
|
25
|
-
}
|
|
40
|
+
}, interval)
|
|
41
|
+
// A watcher alone must never keep a one-shot run alive.
|
|
42
|
+
timer.unref?.()
|
|
43
|
+
return () => realClearInterval(timer)
|
|
26
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
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",
|