@saifymatteo/pi-extensions 0.0.0-stage → 0.1.1

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 (183) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +151 -2
  4. package/inventory.json +73 -0
  5. package/package.json +74 -4
  6. package/packages/pi-bg-bash/LICENSE +661 -0
  7. package/packages/pi-bg-bash/NOTICE.md +3 -0
  8. package/packages/pi-bg-bash/README.md +41 -0
  9. package/packages/pi-bg-bash/examples/settings.example.json +6 -0
  10. package/packages/pi-bg-bash/package.json +38 -0
  11. package/packages/pi-bg-bash/src/index.ts +969 -0
  12. package/packages/pi-bg-bash/src/lib/async-work.ts +27 -0
  13. package/packages/pi-bg-bash/test/bg-bash.test.mjs +34 -0
  14. package/packages/pi-claude-usage/LICENSE +661 -0
  15. package/packages/pi-claude-usage/NOTICE.md +3 -0
  16. package/packages/pi-claude-usage/README.md +27 -0
  17. package/packages/pi-claude-usage/package.json +35 -0
  18. package/packages/pi-claude-usage/src/index.ts +130 -0
  19. package/packages/pi-clipboard-image/LICENSE +661 -0
  20. package/packages/pi-clipboard-image/NOTICE.md +3 -0
  21. package/packages/pi-clipboard-image/README.md +27 -0
  22. package/packages/pi-clipboard-image/package.json +36 -0
  23. package/packages/pi-clipboard-image/src/index.ts +527 -0
  24. package/packages/pi-codeblock-borders/LICENSE +661 -0
  25. package/packages/pi-codeblock-borders/NOTICE.md +3 -0
  26. package/packages/pi-codeblock-borders/README.md +30 -0
  27. package/packages/pi-codeblock-borders/package.json +35 -0
  28. package/packages/pi-codeblock-borders/src/index.ts +302 -0
  29. package/packages/pi-codegraph/LICENSE +661 -0
  30. package/packages/pi-codegraph/NOTICE.md +5 -0
  31. package/packages/pi-codegraph/README.md +35 -0
  32. package/packages/pi-codegraph/examples/codegraph.example.json +4 -0
  33. package/packages/pi-codegraph/package.json +37 -0
  34. package/packages/pi-codegraph/src/index.ts +573 -0
  35. package/packages/pi-codegraph/test/codegraph.test.mjs +21 -0
  36. package/packages/pi-compact-tools/LICENSE +661 -0
  37. package/packages/pi-compact-tools/NOTICE.md +3 -0
  38. package/packages/pi-compact-tools/README.md +27 -0
  39. package/packages/pi-compact-tools/package.json +36 -0
  40. package/packages/pi-compact-tools/src/index.ts +1161 -0
  41. package/packages/pi-decisions/LICENSE +661 -0
  42. package/packages/pi-decisions/NOTICE.md +3 -0
  43. package/packages/pi-decisions/README.md +37 -0
  44. package/packages/pi-decisions/examples/service-models.example.json +27 -0
  45. package/packages/pi-decisions/package.json +37 -0
  46. package/packages/pi-decisions/src/index.ts +364 -0
  47. package/packages/pi-decisions/src/lib/service-models.ts +95 -0
  48. package/packages/pi-decisions/test/service-models.test.mjs +71 -0
  49. package/packages/pi-load-pi-dotenv/LICENSE +661 -0
  50. package/packages/pi-load-pi-dotenv/NOTICE.md +3 -0
  51. package/packages/pi-load-pi-dotenv/README.md +27 -0
  52. package/packages/pi-load-pi-dotenv/package.json +32 -0
  53. package/packages/pi-load-pi-dotenv/src/index.ts +66 -0
  54. package/packages/pi-model-go/LICENSE +661 -0
  55. package/packages/pi-model-go/NOTICE.md +5 -0
  56. package/packages/pi-model-go/README.md +33 -0
  57. package/packages/pi-model-go/package.json +36 -0
  58. package/packages/pi-model-go/src/index.ts +171 -0
  59. package/packages/pi-model-go/src/lib/dialog.ts +31 -0
  60. package/packages/pi-model-go/src/lib/model-go.ts +239 -0
  61. package/packages/pi-model-go/test/behavior.test.mjs +17 -0
  62. package/packages/pi-prestart-working/LICENSE +661 -0
  63. package/packages/pi-prestart-working/NOTICE.md +3 -0
  64. package/packages/pi-prestart-working/README.md +25 -0
  65. package/packages/pi-prestart-working/package.json +35 -0
  66. package/packages/pi-prestart-working/src/index.ts +86 -0
  67. package/packages/pi-prestart-working/test/behavior.test.mjs +56 -0
  68. package/packages/pi-root-guard/LICENSE +661 -0
  69. package/packages/pi-root-guard/NOTICE.md +3 -0
  70. package/packages/pi-root-guard/README.md +27 -0
  71. package/packages/pi-root-guard/package.json +32 -0
  72. package/packages/pi-root-guard/src/index.ts +202 -0
  73. package/packages/pi-session-finder/LICENSE +661 -0
  74. package/packages/pi-session-finder/NOTICE.md +3 -0
  75. package/packages/pi-session-finder/README.md +33 -0
  76. package/packages/pi-session-finder/package.json +36 -0
  77. package/packages/pi-session-finder/src/index.ts +306 -0
  78. package/packages/pi-session-finder/src/lib/dialog.ts +31 -0
  79. package/packages/pi-session-finder/test/behavior.test.mjs +18 -0
  80. package/packages/pi-session-id/LICENSE +661 -0
  81. package/packages/pi-session-id/NOTICE.md +3 -0
  82. package/packages/pi-session-id/README.md +31 -0
  83. package/packages/pi-session-id/package.json +35 -0
  84. package/packages/pi-session-id/src/index.ts +20 -0
  85. package/packages/pi-session-id/test/behavior.test.mjs +25 -0
  86. package/packages/pi-session-nav/LICENSE +661 -0
  87. package/packages/pi-session-nav/NOTICE.md +3 -0
  88. package/packages/pi-session-nav/README.md +32 -0
  89. package/packages/pi-session-nav/package.json +36 -0
  90. package/packages/pi-session-nav/src/index.ts +203 -0
  91. package/packages/pi-session-nav/src/lib/dialog.ts +31 -0
  92. package/packages/pi-session-nav/test/behavior.test.mjs +36 -0
  93. package/packages/pi-session-rename/LICENSE +661 -0
  94. package/packages/pi-session-rename/NOTICE.md +3 -0
  95. package/packages/pi-session-rename/README.md +31 -0
  96. package/packages/pi-session-rename/package.json +35 -0
  97. package/packages/pi-session-rename/src/index.ts +456 -0
  98. package/packages/pi-session-rename/test/behavior.test.mjs +10 -0
  99. package/packages/pi-session-retention/LICENSE +661 -0
  100. package/packages/pi-session-retention/NOTICE.md +3 -0
  101. package/packages/pi-session-retention/README.md +62 -0
  102. package/packages/pi-session-retention/package.json +36 -0
  103. package/packages/pi-session-retention/src/README.md +42 -0
  104. package/packages/pi-session-retention/src/core.ts +333 -0
  105. package/packages/pi-session-retention/src/dialog.ts +118 -0
  106. package/packages/pi-session-retention/src/index.ts +311 -0
  107. package/packages/pi-session-retention/src/lib/dialog.ts +31 -0
  108. package/packages/pi-session-retention/src/safety.ts +50 -0
  109. package/packages/pi-session-retention/test/behavior.test.mjs +42 -0
  110. package/packages/pi-skill-usage-log/LICENSE +661 -0
  111. package/packages/pi-skill-usage-log/NOTICE.md +3 -0
  112. package/packages/pi-skill-usage-log/README.md +32 -0
  113. package/packages/pi-skill-usage-log/package.json +36 -0
  114. package/packages/pi-skill-usage-log/src/index.ts +316 -0
  115. package/packages/pi-skill-usage-log/src/lib/dialog.ts +31 -0
  116. package/packages/pi-skill-usage-log/src/lib/usage-log.ts +62 -0
  117. package/packages/pi-skill-usage-log/test/behavior.test.mjs +34 -0
  118. package/packages/pi-tool-usage-log/LICENSE +661 -0
  119. package/packages/pi-tool-usage-log/NOTICE.md +5 -0
  120. package/packages/pi-tool-usage-log/README.md +32 -0
  121. package/packages/pi-tool-usage-log/package.json +36 -0
  122. package/packages/pi-tool-usage-log/src/index.ts +233 -0
  123. package/packages/pi-tool-usage-log/src/lib/dialog.ts +31 -0
  124. package/packages/pi-tool-usage-log/src/lib/usage-log.ts +62 -0
  125. package/packages/pi-tool-usage-log/src/skill-usage-log.ts +316 -0
  126. package/packages/pi-tool-usage-log/test/behavior.test.mjs +23 -0
  127. package/packages/pi-tts/LICENSE +661 -0
  128. package/packages/pi-tts/NOTICE.md +3 -0
  129. package/packages/pi-tts/README.md +37 -0
  130. package/packages/pi-tts/examples/service-models.example.json +27 -0
  131. package/packages/pi-tts/package.json +37 -0
  132. package/packages/pi-tts/src/index.ts +135 -0
  133. package/packages/pi-tts/src/lib/service-models.ts +95 -0
  134. package/packages/pi-tts/test/tts-output.test.mjs +47 -0
  135. package/packages/pi-tts/test/tts.test.mjs +57 -0
  136. package/packages/pi-user-image-previews/LICENSE +661 -0
  137. package/packages/pi-user-image-previews/NOTICE.md +3 -0
  138. package/packages/pi-user-image-previews/README.md +27 -0
  139. package/packages/pi-user-image-previews/package.json +36 -0
  140. package/packages/pi-user-image-previews/src/index.ts +168 -0
  141. package/packages/pi-vision/LICENSE +661 -0
  142. package/packages/pi-vision/NOTICE.md +5 -0
  143. package/packages/pi-vision/README.md +43 -0
  144. package/packages/pi-vision/examples/vision-toolkit.example.json +12 -0
  145. package/packages/pi-vision/package.json +38 -0
  146. package/packages/pi-vision/skills/vision-tools/LICENSE +21 -0
  147. package/packages/pi-vision/skills/vision-tools/SKILL.md +134 -0
  148. package/packages/pi-vision/skills/vision-tools/agents/openai.yaml +4 -0
  149. package/packages/pi-vision/skills/vision-tools/bin/crop +94 -0
  150. package/packages/pi-vision/skills/vision-tools/bin/detect +18 -0
  151. package/packages/pi-vision/skills/vision-tools/bin/glance +98 -0
  152. package/packages/pi-vision/skills/vision-tools/bin/ground +18 -0
  153. package/packages/pi-vision/skills/vision-tools/bin/trace +134 -0
  154. package/packages/pi-vision/skills/vision-tools/bin/vision-doctor +168 -0
  155. package/packages/pi-vision/skills/vision-tools/detect.py +56 -0
  156. package/packages/pi-vision/skills/vision-tools/ground.py +216 -0
  157. package/packages/pi-vision/skills/vision-tools/references/gui.md +93 -0
  158. package/packages/pi-vision/skills/vision-tools/references/long-screenshot-ocr.md +103 -0
  159. package/packages/pi-vision/skills/vision-tools/references/restore-graphic.md +92 -0
  160. package/packages/pi-vision/skills/vision-tools/references/restore-structure.md +47 -0
  161. package/packages/pi-vision/skills/vision-tools/references/restore-ui.md +200 -0
  162. package/packages/pi-vision/skills/vision-tools/scripts/dominant_colors.py +224 -0
  163. package/packages/pi-vision/skills/vision-tools/scripts/extract_fg.py +278 -0
  164. package/packages/pi-vision/skills/vision-tools/scripts/html_shot.py +111 -0
  165. package/packages/pi-vision/skills/vision-tools/scripts/long_screenshot_ocr.py +1245 -0
  166. package/packages/pi-vision/skills/vision-tools/scripts/pixel_diff.py +88 -0
  167. package/packages/pi-vision/skills/vision-tools/vision_client.py +581 -0
  168. package/packages/pi-vision/src/index.ts +728 -0
  169. package/packages/pi-vision/test/vision.test.mjs +30 -0
  170. package/packages/pi-wait/LICENSE +661 -0
  171. package/packages/pi-wait/NOTICE.md +3 -0
  172. package/packages/pi-wait/README.md +37 -0
  173. package/packages/pi-wait/package.json +37 -0
  174. package/packages/pi-wait/src/index.ts +88 -0
  175. package/packages/pi-wait/src/lib/async-work.ts +27 -0
  176. package/packages/pi-wait/test/wait.test.mjs +43 -0
  177. package/packages/pi-zvec/LICENSE +661 -0
  178. package/packages/pi-zvec/NOTICE.md +3 -0
  179. package/packages/pi-zvec/README.md +37 -0
  180. package/packages/pi-zvec/examples/zvec.example.json +5 -0
  181. package/packages/pi-zvec/package.json +37 -0
  182. package/packages/pi-zvec/src/index.ts +754 -0
  183. package/packages/pi-zvec/test/zvec.test.mjs +19 -0
@@ -0,0 +1,27 @@
1
+ # Agent configuration root guard
2
+
3
+ Blocks selected write/edit and simple bash writes into Pi and Claude configuration roots.
4
+
5
+ ## Try locally
6
+
7
+ From the repository root:
8
+
9
+ ```sh
10
+ pi -e ./packages/pi-root-guard
11
+ ```
12
+
13
+ This uses your normal personal resources unless you isolate the agent home. Follow the [root README](../../README.md#isolated-sessions) to avoid loading an existing copy of the extension. Nothing is installed globally.
14
+
15
+ ## Configuration and requirements
16
+
17
+ Uses PI_CODING_AGENT_DIR, CLAUDE_CONFIG_DIR, and optional PI_GUARDRAIL_TEMP. Allows managed Pi subdirectories (agents, extensions, skills, npm, git, temp) and selected Claude working directories.
18
+
19
+ ## Compatibility and safety
20
+
21
+ A guardrail, NOT a filesystem sandbox. Shell parsing is heuristic; arbitrary subprocess writes, aliases/symlinks, and unhandled tools may bypass it. The preserved source also has false positives: `echo "not a redirection > settings.json"` inside a guarded root is treated as a possible write, despite the quoted `>`. Existing error handling is preserved. Use OS permissions/sandboxing for a security boundary.
22
+
23
+ Imported host APIs are declared as peer dependencies and supplied by the Pi host. A `*` peer range describes host resolution, not a guarantee of compatibility with every release. See root verification notes for what was actually exercised.
24
+
25
+ ## Status
26
+
27
+ Part of the [pi-extensions](../../README.md) collection. No personal settings or credentials are included; see [NOTICE.md](NOTICE.md) for licensing and provenance.
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@saifymatteo/pi-root-guard",
3
+ "version": "0.1.1",
4
+ "description": "Blocks selected write/edit and simple bash writes into Pi and Claude configuration roots.",
5
+ "type": "module",
6
+ "license": "AGPL-3.0-only",
7
+ "keywords": [
8
+ "pi-package"
9
+ ],
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "NOTICE.md",
14
+ "LICENSE"
15
+ ],
16
+ "pi": {
17
+ "extensions": [
18
+ "./src/index.ts"
19
+ ]
20
+ },
21
+ "engines": {
22
+ "node": ">=24"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/saifymatteo/pi-extensions.git",
30
+ "directory": "packages/pi-root-guard"
31
+ }
32
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * pi-root-guard.ts — stop the agent from writing into the two agent config
3
+ * roots: the Pi root (PI_CODING_AGENT_DIR, default ~/.pi/agent) and the Claude
4
+ * Code root (CLAUDE_CONFIG_DIR, default ~/.claude). The Pi-root rule is what
5
+ * caused earlier research clutter (pkgextract.py, *.html, *.tgz,
6
+ * repo_tree.json, ...) in D:\Pi.
7
+ *
8
+ * Rules (these two roots only; project folders are not guarded):
9
+ * - File writes inside the Pi root are BLOCKED except under the managed
10
+ * agents/, extensions/, skills/, npm/, git/, and temp/ subdirectories.
11
+ * Refused with a reason directing the agent to a temp dir.
12
+ * - File writes inside the Claude root are BLOCKED except where Claude Code
13
+ * itself writes without asking (plans/, jobs/<id>/tmp/, memory *.md), so
14
+ * Pi can stand in for Claude Code as a harness. Claude Code prompts for
15
+ * the rest; here they are always refused (its dontAsk behaviour) — asking
16
+ * the user is pi-guardrails' job.
17
+ *
18
+ * Intercepts tool_call for `write`/`edit` (exact `path`) plus a conservative
19
+ * `bash` heuristic (redirects, tee, curl -o, cp/mv dst, git clone, tar -x -C).
20
+ * ponytail: bash parsing is heuristic, not a full shell parser — subprocess
21
+ * writes to arbitrary computed paths can slip through. The deterministic
22
+ * fix for those is agent temp-dir discipline, not a shell parser.
23
+ *
24
+ * Cwd independence: the roots are resolved to an ABSOLUTE path (env var expanded,
25
+ * tilde expanded, homedir fallback), so the guard protects the Pi root no
26
+ * matter where the session cwd is. Candidate paths get the same tilde/env
27
+ * expansion, and relative paths resolve against the session cwd (ctx.cwd)
28
+ * rather than the launcher's process.cwd().
29
+ */
30
+ import { homedir } from 'node:os'
31
+ import { join, resolve } from 'node:path'
32
+
33
+ const norm = (p: string): string => p.replace(/\\/g, '/')
34
+ const lower = (p: string): string => norm(p).toLowerCase()
35
+
36
+ const isUnder = (p: string, dir: string): boolean =>
37
+ p === dir || p.startsWith(dir.endsWith('/') ? dir : dir + '/')
38
+
39
+ /** Normalize git-bash `/d/...` (and `D:\`) to drive form `D:/...`. */
40
+ const drivePath = (raw: string): string =>
41
+ raw.replace(/\\/g, '/').replace(/^\/([a-zA-Z])\//, (_m, c: string) => c.toUpperCase() + ':/')
42
+
43
+ /**
44
+ * Expand `~/...` to the real home dir and defined environment variables
45
+ * ($VAR, ${VAR} — bash style) in a candidate path. Undefined variables are
46
+ * left untouched: this mirrors what bash would actually write, and keeps the
47
+ * guard conservative without inventing paths the shell would never produce.
48
+ */
49
+ function expandPath(raw: string): string {
50
+ let p = raw
51
+ if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
52
+ p = join(homedir(), p.slice(1))
53
+ }
54
+ p = p.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (m, braced, plain) => {
55
+ const v = process.env[braced ?? plain]
56
+ return v === undefined ? m : v
57
+ })
58
+ return p
59
+ }
60
+
61
+ /** Absolute, normalized root from an env override, else ~/<fallback...>. */
62
+ function absRoot(override: string | undefined, ...fallback: string[]): string {
63
+ const raw = override?.trim()
64
+ const r = drivePath(raw ? expandPath(raw) : join(homedir(), ...fallback))
65
+ return norm(resolve(homedir(), r)) // normalize absolute paths too; relative overrides anchor at home
66
+ }
67
+
68
+ /** Absolute, normalized Pi root: PI_CODING_AGENT_DIR if set, else ~/.pi/agent. */
69
+ export function envRoot(): string {
70
+ return absRoot(process.env.PI_CODING_AGENT_DIR, '.pi', 'agent')
71
+ }
72
+
73
+ /** Absolute, normalized Claude Code root: CLAUDE_CONFIG_DIR if set, else ~/.claude. */
74
+ export function claudeRoot(): string {
75
+ return absRoot(process.env.CLAUDE_CONFIG_DIR, '.claude')
76
+ }
77
+
78
+ const GUARD_ROOT = envRoot()
79
+ const CLAUDE_ROOT = claudeRoot()
80
+ const TEMP = process.env.PI_GUARDRAIL_TEMP || (GUARD_ROOT ? GUARD_ROOT + '/temp' : 'temp')
81
+ // temp/ is the guard's own escape hatch.
82
+ const PI_ALLOW = ['agents', 'extensions', 'skills', 'npm', 'git', 'temp']
83
+
84
+ // Claude root paths Claude Code writes without asking, copied from
85
+ // https://code.claude.com/docs/en/permission-modes#protected-paths (2026-10-26).
86
+ const CLAUDE_EXCEPTIONS = [
87
+ /^plans\//,
88
+ /^jobs\/[^/]+\/tmp\//,
89
+ /^projects\/[^/]+\/memory\/.+\.md$/,
90
+ /^agent-memory\/.+\.md$/,
91
+ ]
92
+
93
+ /** true when an absolute path is inside the Claude root and not one of Claude Code's exceptions. */
94
+ export function protectedPath(absPath: string, root = CLAUDE_ROOT): boolean {
95
+ if (!root) return false
96
+ const a = lower(resolve(drivePath(absPath)))
97
+ const r = lower(resolve(drivePath(root)))
98
+ if (!isUnder(a, r)) return false
99
+ const rel = a.slice(r.length + 1)
100
+ return !CLAUDE_EXCEPTIONS.some((re) => re.test(rel))
101
+ }
102
+
103
+ /** true when an absolute path is inside the guarded root and NOT allowed. */
104
+ export function blocked(absPath: string, root = GUARD_ROOT, allow = PI_ALLOW): boolean {
105
+ if (!root) return false
106
+ const a = lower(resolve(drivePath(absPath)))
107
+ const r = lower(resolve(drivePath(root)))
108
+ if (!isUnder(a, r)) return false
109
+ return !allow.some((dir) => isUnder(a, lower(join(r, dir))))
110
+ }
111
+
112
+ /** Resolve a possibly-relative path against a base and return abs path iff blocked. */
113
+ function blockedResolved(raw: string, base: string): string | null {
114
+ try {
115
+ const d = drivePath(expandPath(raw))
116
+ const abs = resolve(drivePath(base), d)
117
+ return blocked(abs) || protectedPath(abs) ? abs : null
118
+ } catch {
119
+ return null
120
+ }
121
+ }
122
+
123
+ /** Cwd after a leading `cd <dir>`; quoted occurrences inside other commands do not count. */
124
+ function baseDir(cmd: string, fallback: string): string {
125
+ const cd = /^\s*cd\s+(?:--\s+)?(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(cmd)
126
+ if (!cd) return fallback
127
+ try {
128
+ return resolve(drivePath(fallback), drivePath(expandPath(cd[1] ?? cd[2] ?? cd[3])))
129
+ } catch {
130
+ return fallback
131
+ }
132
+ }
133
+
134
+ /** Scan simple commands in order; this remains a heuristic, not a shell parser. */
135
+ export function blockedBash(cmd: string, base: string = process.cwd()): string | null {
136
+ // Preserve quoted/escaped separators instead of treating them as command boundaries.
137
+ const commands = cmd.match(/(?:[^"';&|()\r\n\\]|\\[\s\S]|"(?:\\[\s\S]|[^"\\])*"|'[^']*')+/g) ?? []
138
+ let cwd = base
139
+ for (const command of commands) {
140
+ const ups: string[] = [] // candidate paths for this command's cwd only
141
+ const pushRaw = (r: string | undefined): void => {
142
+ if (r) ups.push(r)
143
+ }
144
+
145
+ // Match >> greedily so its second > cannot become the captured destination.
146
+ const redir = /(?:^|\s)[12]?>>?\s*(?:"([^"]+)"|'([^']+)'|(\S+))/g
147
+ let m: RegExpExecArray | null
148
+ while ((m = redir.exec(command))) pushRaw(m[1] ?? m[2] ?? m[3])
149
+
150
+ // curl/wget -o/--output and curl -O (URL basename in cwd).
151
+ const curl = /(?:curl|wget)\s+(?:.*\s)?(?:-o|--output)\s+"?([^\s"]+)"?/gi
152
+ while ((m = curl.exec(command))) pushRaw(m[1])
153
+ if (/(?:curl|wget)\s+-O\b/i.test(command)) ups.push('.')
154
+
155
+ // Destinations are the last argument of this command, not of the whole script.
156
+ const lastArg = (verbs: RegExp): string | null => {
157
+ const idx = command.search(verbs)
158
+ if (idx < 0) return null
159
+ const args = command.slice(idx).match(/(?:"([^"]+)"|'([^']+)'|(\S+))/g) ?? []
160
+ return args.length ? args[args.length - 1].replace(/^["']|["']$/g, '') : null
161
+ }
162
+ pushRaw(lastArg(/\bcp\s+/) ?? '')
163
+ pushRaw(lastArg(/\bmv\s+/) ?? '')
164
+ pushRaw(lastArg(/\btee\s+/) ?? '')
165
+ pushRaw(lastArg(/\bgit\s+clone\s+/) ?? '')
166
+ pushRaw(lastArg(/\btar\s+.*\s+-C\s+/) ?? '')
167
+
168
+ for (const u of ups) {
169
+ const hit = blockedResolved(u, cwd)
170
+ if (hit) return hit
171
+ }
172
+ // Redirections occur before cd changes cwd; following commands use the new cwd.
173
+ cwd = baseDir(command, cwd)
174
+ }
175
+ return null
176
+ }
177
+
178
+ export default function register(pi: any): void {
179
+ const piReason = `writes to the Pi root are blocked by the pi-root-guard guardrail — put work artifacts in the temp dir (${TEMP}) instead.`
180
+ const claudeReason = `writes to the Claude root (${CLAUDE_ROOT}) are blocked by the pi-root-guard guardrail — ask the user to make this change.`
181
+ const reason = (hit: string): string => `[pi-root-guard] ${blocked(hit) ? piReason : claudeReason}`
182
+ pi.on('tool_call', (event: any, ctx: any) => {
183
+ try {
184
+ const t = event?.toolName
185
+ const inp = event?.input ?? {}
186
+ // Session cwd (tools execute there) with process.cwd() as a fallback.
187
+ const sessionCwd: string =
188
+ ctx && typeof ctx.cwd === 'string' && ctx.cwd ? ctx.cwd : process.cwd()
189
+ if (t === 'write' || t === 'edit') {
190
+ const hit = typeof inp.path === 'string' ? blockedResolved(inp.path, sessionCwd) : null
191
+ if (hit) return { block: true, reason: `${reason(hit)} (blocked path: ${hit})` }
192
+ } else if (t === 'bash') {
193
+ const cmd: string = typeof inp.command === 'string' ? inp.command : ''
194
+ if (!cmd) return
195
+ const hit = blockedBash(cmd, sessionCwd)
196
+ if (hit) return { block: true, reason: `${reason(hit)} (blocked path: ${hit}, cmd: ${cmd.slice(0, 120)})` }
197
+ }
198
+ } catch {
199
+ /* never break a turn over the guard itself */
200
+ }
201
+ })
202
+ }