pi-code 1.0.23 → 1.0.25
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.
|
@@ -21,8 +21,10 @@ import * as os from 'node:os'
|
|
|
21
21
|
import * as path from 'node:path'
|
|
22
22
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
|
23
23
|
|
|
24
|
+
import { claudeMdExcludeFiles, isExcludedPath, readClaudeMdExcludes } from './context-imports.js'
|
|
24
25
|
import { claudeConfigDir } from './internal/config-dir.js'
|
|
25
26
|
import { publishInstructionLoad } from './internal/instruction-events.js'
|
|
27
|
+
import { readManagedSettings } from './internal/managed-settings.js'
|
|
26
28
|
import { type CompiledGlob, compileGlobs, matchesCompiledGlobs } from './internal/path-rules.js'
|
|
27
29
|
import { isProjectApproved } from './internal/project-approval.js'
|
|
28
30
|
import { findNearestDir } from './internal/project-root.js'
|
|
@@ -147,12 +149,36 @@ interface RuleSet {
|
|
|
147
149
|
|
|
148
150
|
const EMPTY_RULES: RuleSet = { inline: [], scoped: [] }
|
|
149
151
|
|
|
152
|
+
/** The canonical form of a path. A target that does not exist yet (a write
|
|
153
|
+
* creating a new file) canonicalises its nearest existing ancestor and keeps the
|
|
154
|
+
* remaining segments, so both sides of the attach match compare realpaths even
|
|
155
|
+
* for brand-new files in a symlinked checkout. */
|
|
156
|
+
function realpathOr(target: string): string {
|
|
157
|
+
try {
|
|
158
|
+
return fs.realpathSync(target)
|
|
159
|
+
} catch {
|
|
160
|
+
const dir = path.dirname(target)
|
|
161
|
+
if (dir === target) return target
|
|
162
|
+
return path.join(realpathOr(dir), path.basename(target))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
150
166
|
/** Unscoped rules are inlined; path-scoped ones keep their scope as pointers,
|
|
151
|
-
* mirroring Claude Code, where scoped rules attach only to matching files.
|
|
152
|
-
|
|
167
|
+
* mirroring Claude Code, where scoped rules attach only to matching files. Files
|
|
168
|
+
* matching `claudeMdExcludes` are skipped entirely, as the docs' monorepo recipe
|
|
169
|
+
* (excluding another team's `.claude/rules/**`) relies on; the check runs on the
|
|
170
|
+
* realpath so a symlink cannot dodge an exclusion. */
|
|
171
|
+
function readRules(rulesDir: string, isExcluded?: (realPath: string) => boolean): RuleSet {
|
|
153
172
|
const inline: string[] = []
|
|
154
173
|
const scoped: ScopedRule[] = []
|
|
155
174
|
for (const file of findMarkdownFiles(rulesDir)) {
|
|
175
|
+
if (isExcluded) {
|
|
176
|
+
// Both spellings count: a glob written against the lexical path and one
|
|
177
|
+
// written against the resolved real path each exclude, which can only
|
|
178
|
+
// widen an exclusion, never dodge one.
|
|
179
|
+
const lexical = path.join(rulesDir, file)
|
|
180
|
+
if (isExcluded(lexical) || isExcluded(realpathOr(lexical))) continue
|
|
181
|
+
}
|
|
156
182
|
let parsed: Frontmatter
|
|
157
183
|
try {
|
|
158
184
|
parsed = parseFrontmatter(fs.readFileSync(path.join(rulesDir, file), 'utf-8'))
|
|
@@ -223,15 +249,20 @@ export default function claudeRulesExtension(pi: ExtensionAPI) {
|
|
|
223
249
|
let attachTargets: AttachTarget[] = []
|
|
224
250
|
|
|
225
251
|
pi.on('session_start', async (_event, ctx) => {
|
|
226
|
-
globalRules = readRules(globalRulesDir)
|
|
227
252
|
// Project rules are repository text landing in the system prompt, so they load
|
|
228
253
|
// only once the project is approved. isProjectTrusted alone is true for a repo
|
|
229
254
|
// pi never asked about; see project-approval.
|
|
230
255
|
const approved = await isProjectApproved(ctx)
|
|
256
|
+
// Claude's claudeMdExcludes covers rules files too (the docs' monorepo recipe
|
|
257
|
+
// excludes another team's .claude/rules/**), so the same merged glob list the
|
|
258
|
+
// context loader honors gates rule files here.
|
|
259
|
+
const excludeGlobs = readClaudeMdExcludes(claudeMdExcludeFiles(ctx.cwd, os.homedir(), approved), readManagedSettings())
|
|
260
|
+
const isExcluded = (realPath: string): boolean => isExcludedPath(realPath, excludeGlobs, os.homedir())
|
|
261
|
+
globalRules = readRules(globalRulesDir, isExcluded)
|
|
231
262
|
// Nearest at-or-above cwd, so a subdirectory session still reads the rules the
|
|
232
263
|
// approval walk gated on.
|
|
233
264
|
const projectRulesDir = approved ? findNearestDir(ctx.cwd, path.join('.claude', 'rules')) : null
|
|
234
|
-
projectRules = projectRulesDir ? readRules(projectRulesDir) : EMPTY_RULES
|
|
265
|
+
projectRules = projectRulesDir ? readRules(projectRulesDir, isExcluded) : EMPTY_RULES
|
|
235
266
|
|
|
236
267
|
// Global globs are relative to cwd; project globs to the project root (the dir
|
|
237
268
|
// holding .claude), so `db/**` in a repo rule matches repo-relative paths even
|
|
@@ -239,8 +270,8 @@ export default function claudeRulesExtension(pi: ExtensionAPI) {
|
|
|
239
270
|
// than on every tool result; rebuilt per session so a re-run re-attaches.
|
|
240
271
|
const projectRoot = projectRulesDir ? path.dirname(path.dirname(projectRulesDir)) : ctx.cwd
|
|
241
272
|
attachTargets = [
|
|
242
|
-
...globalRules.scoped.map((rule) => ({ globs: rule.paths, compiled: compileGlobs(rule.paths), body: rule.body, root: ctx.cwd, file: path.join(globalRulesDir, rule.rel), memoryType: 'User' as const })),
|
|
243
|
-
...projectRules.scoped.map((rule) => ({ globs: rule.paths, compiled: compileGlobs(rule.paths), body: rule.body, root: projectRoot, file: path.join(projectRulesDir ?? path.join(ctx.cwd, '.claude', 'rules'), rule.rel), memoryType: 'Project' as const })),
|
|
273
|
+
...globalRules.scoped.map((rule) => ({ globs: rule.paths, compiled: compileGlobs(rule.paths), body: rule.body, root: realpathOr(ctx.cwd), file: path.join(globalRulesDir, rule.rel), memoryType: 'User' as const })),
|
|
274
|
+
...projectRules.scoped.map((rule) => ({ globs: rule.paths, compiled: compileGlobs(rule.paths), body: rule.body, root: realpathOr(projectRoot), file: path.join(projectRulesDir ?? path.join(ctx.cwd, '.claude', 'rules'), rule.rel), memoryType: 'Project' as const })),
|
|
244
275
|
]
|
|
245
276
|
pendingScopedRules = attachTargets.length
|
|
246
277
|
// Relative to cwd, which the read tool resolves: an ancestor dir yields a
|
|
@@ -274,7 +305,9 @@ export default function claudeRulesExtension(pi: ExtensionAPI) {
|
|
|
274
305
|
if (event.toolName !== 'read' && event.toolName !== 'edit' && event.toolName !== 'write') return
|
|
275
306
|
const rel = (event.input as { path?: unknown } | undefined)?.path
|
|
276
307
|
if (typeof rel !== 'string' || rel.length === 0) return
|
|
277
|
-
|
|
308
|
+
// Realpath both sides (roots canonicalise at session_start): a tool reporting
|
|
309
|
+
// the resolved real path in a symlinked checkout must still match.
|
|
310
|
+
const abs = realpathOr(path.resolve(ctx.cwd, rel))
|
|
278
311
|
|
|
279
312
|
const bodies: string[] = []
|
|
280
313
|
const remaining: AttachTarget[] = []
|
|
@@ -95,7 +95,7 @@ import { claudeToolInput, claudeToolName, claudeToolResponse, piToolOutput } fro
|
|
|
95
95
|
import { formatHooksSummary, type HookCommand, type HookMatcher, type HooksConfig, hookFiles, isBackgroundHook, loadHooks, loadPluginHooks, readAllowedHttpHookUrls, readDisableAllHooks } from './config.js'
|
|
96
96
|
import { blockedToolCall, jsonBlockVerdict, postToolFeedback, promptContext, runPreToolUse, runUserPromptSubmit, surfaceSystemMessages, tryParseJson } from './decisions.js'
|
|
97
97
|
import { allCommands, matchingCommands, passesIfFilter } from './matcher.js'
|
|
98
|
-
import { type HookRunner, type HookRunResult, runAgentHook, runHookCommand, runHttpHook, runMcpToolHook, runPromptHook, timeoutMs } from './runners.js'
|
|
98
|
+
import { type HookRunner, type HookRunResult, runAgentHook, runHookCommand, runHttpHook, runMcpToolHook, runPromptHook, sessionEndTimeoutMs, timeoutMs } from './runners.js'
|
|
99
99
|
|
|
100
100
|
export * from './config.js'
|
|
101
101
|
export * from './decisions.js'
|
|
@@ -185,6 +185,16 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
185
185
|
if (ctx.thinkingLevel) common.effort = { level: ctx.thinkingLevel }
|
|
186
186
|
return common
|
|
187
187
|
}
|
|
188
|
+
/** Claude's prompt-hook `model` override, resolved against the models this user
|
|
189
|
+
* can run (exact id first, then a substring match); the session model otherwise. */
|
|
190
|
+
const resolveHookModel = (ctx: ExtensionContext, override: string | undefined): ExtensionContext['model'] => {
|
|
191
|
+
if (!override) return ctx.model
|
|
192
|
+
const available = (ctx as { modelRegistry?: { getAvailable?: () => ReadonlyArray<{ id: string; name?: string }> } }).modelRegistry?.getAvailable?.() ?? []
|
|
193
|
+
const needle = override.toLowerCase()
|
|
194
|
+
const match = available.find((model) => model.id.toLowerCase() === needle) ?? available.find((model) => model.id.toLowerCase().includes(needle) || model.name?.toLowerCase().includes(needle))
|
|
195
|
+
return (match as ExtensionContext['model']) ?? ctx.model
|
|
196
|
+
}
|
|
197
|
+
|
|
188
198
|
/** Kills for background hooks still running; Claude kills async hooks at teardown,
|
|
189
199
|
* so session_shutdown reaps anything left rather than let a hung hook pin the
|
|
190
200
|
* event loop past a one-shot run's end. */
|
|
@@ -220,7 +230,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
220
230
|
const merged = { ...commonPayload(ctx), ...extra, ...(payload as Record<string, unknown>) }
|
|
221
231
|
const dispatch = (onChild?: (kill: () => void) => void): Promise<HookRunResult> => {
|
|
222
232
|
if (hook.type === 'http') return runHttpHook(hook, merged, ms, allowedHttpHookUrls)
|
|
223
|
-
if (hook.type === 'prompt') return runPromptHook(hook, merged, ctx.model, ms)
|
|
233
|
+
if (hook.type === 'prompt') return runPromptHook(hook, merged, resolveHookModel(ctx, hook.model), ms)
|
|
224
234
|
if (hook.type === 'agent') return runAgentHook(hook, merged, ms, (ctx.model as { id?: string } | undefined)?.id)
|
|
225
235
|
if (hook.type === 'mcp_tool') return runMcpToolHook(hook, merged, ms)
|
|
226
236
|
return runHookCommand(hook.command, merged, ms, projectDir, hook.args, onChild)
|
|
@@ -483,6 +493,14 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
483
493
|
stopHookActive = false
|
|
484
494
|
return
|
|
485
495
|
}
|
|
496
|
+
// Claude: Stop does not run when the stoppage was a user interrupt; pi marks
|
|
497
|
+
// the aborted turn's final assistant message stopReason "aborted".
|
|
498
|
+
const turnMessages = (event as { messages?: Array<{ role: string; stopReason?: string }> }).messages ?? []
|
|
499
|
+
const lastAssistant = [...turnMessages].reverse().find((message) => message.role === 'assistant')
|
|
500
|
+
if (lastAssistant?.stopReason === 'aborted') {
|
|
501
|
+
stopHookActive = false
|
|
502
|
+
return
|
|
503
|
+
}
|
|
486
504
|
// Claude's Stop payload carries the turn's final assistant text so a hook need
|
|
487
505
|
// not re-read the transcript; included only when there is one.
|
|
488
506
|
const lastText = lastAssistantText((event as { messages?: Array<{ role: string; content: unknown }> }).messages ?? [])
|
|
@@ -543,7 +561,11 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
543
561
|
|
|
544
562
|
pi.on('session_shutdown', async (event, ctx) => {
|
|
545
563
|
const reason = claudeSpelling(SESSION_END_REASON, event.reason)
|
|
546
|
-
|
|
564
|
+
// SessionEnd rides Claude's short shared budget (see sessionEndTimeoutMs) so a
|
|
565
|
+
// slow hook cannot stall session exit, /new or /resume.
|
|
566
|
+
const sessionEndCommands = matchingCommands(config.SessionEnd, reason.names).filter((command) => passesIfFilter(command, undefined))
|
|
567
|
+
const runner = boundRunner(ctx)
|
|
568
|
+
const results = await Promise.all(sessionEndCommands.map((command) => runner(command, { hook_event_name: 'SessionEnd', reason: reason.value }, sessionEndTimeoutMs(command))))
|
|
547
569
|
surfaceSystemMessages(results, (message) => ctx.ui.notify(message, 'warning'))
|
|
548
570
|
// Claude kills async hooks still running at teardown; the session that spawned
|
|
549
571
|
// these is over, and their delivery would target a disposed context anyway.
|
|
@@ -20,6 +20,12 @@ import { type HookCommand, httpUrlAllowed, isBackgroundHook } from './config.js'
|
|
|
20
20
|
// raise their own per-hook `timeout`.
|
|
21
21
|
const DEFAULT_TIMEOUT_S = 60
|
|
22
22
|
|
|
23
|
+
/** Claude's per-type defaults where they are safe to mirror: 30s for `prompt`
|
|
24
|
+
* hooks and 60s for `agent` hooks. Command/http/mcp_tool keep the flat 60s
|
|
25
|
+
* documented divergence from Claude's 600 (a gated hook fails closed here, so ten
|
|
26
|
+
* minutes of default budget would wedge the turn). */
|
|
27
|
+
const TYPE_DEFAULT_TIMEOUT_S: Record<string, number> = { prompt: 30, agent: 60 }
|
|
28
|
+
|
|
23
29
|
export interface HookRunResult {
|
|
24
30
|
code: number
|
|
25
31
|
stdout: string
|
|
@@ -49,10 +55,20 @@ export function timeoutMs(command: HookCommand): number {
|
|
|
49
55
|
// Non-positive values fall back to the default: a 0ms timer would fire before the
|
|
50
56
|
// hook runs, and a timed-out PreToolUse hook fails closed, bricking the tool.
|
|
51
57
|
const declared = command.timeout
|
|
52
|
-
const
|
|
58
|
+
const fallback = TYPE_DEFAULT_TIMEOUT_S[command.type ?? 'command'] ?? DEFAULT_TIMEOUT_S
|
|
59
|
+
const seconds = typeof declared === 'number' && declared > 0 ? Math.min(declared, MAX_TIMEOUT_S) : fallback
|
|
53
60
|
return seconds * 1000
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
/** Claude's SessionEnd budget: hooks share 1.5 seconds so session exit (and /new,
|
|
64
|
+
* /resume) cannot stall on a slow hook; a declared per-hook `timeout` raises the
|
|
65
|
+
* budget to match, up to 60 seconds. */
|
|
66
|
+
export function sessionEndTimeoutMs(command: HookCommand): number {
|
|
67
|
+
const declared = command.timeout
|
|
68
|
+
if (typeof declared === 'number' && declared > 0) return Math.min(declared, 60) * 1000
|
|
69
|
+
return 1500
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
/** Memory backstop for a runaway hook. A decision payload is orders of magnitude smaller. */
|
|
57
73
|
const MAX_HOOK_OUTPUT = 1_000_000
|
|
58
74
|
|
|
@@ -217,7 +233,11 @@ export async function runPromptHook(hook: HookCommand, payload: unknown, model:
|
|
|
217
233
|
if (!model) return { code: 1, stdout: '', stderr: 'no model available for prompt hook', timedOut: false }
|
|
218
234
|
// A replacer function, so `$$`/`$&`/`` $` ``/`$'` inside the payload JSON are inserted
|
|
219
235
|
// verbatim rather than read as replacement patterns (a Bash `echo $$` is a common trigger).
|
|
220
|
-
|
|
236
|
+
// Claude: when $ARGUMENTS is not present, the input JSON is appended to the
|
|
237
|
+
// prompt, so the model never evaluates blind.
|
|
238
|
+
const template = hook.prompt ?? ''
|
|
239
|
+
const withInput = template.includes('$ARGUMENTS') ? template : `${template}\n\n$ARGUMENTS`
|
|
240
|
+
const prompt = substituteArguments(withInput, payload)
|
|
221
241
|
const signal = AbortSignal.timeout(timeoutMs)
|
|
222
242
|
try {
|
|
223
243
|
const { text: answer } = await completeText(model, prompt, { system: PROMPT_HOOK_SYSTEM, maxTokens: 512, signal })
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* root (the settings source), and bare or `./` from the current directory. As
|
|
8
8
|
* allow rules, a single-segment directory pattern anchors at cwd; a bare
|
|
9
9
|
* filename matches at any depth. `*` stays within one segment, `**` crosses
|
|
10
|
-
* directories. Matching is lexical, on resolved paths
|
|
11
|
-
*
|
|
10
|
+
* directories. Matching is lexical, on resolved paths. Bracket expressions parse
|
|
11
|
+
* per Claude's documented glob contract: `[abc]` classes with ranges and `!`
|
|
12
|
+
* negation, an unreadable `[` making the pattern match nothing, and `\[` for a
|
|
13
|
+
* literal bracket.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import * as path from 'node:path'
|
|
@@ -79,26 +81,63 @@ function expandBraces(pattern: string): string[] | null {
|
|
|
79
81
|
return expanded
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
+
/** The end index of a bracket expression starting at `start` (`[`), or -1 when it
|
|
85
|
+
* cannot be read as one, which per Claude makes the whole pattern invalid. A `]`
|
|
86
|
+
* directly after the opening (or after a leading negation) is a literal member. */
|
|
87
|
+
function bracketEnd(pattern: string, start: number): number {
|
|
88
|
+
let i = start + 1
|
|
89
|
+
if (pattern[i] === '!' || pattern[i] === '^') i += 1
|
|
90
|
+
if (pattern[i] === ']') i += 1
|
|
91
|
+
for (; i < pattern.length; i += 1) {
|
|
92
|
+
if (pattern[i] === ']') return i
|
|
93
|
+
}
|
|
94
|
+
return -1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A bracket expression body as a regex character class, escaping regex-relevant
|
|
98
|
+
* characters while keeping `-` ranges; a leading `!` (or `^`) negates. */
|
|
99
|
+
function bracketClass(body: string): string {
|
|
100
|
+
const negated = body.startsWith('!') || body.startsWith('^')
|
|
101
|
+
const members = (negated ? body.slice(1) : body).replace(/[\\\]^]/g, (ch) => `\\${ch}`)
|
|
102
|
+
return `[${negated ? '^' : ''}${members}]`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A `*` run starting at `i`: a double star followed by a slash spans whole
|
|
106
|
+
* directories, a bare double star crosses segments, and a single `*` stays within
|
|
107
|
+
* one. Returns the regex source and the index after the run. */
|
|
108
|
+
function translateStar(pattern: string, i: number): { source: string; next: number } {
|
|
109
|
+
if (pattern[i + 1] === '*') {
|
|
110
|
+
const prevSlash = i === 0 || pattern[i - 1] === '/'
|
|
111
|
+
if (prevSlash && pattern[i + 2] === '/') return { source: '(?:[^/]+/)*', next: i + 3 }
|
|
112
|
+
return { source: '.*', next: i + 2 }
|
|
113
|
+
}
|
|
114
|
+
return { source: '[^/]*', next: i + 1 }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function translateGlob(pattern: string): string | null {
|
|
84
118
|
let out = ''
|
|
85
119
|
let i = 0
|
|
86
120
|
while (i < pattern.length) {
|
|
87
121
|
const ch = pattern[i]
|
|
122
|
+
// Claude: to match a literal bracket, escape it; the escape consumes both chars.
|
|
123
|
+
if (ch === '\\' && (pattern[i + 1] === '[' || pattern[i + 1] === ']')) {
|
|
124
|
+
out += escapeRegExp(pattern[i + 1])
|
|
125
|
+
i += 2
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
// Claude: `[` starts a bracket expression such as `[abc]`; a `[` that cannot be
|
|
129
|
+
// read as one makes the pattern invalid, matching nothing.
|
|
130
|
+
if (ch === '[') {
|
|
131
|
+
const end = bracketEnd(pattern, i)
|
|
132
|
+
if (end === -1) return null
|
|
133
|
+
out += bracketClass(pattern.slice(i + 1, end))
|
|
134
|
+
i = end + 1
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
88
137
|
if (ch === '*') {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
out += '(?:[^/]+/)*' // `**/` spans zero or more whole directories
|
|
93
|
-
i += 3
|
|
94
|
-
continue
|
|
95
|
-
}
|
|
96
|
-
out += '.*'
|
|
97
|
-
i += 2
|
|
98
|
-
continue
|
|
99
|
-
}
|
|
100
|
-
out += '[^/]*'
|
|
101
|
-
i += 1
|
|
138
|
+
const star = translateStar(pattern, i)
|
|
139
|
+
out += star.source
|
|
140
|
+
i = star.next
|
|
102
141
|
continue
|
|
103
142
|
}
|
|
104
143
|
if (ch === '?') {
|
|
@@ -112,13 +151,20 @@ function translateGlob(pattern: string): string {
|
|
|
112
151
|
return out
|
|
113
152
|
}
|
|
114
153
|
|
|
154
|
+
/** A regex source that matches nothing: the compiled form of an invalid pattern. */
|
|
155
|
+
const NEVER_MATCH = '(?!)'
|
|
156
|
+
|
|
115
157
|
/** One gitignore-style pattern as an anchored regular expression source. Brace
|
|
116
158
|
* groups (`{ts,tsx}`, nested, Cartesian across groups) expand into ORed
|
|
117
|
-
* alternatives; an over-budget expansion falls back to the literal pattern.
|
|
159
|
+
* alternatives; an over-budget expansion falls back to the literal pattern. An
|
|
160
|
+
* invalid pattern (an unreadable bracket expression) matches nothing, as Claude
|
|
161
|
+
* documents, rather than matching its literal spelling. */
|
|
118
162
|
export function globToRegExpSource(pattern: string): string {
|
|
119
163
|
const alternatives = expandBraces(pattern) ?? [pattern]
|
|
120
|
-
|
|
121
|
-
|
|
164
|
+
const sources = alternatives.map(translateGlob)
|
|
165
|
+
if (sources.includes(null)) return NEVER_MATCH
|
|
166
|
+
if (sources.length === 1) return sources[0] as string
|
|
167
|
+
return `(?:${sources.join('|')})`
|
|
122
168
|
}
|
|
123
169
|
|
|
124
170
|
/** A rule resolved to an absolute glob per its anchor form. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
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",
|