pi-code 1.0.4 → 1.0.6

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.
Files changed (35) hide show
  1. package/README.md +25 -13
  2. package/extensions/claude-rules.ts +158 -54
  3. package/extensions/commands.ts +417 -41
  4. package/extensions/context-imports.ts +446 -61
  5. package/extensions/hooks.ts +473 -73
  6. package/extensions/init.ts +81 -0
  7. package/extensions/internal/agent-run.ts +42 -0
  8. package/extensions/internal/bash-rules.ts +27 -0
  9. package/extensions/internal/command-file.ts +423 -66
  10. package/extensions/internal/html-markdown.ts +71 -0
  11. package/extensions/internal/instruction-events.ts +70 -0
  12. package/extensions/internal/managed-settings.ts +38 -0
  13. package/extensions/internal/mcp-call.ts +28 -0
  14. package/extensions/internal/mcp-oauth.ts +177 -0
  15. package/extensions/internal/model-complete.ts +68 -0
  16. package/extensions/internal/path-rules.ts +80 -0
  17. package/extensions/internal/plugins.ts +138 -0
  18. package/extensions/internal/project-approval.ts +2 -3
  19. package/extensions/internal/project-root.ts +78 -0
  20. package/extensions/internal/shell-split.ts +65 -0
  21. package/extensions/internal/strip-comments.ts +100 -0
  22. package/extensions/internal/web-transport.ts +3 -1
  23. package/extensions/mcp.ts +579 -30
  24. package/extensions/memory.ts +158 -35
  25. package/extensions/notify.ts +76 -4
  26. package/extensions/output-styles.ts +34 -6
  27. package/extensions/plan-mode/utils.ts +3 -57
  28. package/extensions/question.ts +2 -2
  29. package/extensions/skills.ts +11 -1
  30. package/extensions/status-line.ts +100 -5
  31. package/extensions/subagent/agents.ts +72 -61
  32. package/extensions/subagent/background.ts +25 -6
  33. package/extensions/subagent/index.ts +310 -31
  34. package/extensions/web.ts +93 -15
  35. package/package.json +1 -1
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Upward search for project configuration, shared across extensions.
3
+ *
4
+ * Claude anchors project config (.claude/*, .mcp.json, CLAUDE.local.md) at the
5
+ * project root, so a session started in a subdirectory must still find it. The
6
+ * walk runs from cwd up to the repository root and no further: without the bound,
7
+ * config planted in a world-writable ancestor such as /tmp would be offered to
8
+ * every session beneath it. With no project marker the extent is unknown, so only
9
+ * cwd is considered. The project-approval walk uses the same markers, so whatever
10
+ * these find is exactly what that walk gated.
11
+ */
12
+
13
+ import * as fs from 'node:fs'
14
+ import * as path from 'node:path'
15
+
16
+ /** Project root markers ending the walk. `.git` is a file in worktrees and submodules. */
17
+ export const ROOT_MARKERS = ['.git', 'package.json']
18
+
19
+ /** Project root at or above `from`, or undefined when no marker is found. */
20
+ export function repoRoot(from: string): string | undefined {
21
+ let currentDir = from
22
+ while (true) {
23
+ if (ROOT_MARKERS.some((marker) => fs.existsSync(path.join(currentDir, marker)))) return currentDir
24
+ const parentDir = path.dirname(currentDir)
25
+ if (parentDir === currentDir) return undefined
26
+ currentDir = parentDir
27
+ }
28
+ }
29
+
30
+ function statOf(target: string): fs.Stats | null {
31
+ try {
32
+ return fs.statSync(target)
33
+ } catch {
34
+ return null
35
+ }
36
+ }
37
+
38
+ function findNearest(cwd: string, relative: string, wantDir: boolean): string | null {
39
+ const boundary = repoRoot(cwd) ?? cwd
40
+ let currentDir = cwd
41
+ while (true) {
42
+ const candidate = path.join(currentDir, relative)
43
+ const stat = statOf(candidate)
44
+ if (stat && (wantDir ? stat.isDirectory() : stat.isFile())) return candidate
45
+
46
+ if (currentDir === boundary) return null
47
+ const parentDir = path.dirname(currentDir)
48
+ if (parentDir === currentDir) return null
49
+ currentDir = parentDir
50
+ }
51
+ }
52
+
53
+ /** Nearest `relative` directory at or above `cwd`, stopping at the repository root. */
54
+ export function findNearestDir(cwd: string, relative: string): string | null {
55
+ return findNearest(cwd, relative, true)
56
+ }
57
+
58
+ /** Nearest `relative` file at or above `cwd`, stopping at the repository root. */
59
+ export function findNearestFile(cwd: string, relative: string): string | null {
60
+ return findNearest(cwd, relative, false)
61
+ }
62
+
63
+ /** Every `relative` file between the repository root and cwd, ordered root first,
64
+ * matching Claude's root-down ordering for hierarchy-loaded context. */
65
+ export function ancestorFiles(cwd: string, relative: string): string[] {
66
+ const boundary = repoRoot(cwd) ?? cwd
67
+ const found: string[] = []
68
+ let currentDir = cwd
69
+ while (true) {
70
+ const candidate = path.join(currentDir, relative)
71
+ if (statOf(candidate)?.isFile()) found.push(candidate)
72
+ if (currentDir === boundary) break
73
+ const parentDir = path.dirname(currentDir)
74
+ if (parentDir === currentDir) break
75
+ currentDir = parentDir
76
+ }
77
+ return found.reverse()
78
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Quote-aware splitting of a shell command into its top-level segments.
3
+ *
4
+ * Shared by plan mode's bash guard and the commands extension's allowed-tools
5
+ * scope enforcement: both vet each subcommand on its own, and both refuse to
6
+ * guess when the shell could be hiding another command.
7
+ */
8
+
9
+ // The shell can hide an arbitrary command inside any of these, so callers refuse
10
+ // such a command outright rather than parse it.
11
+ const SUBSTITUTION = /\$\(|`|<\(|>\(/
12
+
13
+ export const hasSubstitution = (command: string): boolean => SUBSTITUTION.test(command)
14
+
15
+ /** Length of the separator at `i`, or 0 when there is none. */
16
+ function separatorAt(command: string, i: number): number {
17
+ const pair = command.slice(i, i + 2)
18
+ if (pair === '&&' || pair === '||' || pair === '|&') return 2
19
+ const ch = command[i]
20
+ return ch === ';' || ch === '|' || ch === '&' || ch === '\n' ? 1 : 0
21
+ }
22
+
23
+ /**
24
+ * Split on the shell separators Claude Code documents (`&&`, `||`, `;`, `|`, `|&`, `&`,
25
+ * newline) so every subcommand is checked on its own, ignoring separators inside quotes:
26
+ * `grep 'a|b'` is one read, not a pipe. Returns nothing on an unbalanced quote, which
27
+ * fails the caller closed rather than guessing at the intended split.
28
+ *
29
+ * A shell AST would be exact; this is the honest approximation for a quoting-only concern.
30
+ */
31
+ export function splitSegments(command: string): string[] {
32
+ const segments: string[] = []
33
+ let current = ''
34
+ let quote: "'" | '"' | undefined
35
+
36
+ for (let i = 0; i < command.length; i++) {
37
+ const ch = command[i]
38
+ if (quote !== undefined) {
39
+ current += ch
40
+ if (ch === quote) quote = undefined
41
+ continue
42
+ }
43
+ if (ch === "'" || ch === '"') {
44
+ quote = ch
45
+ current += ch
46
+ continue
47
+ }
48
+ if (ch === '\\' && i + 1 < command.length) {
49
+ current += ch + command[++i]
50
+ continue
51
+ }
52
+ const separator = separatorAt(command, i)
53
+ if (separator > 0) {
54
+ segments.push(current)
55
+ current = ''
56
+ i += separator - 1
57
+ continue
58
+ }
59
+ current += ch
60
+ }
61
+
62
+ if (quote !== undefined) return []
63
+ segments.push(current)
64
+ return segments.map((segment) => segment.trim()).filter(Boolean)
65
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Block-level HTML comment stripping for context and rule files.
3
+ *
4
+ * Claude Code strips block-level HTML comments (`<!-- maintainer notes -->`)
5
+ * from CLAUDE.md files before injection, while preserving comments inside code
6
+ * blocks. "Block-level" is read as whole-line-anchored: a comment counts only
7
+ * when it starts a line (after optional indentation) and nothing but comments
8
+ * and whitespace occupy the line(s) it spans; those lines are removed entirely,
9
+ * a multi-line comment with every line it covers. A comment sharing a line with
10
+ * real content is inline prose and stays verbatim, as does anything inside a
11
+ * fenced code block (backtick or tilde).
12
+ */
13
+
14
+ /** The fence a line opens or closes, if any; mirrors context-imports. */
15
+ export function fenceMarker(lineStart: string): string | null {
16
+ if (lineStart.startsWith('```')) return '`'
17
+ if (lineStart.startsWith('~~~')) return '~'
18
+ return null
19
+ }
20
+
21
+ /** Length of the fence run starting `lineStart`, a run of `marker` characters. */
22
+ function fenceLength(lineStart: string, marker: string): number {
23
+ let length = 0
24
+ while (length < lineStart.length && lineStart[length] === marker) length++
25
+ return length
26
+ }
27
+
28
+ // CommonMark closes a fenced block only with a fence of the same character that
29
+ // is at least as long as the opener, so both are tracked: a shorter same-char
30
+ // fence line (the classic 3-backtick block quoted inside a 4-backtick one) is
31
+ // content, not a closer.
32
+ interface Fence {
33
+ marker: string
34
+ length: number
35
+ }
36
+
37
+ /** The fence state after a line, plus whether the line is fenced code (opener,
38
+ * body, or closer) and so emitted verbatim rather than scanned for comments. */
39
+ function stepFence(fence: Fence | null, trimmed: string, marker: string | null): { fence: Fence | null; fenced: boolean } {
40
+ if (marker !== null && fence === null) {
41
+ return { fence: { marker, length: fenceLength(trimmed, marker) }, fenced: true }
42
+ }
43
+ if (fence !== null) {
44
+ const closes = marker === fence.marker && fenceLength(trimmed, marker) >= fence.length
45
+ return { fence: closes ? null : fence, fenced: true } // closer included: comments are content, not maintainer notes
46
+ }
47
+ return { fence, fenced: false }
48
+ }
49
+
50
+ /** Advance a line while inside an open multi-line comment. Emits any content that
51
+ * trails the closer onto its own line; returns whether the comment stays open. */
52
+ function continueOpenComment(out: string[], line: string): boolean {
53
+ const close = line.indexOf('-->')
54
+ if (close === -1) return true // still inside the comment: the line goes with it
55
+ const rest = line.slice(close + 3)
56
+ // Content trailing the closer keeps its line; a bare closer line is dropped.
57
+ if (rest.trim().length > 0) out.push(rest.trimStart())
58
+ return false
59
+ }
60
+
61
+ /** Fold line-starting HTML comments off `trimmed`; several may share one line.
62
+ * `allComment` means the line held nothing but comments and whitespace, so it is
63
+ * dropped; `opensBlock` means the last comment opened a multi-line block. */
64
+ function consumeLineComments(trimmed: string): { allComment: boolean; opensBlock: boolean } {
65
+ let rest = trimmed
66
+ let sawComment = false
67
+ while (rest.startsWith('<!--')) {
68
+ sawComment = true
69
+ const close = rest.indexOf('-->')
70
+ if (close === -1) return { allComment: true, opensBlock: true } // opens a multi-line comment
71
+ rest = rest.slice(close + 3).trimStart()
72
+ }
73
+ return { allComment: sawComment && rest.length === 0, opensBlock: false }
74
+ }
75
+
76
+ /** Remove whole-line HTML comments, keeping fenced code and inline comments. */
77
+ export function stripBlockComments(text: string): string {
78
+ const out: string[] = []
79
+ let fence: Fence | null = null
80
+ let inComment = false
81
+ for (const line of text.split('\n')) {
82
+ if (inComment) {
83
+ inComment = continueOpenComment(out, line)
84
+ continue
85
+ }
86
+ const trimmed = line.trimStart()
87
+ const step = stepFence(fence, trimmed, fenceMarker(trimmed))
88
+ fence = step.fence
89
+ if (step.fenced) {
90
+ out.push(line)
91
+ continue
92
+ }
93
+ const { allComment, opensBlock } = consumeLineComments(trimmed)
94
+ if (opensBlock) inComment = true
95
+ if (allComment) continue // the whole line was comment
96
+ // No comment, or a line-starting comment followed by content: inline, verbatim.
97
+ out.push(line)
98
+ }
99
+ return out.join('\n')
100
+ }
@@ -32,7 +32,9 @@ export function httpFetch(url: URL, opts: TransportOptions): Promise<Response> {
32
32
  url,
33
33
  {
34
34
  method: 'GET',
35
- headers: { 'User-Agent': opts.userAgent },
35
+ // Prefer markdown, as Claude's WebFetch does, so a content-negotiating
36
+ // server can return markdown directly and skip the lossy HTML conversion.
37
+ headers: { 'User-Agent': opts.userAgent, Accept: 'text/markdown, text/html;q=0.9, */*;q=0.8' },
36
38
  signal: opts.signal,
37
39
  lookup: opts.lookup,
38
40
  // servername is left to default to url.hostname, so SNI and certificate