@oh-my-pi/pi-coding-agent 1.337.0

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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,1004 @@
1
+ # Extension Loading
2
+
3
+ Unified system for loading hooks, tools, skills, and themes from local files, directories, npm packages, and git repositories.
4
+
5
+ ## Extension Types
6
+
7
+ | Type | Root Entry | Subdir Entry | Purpose |
8
+ |------|------------|--------------|---------|
9
+ | Hooks | `*.ts` / `*.js` | `index.ts` / `index.js` / package.json `main` | Event handlers for agent lifecycle |
10
+ | Tools | `*.ts` / `*.js` | `index.ts` / `index.js` / package.json `main` | Custom tools for the agent |
11
+ | Skills | `*.md` | `SKILL.md` | Context/instructions loaded into agent |
12
+ | Themes | `*.theme.json` | `*.theme.json` (recursive) | Color schemes for TUI |
13
+
14
+ **Note:** Themes use `*.theme.json` pattern scanned recursively at all levels, allowing flat theme packs without requiring subdirectories.
15
+
16
+ ## Sources
17
+
18
+ Extensions can be loaded from:
19
+
20
+ ### File Paths
21
+ ```
22
+ ./my-hook.ts
23
+ ~/global-hook.ts
24
+ /absolute/path/hook.ts
25
+ ```
26
+
27
+ ### Directories
28
+ ```
29
+ ./my-hooks/
30
+ ~/.pi/agent/hooks/
31
+ ```
32
+
33
+ ### npm Packages
34
+ ```
35
+ npm:package-name
36
+ npm:package-name@1.2.3
37
+ npm:package-name@latest
38
+ npm:@scope/package-name
39
+ npm:@scope/package-name@1.2.3
40
+ ```
41
+
42
+ ### Git Repositories
43
+ ```
44
+ git:https://github.com/user/repo
45
+ git:https://github.com/user/repo@v1.0.0 # tag
46
+ git:https://github.com/user/repo@abc123f # commit
47
+ git:https://github.com/user/repo#branch # branch
48
+ ```
49
+
50
+ ## Storage Layout
51
+
52
+ ### Permanent (settings.json)
53
+ ```
54
+ ~/.pi/agent/
55
+ hooks/
56
+ my-local-hook.ts # root-level file
57
+ complex-hook/ # directory with entry point
58
+ index.ts
59
+ utils.ts
60
+ npm/
61
+ my-hook@1.2.3/ # npm package
62
+ package.json
63
+ node_modules/
64
+ index.js
65
+ @scope/
66
+ scoped-hook@2.0.0/
67
+ ...
68
+ git/
69
+ github.com/user/repo@v1.0.0/ # git repo
70
+ ...
71
+ tools/
72
+ ... (same structure)
73
+ skills/
74
+ ... (same structure, but SKILL.md instead of index.ts)
75
+ themes/
76
+ dark.theme.json # root-level theme
77
+ light.theme.json
78
+ community-pack/ # theme pack (no entry point needed)
79
+ nord.theme.json
80
+ dracula.theme.json
81
+ npm/
82
+ cool-themes@1.0.0/
83
+ monokai.theme.json
84
+ solarized.theme.json
85
+ ```
86
+
87
+ ### Ephemeral (CLI flags)
88
+ ```
89
+ /tmp/pi-extensions/
90
+ hooks/
91
+ npm/
92
+ my-hook@1.0.0/
93
+ git/
94
+ github.com/user/repo@v1.0.0/
95
+ tools/
96
+ ...
97
+ skills/
98
+ ...
99
+ themes/
100
+ ...
101
+ ```
102
+
103
+ Temp directory persists until OS clears `/tmp/`. No re-download needed across sessions (usually).
104
+
105
+ ## Entry Point Resolution
106
+
107
+ For each discovered directory, resolve entry point in order:
108
+
109
+ ### Hooks & Tools
110
+ 1. `index.ts` (if exists)
111
+ 2. `index.js` (if exists)
112
+ 3. `main` field in `package.json` (if exists)
113
+
114
+ ### Skills
115
+ 1. `SKILL.md` (required)
116
+
117
+ ### Themes
118
+ Themes use recursive pattern matching instead of fixed entry points:
119
+ - Scan recursively for `*.theme.json` files at all levels
120
+ - Each matching file is a separate theme
121
+ - Path derived from filename (e.g., `dark.theme.json` → `dark`, `pack/nord.theme.json` → `pack/nord`)
122
+
123
+ ## Scanning Algorithm
124
+
125
+ ```
126
+ scan(baseDir, config):
127
+ results = []
128
+
129
+ for entry in baseDir:
130
+ skip if entry.name starts with "."
131
+ skip if entry.name == "node_modules"
132
+ skip if entry.name ends with ".installing"
133
+
134
+ if entry is file:
135
+ if matches rootPattern (e.g., *.ts, *.js, *.md, *.theme.json):
136
+ results.add(entry)
137
+
138
+ if entry is directory:
139
+ if config.recursivePattern:
140
+ # For themes: scan recursively for *.theme.json everywhere
141
+ results.addAll(scan(directory, config))
142
+ else if has entryPoint (index.ts, index.js, SKILL.md):
143
+ results.add(directory) # load as single extension
144
+ else:
145
+ results.addAll(scan(directory, config)) # recurse to find extensions
146
+
147
+ return results
148
+ ```
149
+
150
+ **Default directories scanned (always, regardless of settings.json):**
151
+ - `~/.pi/agent/<type>/`
152
+ - `<cwd>/.pi/<type>/`
153
+
154
+ ## Extension Packs
155
+
156
+ A key use case is pulling in a **pack** (collection) of extensions via a directory, npm package, or git repo, then filtering to a subset.
157
+
158
+ **Example: Skill pack**
159
+ ```
160
+ npm:pi-skills@1.0.0 contains:
161
+ skills/
162
+ brave-search/SKILL.md
163
+ browser-tools/SKILL.md
164
+ transcribe/SKILL.md
165
+ youtube-transcript/SKILL.md
166
+ ... (10+ skills)
167
+ ```
168
+
169
+ You want only 2 of them:
170
+ ```json
171
+ {
172
+ "skills": {
173
+ "paths": ["npm:pi-skills@1.0.0"],
174
+ "filter": ["brave-search", "youtube-transcript"]
175
+ }
176
+ }
177
+ ```
178
+
179
+ **Example: Theme pack**
180
+ ```
181
+ npm:community-themes@1.0.0 contains:
182
+ themes/
183
+ nord.theme.json
184
+ dracula.theme.json
185
+ solarized-dark.theme.json
186
+ solarized-light.theme.json
187
+ monokai.theme.json
188
+ ```
189
+
190
+ Exclude solarized variants:
191
+ ```json
192
+ {
193
+ "themes": {
194
+ "paths": ["npm:community-themes@1.0.0"],
195
+ "filter": ["!solarized-*"]
196
+ }
197
+ }
198
+ ```
199
+
200
+ **Example: Hook pack**
201
+ ```
202
+ npm:audit-hooks@1.0.0 contains:
203
+ hooks/
204
+ file-audit/index.ts
205
+ command-audit/index.ts
206
+ network-audit/index.ts
207
+ debug-logger/index.ts
208
+ ```
209
+
210
+ All except debug:
211
+ ```json
212
+ {
213
+ "hooks": {
214
+ "paths": ["npm:audit-hooks@1.0.0"],
215
+ "filter": ["!debug-*"]
216
+ }
217
+ }
218
+ ```
219
+
220
+ ## Filtering
221
+
222
+ Single filter array with `!` prefix for exclusion. Patterns are matched against extension paths (directory or filename without extension).
223
+
224
+ ```json
225
+ {
226
+ "filter": ["pattern1", "pattern2", "!excluded-pattern"]
227
+ }
228
+ ```
229
+
230
+ **Logic:**
231
+ 1. Collect all patterns without `!` prefix → include patterns
232
+ 2. Collect all patterns with `!` prefix → exclude patterns
233
+ 3. If include patterns exist: start with extensions matching any include pattern
234
+ 4. If no include patterns: start with all extensions
235
+ 5. Remove extensions matching any exclude pattern
236
+
237
+ **Examples:**
238
+ ```json
239
+ ["brave-search"] // only brave-search
240
+ ["brave-*", "docker"] // brave-search, brave-api, docker
241
+ ["!transcribe"] // all except transcribe
242
+ ["audit-*", "!audit-debug"] // audit-* except audit-debug
243
+ ```
244
+
245
+ Patterns are glob patterns matched against extension paths.
246
+
247
+ ## CLI Arguments
248
+
249
+ ### Adding Sources
250
+ ```bash
251
+ pi --hook <path|npm:|git:> # add hook source (repeatable)
252
+ pi --tool <path|npm:|git:> # add custom tool source (repeatable)
253
+ pi --skill <path|npm:|git:> # add skill source (repeatable)
254
+ pi --theme <path|npm:|git:> # add theme source (repeatable)
255
+ ```
256
+
257
+ **Installation locations for npm/git sources:**
258
+
259
+ | Source | Install location |
260
+ |--------|------------------|
261
+ | CLI flags | `/tmp/pi-extensions/<type>/npm/` or `git/` |
262
+ | Global settings (`~/.pi/agent/settings.json`) | `~/.pi/agent/<type>/npm/` or `git/` |
263
+ | Project settings (`<cwd>/.pi/settings.json`) | `<cwd>/.pi/<type>/npm/` or `git/` |
264
+
265
+ File/directory paths are used directly (no installation).
266
+
267
+ - **CLI = ephemeral**: cached in temp until OS clears `/tmp/`
268
+ - **Global settings = permanent**: installed to user's agent directory
269
+ - **Project settings = project-local**: installed to project's `.pi/` directory
270
+
271
+ Examples:
272
+ - `--hook npm:my-hook@1.0.0` → `/tmp/pi-extensions/hooks/npm/my-hook@1.0.0/`
273
+ - Global settings.json `npm:my-hook@1.0.0` → `~/.pi/agent/hooks/npm/my-hook@1.0.0/`
274
+ - Project settings.json `npm:my-hook@1.0.0` → `<cwd>/.pi/hooks/npm/my-hook@1.0.0/`
275
+
276
+ This encourages: try via CLI, if you like it, add to settings.json for permanent install.
277
+
278
+ ### Filtering
279
+ ```bash
280
+ pi --hooks "pattern1,pattern2,!excluded" # filter hooks
281
+ pi --custom-tools "pattern1,!excluded" # filter custom tools
282
+ pi --skills "pattern1,pattern2" # filter skills
283
+ pi --themes "pattern1" # filter themes
284
+ ```
285
+
286
+ ### Disabling
287
+ ```bash
288
+ pi --no-hooks # disable all hooks
289
+ pi --no-custom-tools # disable all custom tools
290
+ pi --no-skills # disable all skills (already exists)
291
+ ```
292
+
293
+ ### Built-in Tools
294
+ ```bash
295
+ pi --tools read,bash,edit,write # select which built-in tools to enable (unchanged)
296
+ ```
297
+
298
+ ## Settings Hierarchy
299
+
300
+ Extensions are configured in settings.json at two levels:
301
+ - **Global**: `~/.pi/agent/settings.json`
302
+ - **Project**: `<cwd>/.pi/settings.json`
303
+
304
+ **Merge behavior:**
305
+ - `paths`: **additive** - project paths are added to global paths
306
+ - `filter`: **override** - project filter replaces global filter if specified
307
+
308
+ **Example:**
309
+ ```json
310
+ // Global: ~/.pi/agent/settings.json
311
+ {
312
+ "hooks": {
313
+ "paths": ["npm:audit-hooks@1.0.0"],
314
+ "filter": ["!debug-*"]
315
+ }
316
+ }
317
+
318
+ // Project: .pi/settings.json
319
+ {
320
+ "hooks": {
321
+ "paths": ["./project-hooks/"],
322
+ "filter": ["audit-*"] // overrides global filter
323
+ }
324
+ }
325
+
326
+ // Effective:
327
+ {
328
+ "hooks": {
329
+ "paths": ["npm:audit-hooks@1.0.0", "./project-hooks/"],
330
+ "filter": ["audit-*"]
331
+ }
332
+ }
333
+ ```
334
+
335
+ ## settings.json Structure
336
+
337
+ ```json
338
+ {
339
+ "hooks": {
340
+ "paths": [
341
+ "./my-hooks/",
342
+ "npm:@scope/hook@1.0.0",
343
+ "git:https://github.com/user/hooks@v1.0.0"
344
+ ],
345
+ "filter": ["audit-*", "!audit-debug"]
346
+ },
347
+ "tools": {
348
+ "paths": ["npm:cool-tools@2.0.0"],
349
+ "filter": ["!dangerous-tool"]
350
+ },
351
+ "skills": {
352
+ "paths": ["npm:pi-skills@1.0.0", "~/my-skills/"],
353
+ "filter": ["brave-search", "git-*", "!git-legacy"]
354
+ },
355
+ "themes": {
356
+ "paths": ["npm:community-themes@1.0.0"]
357
+ }
358
+ }
359
+ ```
360
+
361
+ **Migration from current format:**
362
+ - `hooks: string[]` → `hooks.paths: string[]`
363
+ - `customTools: string[]` → `tools.paths: string[]`
364
+ - `skills.customDirectories` → `skills.paths`
365
+ - `skills.includeSkills` → `skills.filter` (patterns without `!`)
366
+ - `skills.ignoredSkills` → `skills.filter` (patterns with `!` prefix)
367
+
368
+ ## Installation Flow
369
+
370
+ Target directory depends on source:
371
+ - **CLI flags**: `/tmp/pi-extensions/<type>/npm/` or `git/`
372
+ - **Global settings.json**: `~/.pi/agent/<type>/npm/` or `git/`
373
+ - **Project settings.json**: `<cwd>/.pi/<type>/npm/` or `git/`
374
+
375
+ ### Atomic Installation
376
+
377
+ To prevent corrupted state from interrupted installs (Ctrl+C):
378
+ 1. Install to `<target>.installing/` (temporary)
379
+ 2. On success, atomically rename to `<target>/`
380
+ 3. If interrupted, `<target>/` doesn't exist → next run retries cleanly
381
+ 4. Scanner filters out `*.installing` directories (see Scanning Algorithm)
382
+
383
+ ### npm Packages
384
+ 1. Parse specifier: `npm:@scope/pkg@1.2.3` → name: `@scope/pkg`, version: `1.2.3`
385
+ 2. Determine target dir based on source (CLI → temp, global → agent dir, project → cwd/.pi/)
386
+ 3. If `<target>/` exists and has matching version in package.json → skip install
387
+ 4. Otherwise:
388
+ - Remove stale `<target>.installing/` if exists
389
+ - `npm pack @scope/pkg@1.2.3` → download tarball
390
+ - Extract to `<target>.installing/`
391
+ - If `package.json` has `dependencies` → run `npm install`
392
+ - Rename `<target>.installing/` → `<target>/`
393
+
394
+ ### Git Repositories
395
+ 1. Parse specifier: `git:https://github.com/user/repo@v1.0.0`
396
+ 2. Determine target dir based on source (CLI → temp, global → agent dir, project → cwd/.pi/)
397
+ 3. If `<target>/` exists → skip clone
398
+ 4. Otherwise:
399
+ - Remove stale `<target>.installing/` if exists
400
+ - `git clone <url>` to `<target>.installing/`
401
+ - `git checkout <tag|commit|branch>`
402
+ - If `package.json` has `dependencies` → run `npm install`
403
+ - Rename `<target>.installing/` → `<target>/`
404
+
405
+ ## Extension Management Commands
406
+
407
+ ### Install
408
+
409
+ Adds extension to settings.json and installs to disk.
410
+
411
+ ```bash
412
+ pi install <type> <source> # global (default)
413
+ pi install <type> -p <source> # project-local
414
+ pi install <type> --project <source> # project-local
415
+
416
+ # Examples:
417
+ pi install hook npm:@scope/my-hook@1.0.0
418
+ # → adds to ~/.pi/agent/settings.json
419
+ # → installs to ~/.pi/agent/hooks/npm/@scope/my-hook@1.0.0/
420
+
421
+ pi install tool -p git:https://github.com/user/tool@v1.0.0
422
+ # → adds to <cwd>/.pi/settings.json
423
+ # → installs to <cwd>/.pi/tools/git/github.com/user/tool@v1.0.0/
424
+ ```
425
+
426
+ ### Remove
427
+
428
+ Removes extension from settings.json and deletes from disk.
429
+
430
+ ```bash
431
+ pi remove <type> <name> # from global
432
+ pi remove <type> -p <name> # from project
433
+
434
+ # Examples:
435
+ pi remove hook my-hook # from ~/.pi/agent/settings.json + delete
436
+ pi remove skill -p brave-search # from <cwd>/.pi/settings.json + delete
437
+ ```
438
+
439
+ ### Update
440
+
441
+ Updates npm/git extensions to latest versions.
442
+
443
+ ```bash
444
+ pi update # all (project + global)
445
+ pi update -p # project only
446
+ pi update <type>... # specific types
447
+ pi update -p <type>... # project, specific types
448
+
449
+ # Examples:
450
+ pi update # update everything
451
+ pi update hook tool # update hooks and tools
452
+ pi update -p skill # update project skills only
453
+ ```
454
+
455
+ **Update behavior:**
456
+ - `npm:pkg@<version>`: check if newer version exists (e.g., `@latest` resolves to newer)
457
+ - `git:repo#branch`: `git pull`
458
+ - `git:repo@tag` or `git:repo@commit`: no-op (pinned)
459
+ - Local files/directories: no-op
460
+
461
+ ## Loading Flow (Full)
462
+
463
+ 1. **Collect sources:**
464
+ - Default directories: `~/.pi/agent/<type>/`, `./.pi/<type>/`
465
+ - settings.json `<type>.paths`
466
+ - CLI `--<type>` arguments
467
+
468
+ 2. **Install remote sources:**
469
+ - Process `npm:` and `git:` specifiers
470
+ - Install to `~/.pi/agent/<type>/npm/` or `git/`
471
+
472
+ 3. **Scan all sources:**
473
+ - Recursively discover extensions
474
+ - Compute relative path for each
475
+
476
+ 4. **Apply filter:**
477
+ - Combine settings.json `<type>.filter` and CLI `--<type>s` patterns
478
+ - Filter by path (no loading yet)
479
+
480
+ 5. **Load survivors:**
481
+ - Parse/execute only extensions that passed filter
482
+ - Validate (frontmatter, exports, schema)
483
+ - Report errors for invalid extensions
484
+
485
+ ---
486
+
487
+ # Implementation Plan
488
+
489
+ ## Overview
490
+
491
+ This implementation consolidates four separate loading systems (hooks, tools, skills, themes) into a unified extension loading framework with shared logic for source resolution, installation, scanning, filtering, and loading.
492
+
493
+ ## New Files
494
+
495
+ ### `src/core/extensions/types.ts`
496
+ Extension type definitions shared across all loaders.
497
+
498
+ ```typescript
499
+ export type ExtensionType = "hooks" | "tools" | "skills" | "themes";
500
+
501
+ export interface ExtensionSource {
502
+ type: "file" | "directory" | "npm" | "git";
503
+ specifier: string; // original specifier from config/CLI
504
+ resolvedPath?: string; // resolved local path after install
505
+ }
506
+
507
+ export interface DiscoveredExtension {
508
+ path: string; // relative path (e.g., "brave-search", "npm/@scope/pkg@1.0.0")
509
+ absolutePath: string; // absolute filesystem path
510
+ entryPoint: string; // resolved entry point file
511
+ source: ExtensionSource;
512
+ }
513
+
514
+ export interface ExtensionConfig {
515
+ paths?: string[];
516
+ filter?: string[];
517
+ }
518
+
519
+ export interface ExtensionTypeConfig {
520
+ rootPatterns: string[]; // e.g., ["*.ts", "*.js"]
521
+ subdirEntryPoints: string[]; // e.g., ["index.ts", "index.js"]
522
+ packageJsonFallback: boolean; // whether to check package.json main
523
+ }
524
+
525
+ export const EXTENSION_CONFIGS: Record<ExtensionType, ExtensionTypeConfig> = {
526
+ hooks: {
527
+ rootPatterns: ["*.ts", "*.js"],
528
+ subdirEntryPoints: ["index.ts", "index.js"],
529
+ packageJsonFallback: true,
530
+ recursivePattern: false,
531
+ },
532
+ tools: {
533
+ rootPatterns: ["*.ts", "*.js"],
534
+ subdirEntryPoints: ["index.ts", "index.js"],
535
+ packageJsonFallback: true,
536
+ recursivePattern: false,
537
+ },
538
+ skills: {
539
+ rootPatterns: ["*.md"],
540
+ subdirEntryPoints: ["SKILL.md"],
541
+ packageJsonFallback: false,
542
+ recursivePattern: false,
543
+ },
544
+ themes: {
545
+ rootPatterns: ["*.theme.json"],
546
+ subdirEntryPoints: [], // not used
547
+ packageJsonFallback: false,
548
+ recursivePattern: true, // scan for *.theme.json at all levels
549
+ },
550
+ };
551
+ ```
552
+
553
+ ### `src/core/extensions/source-resolver.ts`
554
+ Handles parsing and installing npm/git sources.
555
+
556
+ ```typescript
557
+ export function parseSource(specifier: string): ExtensionSource;
558
+ export type InstallLocation = "cli" | "global" | "project";
559
+
560
+ export async function installSource(
561
+ source: ExtensionSource,
562
+ type: ExtensionType,
563
+ location: InstallLocation,
564
+ cwd: string, // needed for project-local installs
565
+ ): Promise<string>;
566
+ export function isRemoteSource(specifier: string): boolean;
567
+ export function getInstallDir(type: ExtensionType, location: InstallLocation, cwd: string): string;
568
+ ```
569
+
570
+ Key functions:
571
+ - `parseNpmSpecifier(spec)`: Parse `npm:@scope/pkg@1.2.3` → `{ name, version }`
572
+ - `parseGitSpecifier(spec)`: Parse `git:url@tag` or `git:url#branch`
573
+ - `installNpmPackage(name, version, targetDir)`: `npm pack` + extract + `npm install`
574
+ - `installGitRepo(url, ref, targetDir)`: `git clone` + checkout + `npm install`
575
+ - `getTargetDir(type, ephemeral)`: Returns temp dir or agent dir based on source
576
+
577
+ ### `src/core/extensions/scanner.ts`
578
+ Unified recursive scanning for all extension types.
579
+
580
+ ```typescript
581
+ export function scanDirectory(
582
+ baseDir: string,
583
+ config: ExtensionTypeConfig,
584
+ ): DiscoveredExtension[];
585
+
586
+ export function resolveEntryPoint(
587
+ dir: string,
588
+ config: ExtensionTypeConfig,
589
+ ): string | null;
590
+
591
+ export function getRelativePath(
592
+ absolutePath: string,
593
+ baseDir: string,
594
+ config: ExtensionTypeConfig,
595
+ ): string; // strips entry point filename and extension
596
+ ```
597
+
598
+ ### `src/core/extensions/filter.ts`
599
+ Filter logic using glob patterns with `!` exclusion. Matches against `extension.path`.
600
+
601
+ ```typescript
602
+ export function applyFilter(
603
+ extensions: DiscoveredExtension[],
604
+ patterns: string[],
605
+ ): DiscoveredExtension[];
606
+
607
+ export function parseFilterPatterns(patterns: string[]): {
608
+ include: string[];
609
+ exclude: string[];
610
+ };
611
+
612
+ export function matchesPattern(path: string, pattern: string): boolean;
613
+ ```
614
+
615
+ ### `src/core/extensions/loader.ts`
616
+ Main entry point coordinating the full loading flow.
617
+
618
+ ```typescript
619
+ export interface LoadExtensionsOptions {
620
+ type: ExtensionType;
621
+ cwd: string;
622
+ agentDir: string;
623
+ globalPaths: string[]; // from global settings.json → install to agentDir
624
+ projectPaths: string[]; // from project settings.json → install to cwd/.pi/
625
+ cliPaths: string[]; // from CLI flags → install to /tmp/
626
+ filter: string[]; // combined filter patterns
627
+ }
628
+
629
+ export interface LoadExtensionsResult<T> {
630
+ extensions: T[];
631
+ errors: Array<{ path: string; error: string }>;
632
+ }
633
+
634
+ export async function discoverExtensions(
635
+ options: LoadExtensionsOptions,
636
+ ): Promise<DiscoveredExtension[]>;
637
+ ```
638
+
639
+ ### `src/core/extensions/index.ts`
640
+ Public exports.
641
+
642
+ ## Modified Files
643
+
644
+ ### `src/config.ts`
645
+
646
+ Add directory getters:
647
+
648
+ ```typescript
649
+ export function getHooksDir(): string {
650
+ return join(getAgentDir(), "hooks");
651
+ }
652
+
653
+ export function getSkillsDir(): string {
654
+ return join(getAgentDir(), "skills");
655
+ }
656
+
657
+ // getToolsDir() already exists
658
+ // getThemesDir() = bundled themes (in package)
659
+ // getCustomThemesDir() = ~/.pi/agent/themes/ (user themes) - already exists
660
+ ```
661
+
662
+ ### `src/core/settings-manager.ts`
663
+
664
+ Update `Settings` interface:
665
+
666
+ ```typescript
667
+ // Old:
668
+ hooks?: string[];
669
+ customTools?: string[];
670
+ skills?: SkillsSettings;
671
+
672
+ // New:
673
+ hooks?: ExtensionConfig;
674
+ tools?: ExtensionConfig;
675
+ skills?: ExtensionConfig; // simplified from SkillsSettings
676
+ themes?: ExtensionConfig;
677
+ ```
678
+
679
+ Add migration logic for old format:
680
+
681
+ ```typescript
682
+ function migrateSettings(settings: unknown): Settings {
683
+ // Convert hooks: string[] → hooks: { paths: string[] }
684
+ // Convert customTools: string[] → tools: { paths: string[] }
685
+ // Convert skills.customDirectories → skills.paths
686
+ // Convert skills.includeSkills/ignoredSkills → skills.filter
687
+ }
688
+ ```
689
+
690
+ Add unified getters:
691
+
692
+ ```typescript
693
+ getExtensionConfig(type: ExtensionType): ExtensionConfig;
694
+ getExtensionPaths(type: ExtensionType): string[];
695
+ getExtensionFilter(type: ExtensionType): string[];
696
+ ```
697
+
698
+ Update merge logic (paths are additive, filter overrides):
699
+
700
+ ```typescript
701
+ function mergeExtensionConfig(global: ExtensionConfig, project: ExtensionConfig): ExtensionConfig {
702
+ return {
703
+ paths: [...(global.paths ?? []), ...(project.paths ?? [])], // additive
704
+ filter: project.filter ?? global.filter, // override
705
+ };
706
+ }
707
+ ```
708
+
709
+ ### `src/cli/args.ts`
710
+
711
+ Update `Args` interface:
712
+
713
+ ```typescript
714
+ // Built-in tools (unchanged)
715
+ tools?: ToolName[]; // --tools read,bash,edit,write
716
+
717
+ // Source flags
718
+ hooks?: string[]; // --hook (existing, repeatable)
719
+ customTools?: string[]; // --tool (existing, repeatable)
720
+ skills?: string[]; // --skill (new, repeatable)
721
+ themes?: string[]; // --theme (new, repeatable)
722
+
723
+ // Filter flags
724
+ hooksFilter?: string[]; // --hooks "patterns"
725
+ customToolsFilter?: string[];// --custom-tools "patterns"
726
+ skillsFilter?: string[]; // --skills "patterns" (existing)
727
+ themesFilter?: string[]; // --themes "patterns"
728
+
729
+ // Disable flags
730
+ noHooks?: boolean; // --no-hooks
731
+ noCustomTools?: boolean; // --no-custom-tools
732
+ noSkills?: boolean; // --no-skills (existing)
733
+ ```
734
+
735
+ Update argument parsing:
736
+
737
+ ```typescript
738
+ // --tools (built-in tools, unchanged)
739
+ } else if (arg === "--tools" && i + 1 < args.length) {
740
+ // ... existing logic for built-in tools
741
+
742
+ // --tool (add custom tool source)
743
+ } else if (arg === "--tool" && i + 1 < args.length) {
744
+ result.customTools = result.customTools ?? [];
745
+ result.customTools.push(args[++i]);
746
+
747
+ // --custom-tools (filter custom tools)
748
+ } else if (arg === "--custom-tools" && i + 1 < args.length) {
749
+ result.customToolsFilter = args[++i].split(",").map(s => s.trim());
750
+
751
+ // --no-custom-tools
752
+ } else if (arg === "--no-custom-tools") {
753
+ result.noCustomTools = true;
754
+
755
+ // --skill (add source) - new
756
+ } else if (arg === "--skill" && i + 1 < args.length) {
757
+ result.skills = result.skills ?? [];
758
+ result.skills.push(args[++i]);
759
+
760
+ // --theme (add source) - new
761
+ } else if (arg === "--theme" && i + 1 < args.length) {
762
+ result.themes = result.themes ?? [];
763
+ result.themes.push(args[++i]);
764
+
765
+ // --themes (filter) - new
766
+ } else if (arg === "--themes" && i + 1 < args.length) {
767
+ result.themesFilter = args[++i].split(",").map(s => s.trim());
768
+
769
+ // --hooks (filter) - new
770
+ } else if (arg === "--hooks" && i + 1 < args.length) {
771
+ result.hooksFilter = args[++i].split(",").map(s => s.trim());
772
+
773
+ // --no-hooks - new
774
+ } else if (arg === "--no-hooks") {
775
+ result.noHooks = true;
776
+ ```
777
+
778
+ Add subcommand handling for `pi install`, `pi remove`, `pi update`.
779
+
780
+ ### `src/core/hooks/loader.ts`
781
+
782
+ Refactor to use extension system:
783
+
784
+ ```typescript
785
+ import { discoverExtensions, type DiscoveredExtension } from "../extensions/index.js";
786
+
787
+ export async function discoverAndLoadHooks(
788
+ options: {
789
+ cwd: string;
790
+ agentDir?: string;
791
+ configuredPaths?: string[];
792
+ cliPaths?: string[];
793
+ filter?: string[];
794
+ }
795
+ ): Promise<LoadHooksResult> {
796
+ const discovered = await discoverExtensions({
797
+ type: "hooks",
798
+ defaultDirs: [join(agentDir, "hooks"), join(cwd, ".pi", "hooks")],
799
+ configuredPaths: options.configuredPaths ?? [],
800
+ cliPaths: options.cliPaths ?? [],
801
+ filter: options.filter ?? [],
802
+ });
803
+
804
+ // Load each discovered hook using existing jiti logic
805
+ const results = await Promise.all(
806
+ discovered.map(ext => loadHook(ext.entryPoint, cwd))
807
+ );
808
+
809
+ // ... rest of existing logic
810
+ }
811
+ ```
812
+
813
+ Remove duplicate code:
814
+ - `expandPath()` → use from extensions/source-resolver.ts
815
+ - `resolveHookPath()` → use from extensions/scanner.ts
816
+ - Discovery logic → use discoverExtensions()
817
+
818
+ ### `src/core/custom-tools/loader.ts`
819
+
820
+ Same refactoring pattern as hooks/loader.ts.
821
+
822
+ ### `src/core/skills.ts`
823
+
824
+ Refactor `loadSkills()` and `loadSkillsFromDir()`:
825
+
826
+ ```typescript
827
+ export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult {
828
+ const discovered = await discoverExtensions({
829
+ type: "skills",
830
+ defaultDirs: [
831
+ // existing default dirs
832
+ join(homedir(), ".codex", "skills"),
833
+ join(homedir(), ".claude", "skills"),
834
+ join(agentDir, "skills"),
835
+ join(cwd, ".pi", "skills"),
836
+ ],
837
+ configuredPaths: options.paths ?? [],
838
+ cliPaths: options.cliPaths ?? [],
839
+ filter: options.filter ?? [],
840
+ });
841
+
842
+ // Load each discovered skill using existing parsing logic
843
+ // ...
844
+ }
845
+ ```
846
+
847
+ Remove:
848
+ - `loadSkillsFromDirInternal()` recursive logic → use scanner.ts
849
+ - `matchesIncludePatterns()`/`matchesIgnorePatterns()` → use filter.ts
850
+
851
+ ### `src/modes/interactive/theme/theme.ts`
852
+
853
+ Refactor `getAvailableThemes()` and `loadThemeJson()`:
854
+
855
+ ```typescript
856
+ export function getAvailableThemes(): string[] {
857
+ const discovered = discoverExtensions({
858
+ type: "themes",
859
+ defaultDirs: [getThemesDir(), getCustomThemesDir()],
860
+ configuredPaths: settingsManager.getExtensionPaths("themes"),
861
+ cliPaths: [], // from args
862
+ filter: settingsManager.getExtensionFilter("themes"),
863
+ });
864
+
865
+ return discovered.map(ext => ext.path);
866
+ }
867
+ ```
868
+
869
+ ### `src/core/sdk.ts`
870
+
871
+ Update to pass new options structure:
872
+
873
+ ```typescript
874
+ // Hooks
875
+ const { hooks, errors } = await discoverAndLoadHooks({
876
+ cwd,
877
+ agentDir,
878
+ configuredPaths: settingsManager.getExtensionPaths("hooks"),
879
+ cliPaths: options.additionalHookPaths,
880
+ filter: [...settingsManager.getExtensionFilter("hooks"), ...(options.hooksFilter ?? [])],
881
+ });
882
+
883
+ // Tools
884
+ const result = await discoverAndLoadCustomTools({
885
+ cwd,
886
+ agentDir,
887
+ configuredPaths: settingsManager.getExtensionPaths("tools"),
888
+ cliPaths: options.additionalToolPaths,
889
+ filter: [...settingsManager.getExtensionFilter("tools"), ...(options.toolsFilter ?? [])],
890
+ builtInToolNames: Object.keys(allTools),
891
+ });
892
+ ```
893
+
894
+ ### `src/modes/interactive/interactive-mode.ts`
895
+
896
+ Update skill loading to use new options structure.
897
+
898
+ ## Migration & Backwards Compatibility
899
+
900
+ ### Settings Migration
901
+
902
+ When loading settings.json, detect old format and migrate:
903
+
904
+ ```typescript
905
+ // Old format detection
906
+ if (Array.isArray(settings.hooks)) {
907
+ settings.hooks = { paths: settings.hooks };
908
+ }
909
+ if (Array.isArray(settings.customTools)) {
910
+ settings.tools = { paths: settings.customTools };
911
+ delete settings.customTools;
912
+ }
913
+ if (settings.skills?.customDirectories) {
914
+ settings.skills.paths = settings.skills.customDirectories;
915
+ delete settings.skills.customDirectories;
916
+ }
917
+ // ... etc
918
+ ```
919
+
920
+ ### CLI Compatibility
921
+
922
+ - `--tools` with built-in tool names still works (detected by checking if values match known tool names)
923
+ - Alternatively, deprecation warning and suggest `--builtin-tools`
924
+
925
+ ## Implementation Order
926
+
927
+ 1. **Phase 1: Core extension framework**
928
+ - Create `src/core/extensions/` directory
929
+ - Implement types.ts, scanner.ts, filter.ts
930
+ - Unit tests for scanning and filtering
931
+
932
+ 2. **Phase 2: Source resolution**
933
+ - Implement source-resolver.ts (npm + git)
934
+ - Add `npm pack` and `git clone` logic
935
+ - Unit tests for source parsing and installation
936
+
937
+ 3. **Phase 3: Settings migration**
938
+ - Update settings-manager.ts with new types
939
+ - Add migration logic
940
+ - Update config.ts with new directory getters
941
+
942
+ 4. **Phase 4: Refactor loaders**
943
+ - Refactor hooks/loader.ts
944
+ - Refactor custom-tools/loader.ts
945
+ - Refactor skills.ts
946
+ - Refactor theme.ts
947
+ - Remove duplicate code
948
+
949
+ 5. **Phase 5: CLI updates**
950
+ - Add new flags to args.ts
951
+ - Update help text
952
+ - Add `pi install`, `pi remove`, `pi update` subcommands
953
+
954
+ 6. **Phase 6: Integration**
955
+ - Update sdk.ts
956
+ - Update interactive-mode.ts
957
+ - End-to-end testing
958
+
959
+ 7. **Phase 7: Documentation**
960
+ - Update README.md
961
+ - Update docs/hooks.md, docs/custom-tools.md
962
+ - Add examples for npm/git extensions
963
+
964
+ ## Testing Strategy
965
+
966
+ ### Unit Tests
967
+ - `test/extensions/scanner.test.ts`: Directory scanning, entry point resolution
968
+ - `test/extensions/filter.test.ts`: Pattern matching, include/exclude logic
969
+ - `test/extensions/source-resolver.test.ts`: npm/git specifier parsing
970
+
971
+ ### Integration Tests
972
+ - `test/extensions/npm-install.test.ts`: Full npm package installation flow
973
+ - `test/extensions/git-clone.test.ts`: Full git repository cloning flow
974
+ - `test/extensions/loading.test.ts`: End-to-end extension discovery and loading
975
+
976
+ ### Migration Tests
977
+ - `test/settings-migration.test.ts`: Old → new settings format conversion
978
+
979
+ ## File Summary
980
+
981
+ ### New Files (7)
982
+ - `src/core/extensions/types.ts`
983
+ - `src/core/extensions/source-resolver.ts`
984
+ - `src/core/extensions/scanner.ts`
985
+ - `src/core/extensions/filter.ts`
986
+ - `src/core/extensions/loader.ts`
987
+ - `src/core/extensions/index.ts`
988
+ - `docs/extension-loading.md` (this file)
989
+
990
+ ### Modified Files (9)
991
+ - `src/config.ts` - add directory getters
992
+ - `src/core/settings-manager.ts` - new types, migration
993
+ - `src/cli/args.ts` - new flags, update parsing
994
+ - `src/core/hooks/loader.ts` - refactor to use extensions
995
+ - `src/core/custom-tools/loader.ts` - refactor to use extensions
996
+ - `src/core/skills.ts` - refactor to use extensions
997
+ - `src/modes/interactive/theme/theme.ts` - refactor to use extensions
998
+ - `src/core/sdk.ts` - update option passing
999
+ - `src/modes/interactive/interactive-mode.ts` - update skill loading
1000
+
1001
+ ### Deleted Code (moved to extensions/)
1002
+ - Duplicate `expandPath()`, `normalizeUnicodeSpaces()` functions
1003
+ - Duplicate discovery/scanning logic
1004
+ - Duplicate path resolution logic