pi-code 1.0.44 → 1.0.46
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.
|
@@ -158,7 +158,10 @@ export default function gitCheckpointExtension(pi: ExtensionAPI) {
|
|
|
158
158
|
|
|
159
159
|
function gitShadow(args: string[]): ReturnType<ExtensionAPI['exec']> {
|
|
160
160
|
if (!shadowDir || !workTree) return Promise.resolve({ stdout: '', stderr: 'shadow repo not initialized', code: 1, killed: false })
|
|
161
|
-
|
|
161
|
+
// A snapshot layer must be byte-faithful: with the host's autocrlf (the
|
|
162
|
+
// Windows default) the shadow checkout would rewrite every restored file's
|
|
163
|
+
// line endings, so conversion is pinned off for every shadow operation.
|
|
164
|
+
return pi.exec('git', ['-c', 'core.autocrlf=false', '--git-dir', shadowDir, '--work-tree', workTree, ...args], { cwd: workTree })
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
async function ensureShadow(ctx: ExtensionContext): Promise<void> {
|
|
@@ -218,6 +218,11 @@ function mergeHooksJson(config: HooksConfig, raw: string, source: string, source
|
|
|
218
218
|
/** Each enabled plugin's hooks (hooks/hooks.json, or wherever the manifest points),
|
|
219
219
|
* with ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PLUGIN_DATA} substituted before parsing so a
|
|
220
220
|
* hook can name its bundled scripts by real path. */
|
|
221
|
+
/** The substitution here lands inside raw JSON, so values must arrive escaped:
|
|
222
|
+
* an unescaped Windows root injected \U-style sequences, the parse threw, and
|
|
223
|
+
* every hook the plugin declared silently vanished. */
|
|
224
|
+
const jsonEscape = (value: string): string => JSON.stringify(value).slice(1, -1)
|
|
225
|
+
|
|
221
226
|
export function loadPluginHooks(config: HooksConfig, plugins: InstalledPlugin[], sources?: Map<HookMatcher, string>): void {
|
|
222
227
|
for (const plugin of plugins) {
|
|
223
228
|
const declared = plugin.manifest.hooks
|
|
@@ -225,12 +230,12 @@ export function loadPluginHooks(config: HooksConfig, plugins: InstalledPlugin[],
|
|
|
225
230
|
// numeric event keys), so it falls through to the default path rather than
|
|
226
231
|
// silently registering nothing.
|
|
227
232
|
if (declared !== null && typeof declared === 'object' && !Array.isArray(declared)) {
|
|
228
|
-
mergeHooksJson(config, substitutePluginVars(JSON.stringify({ hooks: declared }), plugin), `${plugin.name} (plugin.json)`, sources, `plugin:${plugin.name}`)
|
|
233
|
+
mergeHooksJson(config, substitutePluginVars(JSON.stringify({ hooks: declared }), plugin, jsonEscape), `${plugin.name} (plugin.json)`, sources, `plugin:${plugin.name}`)
|
|
229
234
|
continue
|
|
230
235
|
}
|
|
231
236
|
const file = path.resolve(plugin.root, typeof declared === 'string' ? declared : path.join('hooks', 'hooks.json'))
|
|
232
237
|
try {
|
|
233
|
-
mergeHooksJson(config, substitutePluginVars(fs.readFileSync(file, 'utf-8'), plugin), file, sources, `plugin:${plugin.name}`)
|
|
238
|
+
mergeHooksJson(config, substitutePluginVars(fs.readFileSync(file, 'utf-8'), plugin, jsonEscape), file, sources, `plugin:${plugin.name}`)
|
|
234
239
|
} catch {
|
|
235
240
|
// a plugin without hooks contributes nothing
|
|
236
241
|
}
|
|
@@ -169,6 +169,11 @@ export function globToRegExpSource(pattern: string): string {
|
|
|
169
169
|
|
|
170
170
|
/** A rule resolved to an absolute glob per its anchor form. */
|
|
171
171
|
function resolveRule(rule: string, anchors: PathAnchors): string {
|
|
172
|
+
// A ${CLAUDE_*}-substituted rule arrives already absolute in the platform's
|
|
173
|
+
// own spelling; on Windows that starts with a drive letter (bare, or behind
|
|
174
|
+
// the / that substitutePathRule prefixes to mark absoluteness), which the
|
|
175
|
+
// POSIX anchor forms below would misread and bury under an anchor.
|
|
176
|
+
if (/^\/?[A-Za-z]:[\\/]/.test(rule)) return rule.replace(/^\//, '')
|
|
172
177
|
if (rule.startsWith('//')) return rule.slice(1)
|
|
173
178
|
if (rule.startsWith('~/')) return path.join(anchors.home, rule.slice(2))
|
|
174
179
|
if (rule.startsWith('/')) return path.join(anchors.projectRoot, rule.slice(1))
|
|
@@ -230,14 +235,24 @@ export function matchesCompiledGlobs(relPath: string, globs: CompiledGlob[]): bo
|
|
|
230
235
|
|
|
231
236
|
/** Whether the accessed file matches at least one rule. No rules means no match:
|
|
232
237
|
* a granted-but-scoped tool with an empty scope set stays blocked, never open. */
|
|
238
|
+
/** Both comparison sides in posix form. On Windows, resolve stamps the drive on
|
|
239
|
+
* the target while join-built rules stay drive-less, and backslash separators
|
|
240
|
+
* collide with glob syntax, so unnormalized rules could never match. */
|
|
241
|
+
const toPosix = (target: string): string => {
|
|
242
|
+
const withSlashes = target.split(path.sep).join('/')
|
|
243
|
+
// The drive letter goes (case-insensitively) so rule and target agree even
|
|
244
|
+
// when only one side carries C:.
|
|
245
|
+
return withSlashes.replace(/^\/?[A-Za-z]:\//, '/')
|
|
246
|
+
}
|
|
247
|
+
|
|
233
248
|
export function matchesPathRules(filePath: string, rules: string[], anchors: PathAnchors): boolean {
|
|
234
|
-
const target = path.resolve(anchors.cwd, filePath)
|
|
249
|
+
const target = toPosix(path.resolve(anchors.cwd, filePath))
|
|
235
250
|
return rules.some((rule) => {
|
|
236
251
|
const trimmed = rule.trim()
|
|
237
252
|
// An empty specifier (`Read()`) matches nothing, so the tool stays blocked
|
|
238
253
|
// rather than falling open, mirroring `Bash()`.
|
|
239
254
|
if (trimmed === '') return false
|
|
240
|
-
const resolved = resolveRule(trimmed, anchors)
|
|
255
|
+
const resolved = toPosix(resolveRule(trimmed, anchors))
|
|
241
256
|
return new RegExp(`^${globToRegExpSource(resolved)}$`).test(target)
|
|
242
257
|
})
|
|
243
258
|
}
|
|
@@ -239,10 +239,13 @@ function resolvePlugin(home: string, cacheDir: string, marketplace: string, plug
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
/** The two plugin path variables, textually substituted into plugin-shipped
|
|
242
|
-
* config (hook commands, MCP server definitions, command bodies).
|
|
243
|
-
|
|
242
|
+
* config (hook commands, MCP server definitions, command bodies). A caller
|
|
243
|
+
* substituting into text that is still raw JSON must pass an escapeValue that
|
|
244
|
+
* JSON-escapes: a Windows root (C:\Users\...) inserted verbatim injects invalid
|
|
245
|
+
* escape sequences and the subsequent parse throws. */
|
|
246
|
+
export function substitutePluginVars(value: string, plugin: InstalledPlugin, escapeValue: (substituted: string) => string = (substituted) => substituted): string {
|
|
244
247
|
return value
|
|
245
|
-
.replaceAll('${CLAUDE_PLUGIN_ROOT}', plugin.root)
|
|
246
|
-
.replaceAll('${CLAUDE_PLUGIN_DATA}', plugin.dataDir)
|
|
247
|
-
.replace(/\$\{user_config\.(\w+)\}/g, (_, key: string) => plugin.userConfig?.[key] ?? '')
|
|
248
|
+
.replaceAll('${CLAUDE_PLUGIN_ROOT}', escapeValue(plugin.root))
|
|
249
|
+
.replaceAll('${CLAUDE_PLUGIN_DATA}', escapeValue(plugin.dataDir))
|
|
250
|
+
.replace(/\$\{user_config\.(\w+)\}/g, (_, key: string) => escapeValue(plugin.userConfig?.[key] ?? ''))
|
|
248
251
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
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",
|