@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7

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 (244) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
  3. package/dist/cli.js +3342 -3274
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +39 -2
  7. package/dist/types/cursor.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  9. package/dist/types/extensibility/shared-events.d.ts +18 -1
  10. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  11. package/dist/types/internal-urls/parse.d.ts +12 -0
  12. package/dist/types/internal-urls/router.d.ts +6 -0
  13. package/dist/types/internal-urls/types.d.ts +6 -0
  14. package/dist/types/lsp/config.d.ts +1 -0
  15. package/dist/types/lsp/types.d.ts +2 -0
  16. package/dist/types/mcp/manager.d.ts +5 -0
  17. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  20. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  21. package/dist/types/sdk.d.ts +3 -1
  22. package/dist/types/session/agent-session-types.d.ts +8 -6
  23. package/dist/types/session/agent-session.d.ts +28 -1
  24. package/dist/types/session/model-controls.d.ts +4 -1
  25. package/dist/types/session/session-advisors.d.ts +7 -1
  26. package/dist/types/session/session-tools.d.ts +44 -6
  27. package/dist/types/session/streaming-output.d.ts +7 -2
  28. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  29. package/dist/types/session/turn-recovery.d.ts +5 -3
  30. package/dist/types/task/index.d.ts +1 -1
  31. package/dist/types/task/types.d.ts +3 -11
  32. package/dist/types/thinking.d.ts +21 -2
  33. package/dist/types/tools/index.d.ts +8 -3
  34. package/dist/types/tools/output-meta.d.ts +5 -0
  35. package/dist/types/tools/read.d.ts +9 -1
  36. package/dist/types/tools/xdev.d.ts +53 -67
  37. package/dist/types/utils/cpuprofile.d.ts +51 -0
  38. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  39. package/dist/types/utils/profile-tree.d.ts +47 -0
  40. package/dist/types/utils/sample-profile.d.ts +67 -0
  41. package/dist/types/utils/title-generator.d.ts +17 -16
  42. package/package.json +12 -12
  43. package/src/capability/index.ts +43 -12
  44. package/src/capability/mcp.ts +21 -0
  45. package/src/capability/types.ts +20 -1
  46. package/src/cli/read-cli.ts +44 -2
  47. package/src/config/settings-schema.ts +42 -2
  48. package/src/config/settings.ts +35 -0
  49. package/src/cursor.ts +4 -3
  50. package/src/discovery/builtin-rules/index.ts +2 -0
  51. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  52. package/src/eval/py/runner.py +16 -2
  53. package/src/extensibility/extensions/runner.ts +117 -5
  54. package/src/extensibility/extensions/types.ts +0 -1
  55. package/src/extensibility/extensions/wrapper.ts +74 -42
  56. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  57. package/src/extensibility/hooks/types.ts +0 -1
  58. package/src/extensibility/shared-events.ts +18 -1
  59. package/src/internal-urls/mcp-protocol.ts +17 -3
  60. package/src/internal-urls/parse.ts +31 -0
  61. package/src/internal-urls/router.ts +24 -4
  62. package/src/internal-urls/types.ts +6 -0
  63. package/src/live/transport.ts +2 -2
  64. package/src/lsp/client.ts +2 -2
  65. package/src/lsp/config.ts +4 -0
  66. package/src/lsp/types.ts +2 -0
  67. package/src/mcp/config.ts +26 -14
  68. package/src/mcp/manager.ts +26 -9
  69. package/src/mcp/tool-bridge.ts +52 -1
  70. package/src/memories/index.ts +25 -6
  71. package/src/modes/components/custom-editor.ts +39 -16
  72. package/src/modes/components/status-line/segments.ts +3 -1
  73. package/src/modes/components/tips.txt +2 -1
  74. package/src/modes/components/tool-execution.ts +8 -7
  75. package/src/modes/controllers/command-controller.ts +10 -10
  76. package/src/modes/controllers/extension-ui-controller.ts +7 -7
  77. package/src/modes/controllers/selector-controller.ts +7 -2
  78. package/src/modes/rpc/rpc-mode.ts +60 -47
  79. package/src/prompts/steering/user-interjection.md +2 -5
  80. package/src/prompts/tools/task.md +4 -2
  81. package/src/sdk.ts +64 -58
  82. package/src/session/agent-session-types.ts +8 -5
  83. package/src/session/agent-session.ts +144 -11
  84. package/src/session/model-controls.ts +48 -12
  85. package/src/session/session-advisors.ts +30 -14
  86. package/src/session/session-listing.ts +66 -4
  87. package/src/session/session-tools.ts +162 -54
  88. package/src/session/streaming-output.ts +18 -6
  89. package/src/session/tool-choice-queue.ts +19 -4
  90. package/src/session/turn-recovery.ts +25 -6
  91. package/src/slash-commands/builtin-registry.ts +69 -0
  92. package/src/task/executor.ts +11 -3
  93. package/src/task/index.ts +43 -32
  94. package/src/task/types.ts +12 -17
  95. package/src/thinking.ts +68 -5
  96. package/src/tools/bash.ts +16 -9
  97. package/src/tools/index.ts +36 -16
  98. package/src/tools/output-meta.ts +20 -0
  99. package/src/tools/read.ts +89 -15
  100. package/src/tools/write.ts +16 -7
  101. package/src/tools/xdev.ts +198 -210
  102. package/src/utils/cpuprofile.ts +235 -0
  103. package/src/utils/inspect-image-mode.ts +39 -0
  104. package/src/utils/profile-tree.ts +111 -0
  105. package/src/utils/sample-profile.ts +437 -0
  106. package/src/utils/title-generator.ts +88 -34
  107. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  108. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  109. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  110. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  111. package/dist/types/cli/update-cli.test.d.ts +0 -1
  112. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  113. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  114. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  115. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  116. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  117. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  118. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  119. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  120. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  121. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  122. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  123. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  124. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  125. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  126. package/dist/types/hindsight/client.test.d.ts +0 -1
  127. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  128. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  129. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  130. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  131. package/dist/types/launch/protocol.test.d.ts +0 -1
  132. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  133. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  134. package/dist/types/live/protocol.test.d.ts +0 -1
  135. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  136. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  137. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  138. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  139. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  140. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  141. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  142. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  143. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  144. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  145. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  146. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  147. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  148. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  149. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  150. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  151. package/dist/types/modes/print-mode.test.d.ts +0 -1
  152. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  153. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  154. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  155. package/dist/types/modes/warp-events.test.d.ts +0 -1
  156. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  157. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  158. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  159. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  160. package/dist/types/session/blob-store.test.d.ts +0 -1
  161. package/dist/types/session/messages.test.d.ts +0 -1
  162. package/dist/types/session/session-context.test.d.ts +0 -1
  163. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  164. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  165. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  166. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  167. package/dist/types/system-prompt.test.d.ts +0 -1
  168. package/dist/types/task/render.test.d.ts +0 -1
  169. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  170. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  171. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  172. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  173. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  174. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  175. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  176. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  177. package/src/advisor/__tests__/config.test.ts +0 -349
  178. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  179. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  180. package/src/cli/update-cli.test.ts +0 -28
  181. package/src/config/__tests__/model-registry.test.ts +0 -182
  182. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  183. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  184. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  185. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  186. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  187. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  188. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  189. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  190. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  191. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  192. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  193. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  194. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  195. package/src/hindsight/client.test.ts +0 -75
  196. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  197. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  198. package/src/launch/broker-list-order.test.ts +0 -89
  199. package/src/launch/broker-output-snapshot.test.ts +0 -126
  200. package/src/launch/protocol.test.ts +0 -59
  201. package/src/launch/spawn-options.test.ts +0 -31
  202. package/src/launch/terminal-output.test.ts +0 -107
  203. package/src/live/protocol.test.ts +0 -140
  204. package/src/mcp/config-writer.test.ts +0 -43
  205. package/src/mcp/smithery-auth.test.ts +0 -29
  206. package/src/mcp/smithery-registry.test.ts +0 -51
  207. package/src/mcp/transports/stdio.test.ts +0 -427
  208. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  209. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  210. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  211. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  212. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  213. package/src/modes/components/custom-editor.test.ts +0 -510
  214. package/src/modes/components/login-dialog.test.ts +0 -56
  215. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  216. package/src/modes/components/status-line/component.test.ts +0 -84
  217. package/src/modes/components/tool-execution.test.ts +0 -162
  218. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  219. package/src/modes/noninteractive-dispose.test.ts +0 -73
  220. package/src/modes/print-mode.test.ts +0 -71
  221. package/src/modes/session-teardown.test.ts +0 -219
  222. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  223. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  224. package/src/modes/warp-events.test.ts +0 -794
  225. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  226. package/src/plan-mode/model-transition.test.ts +0 -60
  227. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  228. package/src/session/agent-session-error-log.test.ts +0 -59
  229. package/src/session/blob-store.test.ts +0 -56
  230. package/src/session/messages.test.ts +0 -282
  231. package/src/session/session-context.test.ts +0 -384
  232. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  233. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  234. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  235. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  236. package/src/system-prompt.test.ts +0 -236
  237. package/src/task/render.test.ts +0 -290
  238. package/src/task/spawn-policy.test.ts +0 -62
  239. package/src/tools/__tests__/eval-description.test.ts +0 -18
  240. package/src/tools/__tests__/glob.test.ts +0 -37
  241. package/src/tools/__tests__/json-tree.test.ts +0 -35
  242. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  243. package/src/tools/hub/launch-compat.test.ts +0 -40
  244. package/src/vibe/__tests__/token-rate.test.ts +0 -96
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "17.1.5",
4
+ "version": "17.1.7",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -52,17 +52,17 @@
52
52
  "@agentclientprotocol/sdk": "1.2.1",
53
53
  "@babel/parser": "^7.29.7",
54
54
  "@mozilla/readability": "^0.6.0",
55
- "@oh-my-pi/hashline": "17.1.5",
56
- "@oh-my-pi/omp-stats": "17.1.5",
57
- "@oh-my-pi/pi-agent-core": "17.1.5",
58
- "@oh-my-pi/pi-ai": "17.1.5",
59
- "@oh-my-pi/pi-catalog": "17.1.5",
60
- "@oh-my-pi/pi-mnemopi": "17.1.5",
61
- "@oh-my-pi/pi-natives": "17.1.5",
62
- "@oh-my-pi/pi-tui": "17.1.5",
63
- "@oh-my-pi/pi-utils": "17.1.5",
64
- "@oh-my-pi/pi-wire": "17.1.5",
65
- "@oh-my-pi/snapcompact": "17.1.5",
55
+ "@oh-my-pi/hashline": "17.1.7",
56
+ "@oh-my-pi/omp-stats": "17.1.7",
57
+ "@oh-my-pi/pi-agent-core": "17.1.7",
58
+ "@oh-my-pi/pi-ai": "17.1.7",
59
+ "@oh-my-pi/pi-catalog": "17.1.7",
60
+ "@oh-my-pi/pi-mnemopi": "17.1.7",
61
+ "@oh-my-pi/pi-natives": "17.1.7",
62
+ "@oh-my-pi/pi-tui": "17.1.7",
63
+ "@oh-my-pi/pi-utils": "17.1.7",
64
+ "@oh-my-pi/pi-wire": "17.1.7",
65
+ "@oh-my-pi/snapcompact": "17.1.7",
66
66
  "@opentelemetry/api": "^1.9.1",
67
67
  "@opentelemetry/api-logs": "^0.220.0",
68
68
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -102,9 +102,10 @@ async function loadImpl<T>(
102
102
  capability: Capability<T>,
103
103
  providers: Provider<T>[],
104
104
  ctx: LoadContext,
105
- options: LoadOptions,
105
+ options: LoadOptions<T>,
106
106
  ): Promise<CapabilityResult<T>> {
107
107
  const allItems: Array<T & { _source: SourceMeta; _shadowed?: boolean }> = [];
108
+ const suppressedItems = new Set<T & { _source: SourceMeta; _shadowed?: boolean }>();
108
109
  const allWarnings: string[] = [];
109
110
  const contributingProviders: string[] = [];
110
111
  const disabledExtensionIds = options.includeDisabled
@@ -154,6 +155,21 @@ async function loadImpl<T>(
154
155
  continue;
155
156
  }
156
157
 
158
+ if (options.filter && !options.filter(itemWithSource)) {
159
+ continue;
160
+ }
161
+
162
+ if (options.suppress?.(itemWithSource)) {
163
+ // Suppressed items still claim their dedupe key below, so a
164
+ // suppressed higher-priority item shadows same-key lower-priority
165
+ // ones, but they never survive or equivalence-shadow survivors.
166
+ itemWithSource._source.providerName = provider.displayName;
167
+ const suppressed = itemWithSource as T & { _source: SourceMeta; _shadowed?: boolean };
168
+ suppressedItems.add(suppressed);
169
+ allItems.push(suppressed);
170
+ continue;
171
+ }
172
+
157
173
  itemWithSource._source.providerName = provider.displayName;
158
174
  allItems.push(itemWithSource as T & { _source: SourceMeta; _shadowed?: boolean });
159
175
  contributedItemCount += 1;
@@ -164,21 +180,33 @@ async function loadImpl<T>(
164
180
  }
165
181
  }
166
182
 
167
- // Deduplicate by key (first wins = highest priority)
168
- const seen = new Map<string, number>();
183
+ // Deduplicate by key or semantic equivalence (first wins = highest priority)
184
+ const seen = new Set<string>();
169
185
  const deduped: Array<T & { _source: SourceMeta }> = [];
186
+ const equivalent = capability.equivalent;
170
187
 
171
- for (let i = 0; i < allItems.length; i++) {
172
- const item = allItems[i];
188
+ for (const item of allItems) {
173
189
  const key = capability.key(item);
174
190
 
191
+ if (suppressedItems.has(item)) {
192
+ // Claim key ownership (same-name precedence, including disabled
193
+ // state) without surviving or equivalence-shadowing survivors.
194
+ if (key !== undefined) seen.add(key);
195
+ continue;
196
+ }
197
+
175
198
  if (key === undefined) {
176
199
  deduped.push(item);
177
- } else if (!seen.has(key)) {
178
- seen.set(key, i);
179
- deduped.push(item);
180
- } else {
200
+ continue;
201
+ }
202
+
203
+ const keySeen = seen.has(key);
204
+ seen.add(key);
205
+ const aliasSeen = !keySeen && equivalent !== undefined && deduped.some(existing => equivalent(existing, item));
206
+ if (keySeen || aliasSeen) {
181
207
  item._shadowed = true;
208
+ } else {
209
+ deduped.push(item);
182
210
  }
183
211
  }
184
212
 
@@ -198,7 +226,7 @@ async function loadImpl<T>(
198
226
 
199
227
  return {
200
228
  items: deduped,
201
- all: allItems,
229
+ all: suppressedItems.size > 0 ? allItems.filter(item => !suppressedItems.has(item)) : allItems,
202
230
  warnings: allWarnings,
203
231
  providers: contributingProviders,
204
232
  };
@@ -207,7 +235,7 @@ async function loadImpl<T>(
207
235
  /**
208
236
  * Filter providers based on options and disabled state.
209
237
  */
210
- function filterProviders<T>(capability: Capability<T>, options: LoadOptions): Provider<T>[] {
238
+ function filterProviders<T>(capability: Capability<T>, options: LoadOptions<T>): Provider<T>[] {
211
239
  let providers = (capability.providers as Provider<T>[]).filter(p => !disabledProviders.has(p.id));
212
240
 
213
241
  if (options.providers) {
@@ -225,7 +253,10 @@ function filterProviders<T>(capability: Capability<T>, options: LoadOptions): Pr
225
253
  /**
226
254
  * Load a capability by ID.
227
255
  */
228
- export async function loadCapability<T>(capabilityId: string, options: LoadOptions = {}): Promise<CapabilityResult<T>> {
256
+ export async function loadCapability<T>(
257
+ capabilityId: string,
258
+ options: LoadOptions<T> = {},
259
+ ): Promise<CapabilityResult<T>> {
229
260
  const capability = capabilities.get(capabilityId) as Capability<T> | undefined;
230
261
  if (!capability) {
231
262
  throw new Error(`Unknown capability: "${capabilityId}"`);
@@ -53,11 +53,32 @@ export interface MCPServer {
53
53
  _source: SourceMeta;
54
54
  }
55
55
 
56
+ /** Compare the transport inputs that determine which MCP endpoint gets connected. */
57
+ function isSameMCPConnection(left: MCPServer, right: MCPServer): boolean {
58
+ if (!Bun.deepEquals(left.auth, right.auth) || !Bun.deepEquals(left.oauth, right.oauth)) return false;
59
+
60
+ const leftTransport = left.transport ?? (left.command ? "stdio" : left.url ? "http" : "stdio");
61
+ const rightTransport = right.transport ?? (right.command ? "stdio" : right.url ? "http" : "stdio");
62
+ if (leftTransport !== rightTransport) return false;
63
+
64
+ if (leftTransport === "stdio") {
65
+ return (
66
+ left.command === right.command &&
67
+ Bun.deepEquals(left.args, right.args) &&
68
+ Bun.deepEquals(left.env, right.env) &&
69
+ left.cwd === right.cwd
70
+ );
71
+ }
72
+
73
+ return left.url === right.url && Bun.deepEquals(left.headers, right.headers);
74
+ }
75
+
56
76
  export const mcpCapability = defineCapability<MCPServer>({
57
77
  id: "mcps",
58
78
  displayName: "MCP Servers",
59
79
  description: "Model Context Protocol server configurations for external tool integrations",
60
80
  key: server => server.name,
81
+ equivalent: isSameMCPConnection,
61
82
  toExtensionId: server => `mcp:${server.name}`,
62
83
  validate: server => {
63
84
  if (!server.name) return "Missing server name";
@@ -59,7 +59,7 @@ export interface Provider<T> {
59
59
  /**
60
60
  * Options for loading a capability.
61
61
  */
62
- export interface LoadOptions {
62
+ export interface LoadOptions<T = unknown> {
63
63
  /** Only use these providers (by ID). Default: all registered */
64
64
  providers?: string[];
65
65
  /** Exclude these providers (by ID). Default: none */
@@ -72,6 +72,22 @@ export interface LoadOptions {
72
72
  includeDisabled?: boolean;
73
73
  /** Explicit disabled extension IDs to apply instead of settings. */
74
74
  disabledExtensions?: string[];
75
+ /**
76
+ * Drop items before deduplication as if they never existed (e.g. scope
77
+ * exclusions). A dropped item neither survives nor claims its dedupe key,
78
+ * so it cannot shadow anything. Receives the item with its attached
79
+ * `_source`.
80
+ */
81
+ filter?(item: T & { _source: SourceMeta }): boolean;
82
+ /**
83
+ * Exclude items from the results while letting them claim their dedupe key.
84
+ * A suppressed higher-priority item still shadows same-key lower-priority
85
+ * items (a disabled project server keeps the same-named user server off),
86
+ * but never equivalence-shadows a differently-keyed survivor — so a
87
+ * suppressed alias cannot starve an enabled equivalent connection.
88
+ * Return `true` to suppress. Receives the item with its attached `_source`.
89
+ */
90
+ suppress?(item: T & { _source: SourceMeta }): boolean;
75
91
  }
76
92
 
77
93
  /**
@@ -122,6 +138,9 @@ export interface Capability<T> {
122
138
  */
123
139
  key(item: T): string | undefined;
124
140
 
141
+ /** Treat items with different keys as aliases; the first equivalent item wins. */
142
+ equivalent?(left: T, right: T): boolean;
143
+
125
144
  /**
126
145
  * Optional validation. Return error message if invalid, undefined if valid.
127
146
  */
@@ -8,6 +8,12 @@
8
8
  import { getProjectDir } from "@oh-my-pi/pi-utils";
9
9
  import chalk from "chalk";
10
10
  import { Settings } from "../config/settings";
11
+ import { extractUriScheme } from "../internal-urls/parse";
12
+ import { InternalUrlRouter } from "../internal-urls/router";
13
+ import { discoverAndLoadMCPTools } from "../mcp/loader";
14
+ import { MCPManager } from "../mcp/manager";
15
+ import { discoverAuthStorage } from "../session/auth-broker-config";
16
+ import type { AuthStorage } from "../session/auth-storage";
11
17
  import type { ToolSession } from "../tools";
12
18
  import { wrapToolWithMetaNotice } from "../tools/output-meta";
13
19
  import { ReadTool } from "../tools/read";
@@ -17,6 +23,17 @@ export interface ReadCommandArgs {
17
23
  path: string;
18
24
  }
19
25
 
26
+ function shouldDiscoverMcp(path: string): boolean {
27
+ // MCP resource URIs may be hierarchical (`test://notes`) or opaque
28
+ // (`urn:example:document`); `extractUriScheme` recognizes both while
29
+ // rejecting Windows drive paths and selector-shaped filesystem inputs.
30
+ const scheme = extractUriScheme(path);
31
+ if (!scheme) return false;
32
+ if (scheme === "mcp") return true;
33
+ if (["conflict", "file", "http", "https"].includes(scheme)) return false;
34
+ return InternalUrlRouter.instance().getHandler(scheme) === undefined;
35
+ }
36
+
20
37
  export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
21
38
  if (!cmd.path) {
22
39
  process.stderr.write(chalk.red("error: path is required\n"));
@@ -34,9 +51,26 @@ export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
34
51
  getSessionSpawns: () => "*",
35
52
  };
36
53
 
37
- const tool = wrapToolWithMetaNotice(new ReadTool(session));
54
+ let authStorage: AuthStorage | undefined;
55
+ let mcpManager: MCPManager | undefined;
56
+ let failed = false;
38
57
 
39
58
  try {
59
+ if (shouldDiscoverMcp(cmd.path)) {
60
+ authStorage = await discoverAuthStorage();
61
+ const result = await discoverAndLoadMCPTools(cwd, {
62
+ enableProjectConfig: settings.get("mcp.enableProjectConfig") ?? true,
63
+ filterExa: true,
64
+ filterBrowser: settings.get("browser.enabled") ?? false,
65
+ cacheStorage: settings.getStorage(),
66
+ authStorage,
67
+ });
68
+ mcpManager = result.manager;
69
+ session.mcpManager = mcpManager;
70
+ MCPManager.setInstance(mcpManager);
71
+ }
72
+
73
+ const tool = wrapToolWithMetaNotice(new ReadTool(session));
40
74
  const result = await tool.execute("omp-read", { path: cmd.path });
41
75
 
42
76
  for (const block of result.content) {
@@ -52,6 +86,14 @@ export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
52
86
  }
53
87
  } catch (err) {
54
88
  process.stderr.write(`${chalk.red(renderError(err))}\n`);
55
- process.exit(1);
89
+ failed = true;
90
+ } finally {
91
+ if (mcpManager) {
92
+ await mcpManager.disconnectAll();
93
+ if (MCPManager.instance() === mcpManager) MCPManager.setInstance(undefined);
94
+ }
95
+ authStorage?.close();
56
96
  }
97
+
98
+ if (failed) process.exit(1);
57
99
  }
@@ -935,7 +935,7 @@ export const SETTINGS_SCHEMA = {
935
935
  group: "Display",
936
936
  label: "Terminal Title Run State",
937
937
  description:
938
- "Show the agent run state in the terminal title's separator — an animated spinner while working, '>' when it's your turn, '!' when the agent is waiting on you",
938
+ "Show the agent run state in the terminal title's separator — an animated spinner while working (a static ':' on Windows), '>' when it's your turn, '!' when the agent is waiting on you",
939
939
  },
940
940
  },
941
941
 
@@ -3835,14 +3835,28 @@ export const SETTINGS_SCHEMA = {
3835
3835
  },
3836
3836
  },
3837
3837
 
3838
+ // Legacy boolean kept only for back-compat migration to `inspect_image.mode`
3839
+ // (see config/settings.ts). Hidden from UI.
3838
3840
  "inspect_image.enabled": {
3839
3841
  type: "boolean",
3840
3842
  default: false,
3843
+ },
3844
+
3845
+ "inspect_image.mode": {
3846
+ type: "enum",
3847
+ values: ["auto", "on", "off"] as const,
3848
+ default: "auto",
3841
3849
  ui: {
3842
3850
  tab: "tools",
3843
3851
  group: "Available Tools",
3844
3852
  label: "Inspect Image",
3845
- description: "Enable the inspect_image tool, delegating image understanding to a vision-capable model",
3853
+ description:
3854
+ "Controls the inspect_image tool, which delegates image understanding to a vision-capable model. 'auto' exposes it only when the active model lacks native image input; 'on' always exposes it; 'off' never does.",
3855
+ options: [
3856
+ { value: "auto", label: "Auto (only for models without vision)" },
3857
+ { value: "on", label: "On" },
3858
+ { value: "off", label: "Off" },
3859
+ ],
3846
3860
  },
3847
3861
  },
3848
3862
 
@@ -4476,6 +4490,18 @@ export const SETTINGS_SCHEMA = {
4476
4490
  },
4477
4491
  },
4478
4492
 
4493
+ "task.enableEffort": {
4494
+ type: "boolean",
4495
+ default: false,
4496
+ ui: {
4497
+ tab: "tasks",
4498
+ group: "Subagents",
4499
+ label: "Per-Task Effort",
4500
+ description:
4501
+ "Expose the optional effort parameter on task spawns, allowing callers to override each subagent's thinking level",
4502
+ },
4503
+ },
4504
+
4479
4505
  "task.maxConcurrency": {
4480
4506
  type: "number",
4481
4507
  default: 32,
@@ -4588,6 +4614,20 @@ export const SETTINGS_SCHEMA = {
4588
4614
  },
4589
4615
  },
4590
4616
 
4617
+ "task.maxEffort": {
4618
+ type: "enum",
4619
+ values: THINKING_EFFORTS,
4620
+ default: "max",
4621
+ ui: {
4622
+ tab: "tasks",
4623
+ group: "Subagents",
4624
+ label: "Maximum Per-Spawn Effort",
4625
+ description:
4626
+ "Maximum reasoning effort allowed for the task tool's per-spawn effort hint. Lower values prevent callers from escalating subagents above this ceiling; the default preserves the model's full range.",
4627
+ options: THINKING_EFFORTS.map(getThinkingLevelMetadata),
4628
+ },
4629
+ },
4630
+
4591
4631
  "task.disabledAgents": {
4592
4632
  type: "array",
4593
4633
  default: [] as string[],
@@ -39,6 +39,7 @@ import { isLightTheme, setAutoThemeMapping, setColorBlindMode, setSymbolPreset }
39
39
  import { AgentStorage } from "../session/agent-storage";
40
40
  import { AUTO_IMAGE_PROVIDER_ORDER, isImageProviderId } from "../tools/image-providers";
41
41
  import { type EditMode, normalizeEditMode } from "../utils/edit-mode";
42
+ import { INSPECT_IMAGE_MODES } from "../utils/inspect-image-mode";
42
43
  import { isSearchProviderId, SEARCH_PROVIDER_ORDER } from "../web/search/types";
43
44
  import { withFileLock } from "./file-lock";
44
45
  import {
@@ -1366,6 +1367,40 @@ export class Settings {
1366
1367
  }
1367
1368
  }
1368
1369
 
1370
+ // inspect_image.enabled (boolean) -> inspect_image.mode (enum). Explicit
1371
+ // user choices are preserved: true -> "on", false -> "off". Configs with
1372
+ // no legacy key get the new "auto" default, which hides the tool for
1373
+ // models with native image input. Handles nested and quoted-dotted
1374
+ // ("inspect_image.enabled") sources; the target is always the nested
1375
+ // form, which is the only shape the resolver reads.
1376
+ const inspectImageObj = isRecord(raw.inspect_image) ? (raw.inspect_image as Record<string, unknown>) : undefined;
1377
+ const legacyEnabled =
1378
+ typeof inspectImageObj?.enabled === "boolean"
1379
+ ? inspectImageObj.enabled
1380
+ : typeof raw["inspect_image.enabled"] === "boolean"
1381
+ ? (raw["inspect_image.enabled"] as boolean)
1382
+ : undefined;
1383
+ if (legacyEnabled !== undefined) {
1384
+ if (!inspectImageObj) {
1385
+ raw.inspect_image = {};
1386
+ }
1387
+ const target = raw.inspect_image as Record<string, unknown>;
1388
+ const flatMode = raw["inspect_image.mode"];
1389
+ if (target.mode === undefined) {
1390
+ // A quoted-dotted explicit mode wins over the legacy boolean but
1391
+ // must be normalized into the nested form the resolver reads.
1392
+ target.mode =
1393
+ typeof flatMode === "string" && (INSPECT_IMAGE_MODES as readonly string[]).includes(flatMode)
1394
+ ? flatMode
1395
+ : legacyEnabled
1396
+ ? "on"
1397
+ : "off";
1398
+ }
1399
+ delete target.enabled;
1400
+ delete raw["inspect_image.enabled"];
1401
+ delete raw["inspect_image.mode"];
1402
+ }
1403
+
1369
1404
  // task.isolation.enabled (boolean) -> task.isolation.mode (enum)
1370
1405
  const taskObj = raw.task as Record<string, unknown> | undefined;
1371
1406
  const isolationObj = taskObj?.isolation as Record<string, unknown> | undefined;
package/src/cursor.ts CHANGED
@@ -25,7 +25,8 @@ interface CursorExecBridgeOptions {
25
25
  cwd: string;
26
26
  getCwd?: () => string;
27
27
  tools: Map<string, AgentTool>;
28
- getTool?: (name: string) => AgentTool | undefined;
28
+ /** Resolves execution overrides (mounted-device permission wrappers) before the canonical map. */
29
+ getExecutableTool?: (name: string) => AgentTool | undefined;
29
30
  getToolContext?: () => AgentToolContext | undefined;
30
31
  emitEvent?: (event: AgentEvent) => void;
31
32
  /**
@@ -81,7 +82,7 @@ async function executeTool(
81
82
  toolCallId: string,
82
83
  args: Record<string, unknown>,
83
84
  ): Promise<ToolResultMessage> {
84
- const tool = options.tools.get(toolName) ?? options.getTool?.(toolName);
85
+ const tool = options.getExecutableTool?.(toolName) ?? options.tools.get(toolName);
85
86
  if (!tool) {
86
87
  const result = buildToolErrorResult(`Tool "${toolName}" not available`);
87
88
  return createToolResultMessage(toolCallId, toolName, result, true);
@@ -497,7 +498,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
497
498
  async mcp(call: CursorMcpCall) {
498
499
  const toolName = call.toolName || call.name;
499
500
  const toolCallId = decodeToolCallId(call.toolCallId);
500
- const tool = this.options.tools.get(toolName) ?? this.options.getTool?.(toolName);
501
+ const tool = this.options.getExecutableTool?.(toolName) ?? this.options.tools.get(toolName);
501
502
  if (!tool) {
502
503
  const availableTools = Array.from(this.options.tools.keys()).filter(name => name.startsWith("mcp__"));
503
504
  const message = formatMcpToolErrorMessage(toolName, availableTools);
@@ -28,6 +28,7 @@ import tsNoAny from "./ts-no-any.md" with { type: "text" };
28
28
  import tsNoDeprecatedLeftovers from "./ts-no-deprecated-leftovers.md" with { type: "text" };
29
29
  import tsNoDynamicImport from "./ts-no-dynamic-import.md" with { type: "text" };
30
30
  import tsNoInlineCastAccess from "./ts-no-inline-cast-access.md" with { type: "text" };
31
+ import tsNoLocalIsRecord from "./ts-no-local-is-record.md" with { type: "text" };
31
32
  import tsNoReturnType from "./ts-no-return-type.md" with { type: "text" };
32
33
  import tsNoTestTimers from "./ts-no-test-timers.md" with { type: "text" };
33
34
  import tsNoTinyFunctions from "./ts-no-tiny-functions.md" with { type: "text" };
@@ -63,6 +64,7 @@ export const BUILTIN_RULE_SOURCES: readonly BuiltinRuleSource[] = [
63
64
  { name: "ts-no-deprecated-leftovers", content: tsNoDeprecatedLeftovers },
64
65
  { name: "ts-no-dynamic-import", content: tsNoDynamicImport },
65
66
  { name: "ts-no-inline-cast-access", content: tsNoInlineCastAccess },
67
+ { name: "ts-no-local-is-record", content: tsNoLocalIsRecord },
66
68
  { name: "ts-no-return-type", content: tsNoReturnType },
67
69
  { name: "ts-no-test-timers", content: tsNoTestTimers },
68
70
  { name: "ts-no-tiny-functions", content: tsNoTinyFunctions },
@@ -0,0 +1,48 @@
1
+ ---
2
+ description: "Never use isRecord"
3
+ condition:
4
+ - "\\bfunction\\s+isRecord(?:\\s*<[^>]*>)?\\s*\\("
5
+ - "\\b(?:const|let|var)\\s+isRecord\\b\\s*(?::[\\s\\S]{0,300}?)?=\\s*(?:async\\s+)?(?:function\\b|(?:<[^>\\n]*>\\s*)?(?:\\([^)]*\\)|[A-Za-z_$][\\w$]*)\\s*(?::[\\s\\S]{0,300}?)?=>)"
6
+ scope: "tool:edit(*.{ts,tsx,mts,cts}), tool:write(*.{ts,tsx,mts,cts})"
7
+ interruptMode: never
8
+ ---
9
+
10
+ ## Why it's wrong
11
+
12
+ - A `Record<string, unknown>` guard proves only an object, not its fields.
13
+ - It's either unnecessarily complicated, or not strong enough.
14
+ - Repeated guards hide the actual data contract from readers and TypeScript.
15
+
16
+ ## Use
17
+
18
+ `isRecord` narrows values to `Record<string, unknown>`; each field remains `unknown`.
19
+
20
+ For network, config, IPC, persisted, or reused data shapes, parse once at the boundary with the project's schema validator and consume its named output type:
21
+
22
+ ```typescript
23
+ const Config = z.object({ retries: z.number().int().nonnegative() });
24
+ type Config = z.infer<typeof Config>;
25
+
26
+ const config = Config.parse(raw);
27
+ ```
28
+
29
+ If the runtime shape is uncertain, check the properties you use with `typeof`, `Array.isArray`, `in`, or a discriminant. If an existing invariant guarantees the shape, assert the named type at that boundary instead of duplicating a guard:
30
+
31
+ ```typescript
32
+ const config = value as Config;
33
+ ```
34
+
35
+ ## Avoid
36
+
37
+ ```typescript
38
+ function isRecord(value: unknown): value is Record<string, unknown> {
39
+ return !!value && typeof value === "object" && !Array.isArray(value);
40
+ }
41
+
42
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
43
+ value !== null && typeof value === "object";
44
+ ```
45
+
46
+ ## Exceptions
47
+
48
+ A standalone package without a shared type-guard module may define its single canonical guard. Export it from the package's type-guard module; never recreate it at individual call sites.
@@ -1057,7 +1057,7 @@ async def _run_compiled_async(code, ns: dict, *, want_value: bool) -> Any:
1057
1057
 
1058
1058
 
1059
1059
  def _compile_source(source: str) -> tuple[Any, Any | None, bool]:
1060
- module = ast.parse(source, mode="exec")
1060
+ module = ast.parse(source, "<cell>", "exec")
1061
1061
  if not module.body:
1062
1062
  return None, None, False
1063
1063
 
@@ -1279,7 +1279,21 @@ async def _handle_request_async(req: dict) -> None:
1279
1279
 
1280
1280
 
1281
1281
  def _emit_error(rid: str, exc: BaseException) -> None:
1282
- tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
1282
+ if isinstance(exc, SyntaxError) and exc.filename == "<cell>":
1283
+ # Syntax error in the cell source itself: every stack frame is runner
1284
+ # machinery, so emit only the caret display, like a REPL.
1285
+ tb_lines = traceback.format_exception_only(type(exc), exc)
1286
+ else:
1287
+ # Drop the leading runner-internal frames (_handle_request_async ->
1288
+ # _exec_source_async -> _run_compiled_*) so tracebacks start at user
1289
+ # code. If the exception never reached user code it is a runner bug;
1290
+ # keep the full traceback because those frames are the diagnosis.
1291
+ tb = exc.__traceback__
1292
+ while tb is not None and tb.tb_frame.f_code.co_filename == __file__:
1293
+ tb = tb.tb_next
1294
+ tb_lines = traceback.format_exception(
1295
+ type(exc), exc, tb if tb is not None else exc.__traceback__
1296
+ )
1283
1297
  _emit(
1284
1298
  {
1285
1299
  "type": "error",