one-code-extension 0.1.4 → 0.3.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 (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +222 -177
  3. package/agents/explore.md +9 -6
  4. package/agents/general-purpose.md +1 -1
  5. package/agents/plan.md +6 -4
  6. package/docs/guide/README.md +52 -0
  7. package/docs/guide/bring-your-claude-code-setup.md +128 -0
  8. package/docs/guide/configuration.md +173 -0
  9. package/docs/guide/differences-from-claude-code.md +152 -0
  10. package/docs/guide/doctor.md +88 -0
  11. package/docs/guide/hooks.md +111 -0
  12. package/docs/guide/installation.md +126 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
  14. package/docs/guide/providers-and-models.md +196 -0
  15. package/docs/guide/reference.md +226 -0
  16. package/docs/guide/sessions-and-context.md +192 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +178 -0
  18. package/docs/guide/subagents-and-workflows.md +195 -0
  19. package/docs/guide/tasks-and-background-work.md +113 -0
  20. package/docs/guide/terminal-interface.md +157 -0
  21. package/docs/guide/tools.md +177 -0
  22. package/docs/guide/troubleshooting.md +177 -0
  23. package/extensions/ask-user/index.ts +65 -71
  24. package/extensions/ask-user/questions.ts +15 -28
  25. package/extensions/ask-user/widget.ts +533 -0
  26. package/extensions/auto-mode/actions.ts +9 -26
  27. package/extensions/auto-mode/classifier-prompt.ts +127 -0
  28. package/extensions/auto-mode/classifier.ts +305 -227
  29. package/extensions/auto-mode/config.ts +279 -77
  30. package/extensions/auto-mode/defaults.ts +17 -60
  31. package/extensions/auto-mode/instructions.ts +2 -1
  32. package/extensions/auto-mode/model-picker.ts +53 -31
  33. package/extensions/auto-mode/model-select.ts +153 -82
  34. package/extensions/auto-mode/paths.ts +13 -1
  35. package/extensions/auto-mode/pause.ts +8 -0
  36. package/extensions/auto-mode/prompt.ts +205 -238
  37. package/extensions/auto-mode/recoverability.ts +229 -0
  38. package/extensions/auto-mode/rules.ts +92 -75
  39. package/extensions/auto-mode/safety-floor.ts +43 -12
  40. package/extensions/auto-mode/sensitive.ts +11 -0
  41. package/extensions/auto-mode/setup-run.ts +245 -0
  42. package/extensions/auto-mode/setup.ts +235 -0
  43. package/extensions/auto-mode/shell-analysis.ts +332 -26
  44. package/extensions/auto-mode/transcript.ts +102 -0
  45. package/extensions/background/index.ts +256 -43
  46. package/extensions/background/monitor-batch.ts +58 -0
  47. package/extensions/background/registry.ts +9 -0
  48. package/extensions/background/wakeup.ts +45 -0
  49. package/extensions/bash/background.ts +47 -22
  50. package/extensions/bash/guards.ts +229 -0
  51. package/extensions/bash/index.ts +81 -19
  52. package/extensions/branding/index.ts +44 -18
  53. package/extensions/branding/startup.ts +23 -8
  54. package/extensions/claude-compat/index.ts +17 -2
  55. package/extensions/claude-context/index.ts +41 -12
  56. package/extensions/clear/index.ts +27 -0
  57. package/extensions/compaction/fit.ts +121 -0
  58. package/extensions/compaction/index.ts +176 -71
  59. package/extensions/compaction/prompt.ts +21 -1
  60. package/extensions/context-budget/budget.ts +61 -0
  61. package/extensions/context-budget/index.ts +66 -0
  62. package/extensions/context-management/index.ts +40 -16
  63. package/extensions/doctor/build.ts +226 -0
  64. package/extensions/doctor/cli.ts +99 -0
  65. package/extensions/doctor/compat.ts +532 -0
  66. package/extensions/doctor/dependencies.ts +186 -0
  67. package/extensions/doctor/fix-prompt.ts +125 -0
  68. package/extensions/doctor/index.ts +308 -0
  69. package/extensions/doctor/models.ts +231 -0
  70. package/extensions/doctor/presets.ts +208 -0
  71. package/extensions/doctor/report.ts +189 -0
  72. package/extensions/doctor/update-lookup.ts +46 -0
  73. package/extensions/doctor/viewer.ts +124 -0
  74. package/extensions/effort/index.ts +41 -4
  75. package/extensions/effort/slider.ts +33 -2
  76. package/extensions/file-tracker/index.ts +143 -20
  77. package/extensions/file-tracker/replay.ts +46 -0
  78. package/extensions/file-tracker/tracker.ts +51 -4
  79. package/extensions/footer/footer-line.ts +207 -0
  80. package/extensions/footer/index.ts +152 -0
  81. package/extensions/footer/pr.ts +53 -0
  82. package/extensions/hooks/executor.ts +57 -20
  83. package/extensions/hooks/formatter-notice.ts +76 -0
  84. package/extensions/hooks/index.ts +314 -73
  85. package/extensions/hooks/matcher.ts +35 -1
  86. package/extensions/hooks/plugin-hooks.ts +3 -3
  87. package/extensions/hooks/protocol.ts +58 -0
  88. package/extensions/hooks/subagent-bridge.ts +77 -0
  89. package/extensions/hooks/trust.ts +15 -4
  90. package/extensions/init/index.ts +26 -0
  91. package/extensions/init/prompt.ts +40 -0
  92. package/extensions/interrupted/index.ts +35 -0
  93. package/extensions/interrupted/line.ts +9 -0
  94. package/extensions/lib/abort.ts +15 -0
  95. package/extensions/lib/agent-loader.ts +183 -0
  96. package/extensions/lib/anthropic-payload.ts +15 -0
  97. package/extensions/lib/atomic-write.ts +56 -0
  98. package/extensions/lib/bridge-watch.ts +20 -0
  99. package/extensions/lib/cache-retention.ts +60 -0
  100. package/extensions/lib/capability-index.ts +466 -0
  101. package/extensions/lib/claude-context.ts +318 -27
  102. package/extensions/lib/claude-settings.ts +64 -0
  103. package/extensions/lib/consent-preview.ts +30 -0
  104. package/extensions/lib/deferred.ts +144 -13
  105. package/extensions/lib/favorites.ts +43 -0
  106. package/extensions/lib/fetch-timeout.ts +15 -0
  107. package/extensions/lib/frontmatter.ts +31 -0
  108. package/extensions/lib/git.ts +44 -2
  109. package/extensions/lib/hook-gate.ts +90 -0
  110. package/extensions/lib/interrupt.ts +94 -0
  111. package/extensions/lib/mcp-overrides.ts +61 -0
  112. package/extensions/lib/mcp-share.ts +25 -0
  113. package/extensions/lib/mcp-status.ts +34 -0
  114. package/extensions/lib/memory.ts +41 -6
  115. package/extensions/lib/model-facts.generated.json +2480 -0
  116. package/extensions/lib/model-facts.ts +162 -0
  117. package/extensions/lib/model-policy.ts +145 -189
  118. package/extensions/lib/model-tier.ts +480 -64
  119. package/extensions/lib/model-unusable.ts +37 -0
  120. package/extensions/lib/notifications.ts +320 -1
  121. package/extensions/lib/one-code-settings.ts +63 -0
  122. package/extensions/lib/original-command.ts +71 -0
  123. package/extensions/lib/package-version.ts +31 -0
  124. package/extensions/lib/paths.ts +39 -4
  125. package/extensions/lib/permission-gate.ts +192 -0
  126. package/extensions/lib/persisted-output.ts +18 -0
  127. package/extensions/lib/pi-ai-estimate.ts +160 -0
  128. package/extensions/lib/pi-version.ts +1 -1
  129. package/extensions/lib/plan-mode-channels.ts +11 -0
  130. package/extensions/lib/plugin-overrides.ts +24 -0
  131. package/extensions/lib/plugin-root.ts +44 -0
  132. package/extensions/lib/plugins.ts +233 -54
  133. package/extensions/lib/prefix-warm-gate.ts +157 -0
  134. package/extensions/lib/process-tree.ts +56 -0
  135. package/extensions/lib/reminders.ts +321 -68
  136. package/extensions/lib/scratchpad.ts +11 -8
  137. package/extensions/lib/session-lifecycle.ts +28 -0
  138. package/extensions/lib/settings-channels.ts +14 -0
  139. package/extensions/lib/shell-tasks.ts +54 -0
  140. package/extensions/lib/skill-overrides.ts +86 -0
  141. package/extensions/lib/skill-scan.ts +110 -0
  142. package/extensions/lib/text-width.ts +267 -0
  143. package/extensions/lib/tui-render.ts +281 -54
  144. package/extensions/lib/usage-bus.ts +75 -0
  145. package/extensions/lib/usage-tracker.ts +67 -0
  146. package/extensions/lib/worktree-isolation.ts +170 -0
  147. package/extensions/lsp/client.ts +69 -12
  148. package/extensions/lsp/format.ts +12 -2
  149. package/extensions/lsp/index.ts +273 -84
  150. package/extensions/lsp/install-hints.ts +43 -0
  151. package/extensions/lsp/keep-alive.ts +19 -0
  152. package/extensions/lsp/plugin-servers.ts +331 -0
  153. package/extensions/lsp/watcher.ts +157 -0
  154. package/extensions/mcp/announce.ts +99 -0
  155. package/extensions/mcp/client.ts +181 -22
  156. package/extensions/mcp/config.ts +55 -24
  157. package/extensions/mcp/index.ts +586 -86
  158. package/extensions/mcp/oauth/browser.ts +26 -0
  159. package/extensions/mcp/oauth/callback.ts +104 -0
  160. package/extensions/mcp/oauth/flow.ts +68 -0
  161. package/extensions/mcp/oauth/provider.ts +147 -0
  162. package/extensions/mcp/oauth/store.ts +86 -0
  163. package/extensions/mcp/panel/keys.ts +34 -0
  164. package/extensions/mcp/panel/model.ts +62 -0
  165. package/extensions/mcp/panel/render.ts +141 -0
  166. package/extensions/mcp/panel/state.ts +108 -0
  167. package/extensions/mcp/schema.ts +83 -6
  168. package/extensions/mcp/trust.ts +340 -0
  169. package/extensions/memory/entries.ts +163 -0
  170. package/extensions/memory/index.ts +119 -2
  171. package/extensions/memory/open-external.ts +97 -0
  172. package/extensions/memory/panel.ts +124 -0
  173. package/extensions/model-default/index.ts +74 -0
  174. package/extensions/notebook/index.ts +36 -18
  175. package/extensions/notebook/notebook.ts +110 -17
  176. package/extensions/permissions/index.ts +1012 -247
  177. package/extensions/permissions/matcher.ts +488 -57
  178. package/extensions/permissions/modes.ts +55 -7
  179. package/extensions/permissions/project-trust.ts +81 -0
  180. package/extensions/permissions/protected-paths.ts +16 -3
  181. package/extensions/permissions/rule-prose.ts +79 -0
  182. package/extensions/permissions/session-grant.ts +91 -0
  183. package/extensions/permissions/settings.ts +112 -33
  184. package/extensions/permissions/subagent-gate.ts +57 -0
  185. package/extensions/plan-mode/index.ts +143 -33
  186. package/extensions/plan-mode/reminder.ts +16 -19
  187. package/extensions/plan-mode/viewer.ts +40 -31
  188. package/extensions/plugins/counts.ts +68 -0
  189. package/extensions/plugins/index.ts +496 -32
  190. package/extensions/plugins/install/install.ts +167 -0
  191. package/extensions/plugins/install/paths.ts +23 -0
  192. package/extensions/plugins/install/registry.ts +72 -0
  193. package/extensions/plugins/marketplace/git.ts +103 -0
  194. package/extensions/plugins/marketplace/official.ts +36 -0
  195. package/extensions/plugins/marketplace/parse.ts +94 -0
  196. package/extensions/plugins/marketplace/registry.ts +60 -0
  197. package/extensions/plugins/marketplace/sync.ts +146 -0
  198. package/extensions/plugins/marketplace/types.ts +128 -0
  199. package/extensions/plugins/panel/keys.ts +52 -0
  200. package/extensions/plugins/panel/render.ts +315 -0
  201. package/extensions/plugins/panel/rows.ts +230 -0
  202. package/extensions/plugins/panel/state.ts +333 -0
  203. package/extensions/recap/index.ts +215 -0
  204. package/extensions/recap/prompt.ts +44 -0
  205. package/extensions/recap/scheduler.ts +87 -0
  206. package/extensions/search-tools/index.ts +0 -0
  207. package/extensions/search-tools/tools.ts +7 -5
  208. package/extensions/skill/index.ts +360 -34
  209. package/extensions/skill/invoke.ts +185 -0
  210. package/extensions/skill/listing.ts +21 -0
  211. package/extensions/skill/panel/keys.ts +41 -0
  212. package/extensions/skill/panel/render.ts +91 -0
  213. package/extensions/skill/panel/state.ts +115 -0
  214. package/extensions/spinner/index.ts +110 -0
  215. package/extensions/spinner/line.ts +51 -0
  216. package/extensions/spinner/verbs.ts +201 -0
  217. package/extensions/subagents/agents.ts +88 -44
  218. package/extensions/subagents/default-model.ts +62 -32
  219. package/extensions/subagents/delegation-steer.ts +23 -0
  220. package/extensions/subagents/hand-back-review.ts +67 -0
  221. package/extensions/subagents/index.ts +1545 -346
  222. package/extensions/subagents/live-runs.ts +293 -0
  223. package/extensions/subagents/model-select.ts +252 -82
  224. package/extensions/subagents/outcome.ts +90 -0
  225. package/extensions/subagents/panel-keys.ts +41 -0
  226. package/extensions/subagents/panel-render.ts +357 -0
  227. package/extensions/subagents/panel-widget.ts +330 -0
  228. package/extensions/subagents/pending-claim.ts +45 -0
  229. package/extensions/subagents/prose.ts +105 -0
  230. package/extensions/subagents/runner.ts +674 -0
  231. package/extensions/subagents/runs.ts +68 -1
  232. package/extensions/subagents/send-to-main-tool.ts +55 -0
  233. package/extensions/subagents/session-turns.ts +138 -0
  234. package/extensions/subagents/shell-panel.ts +198 -0
  235. package/extensions/subagents/usage.ts +16 -4
  236. package/extensions/subagents/worktree.ts +13 -0
  237. package/extensions/system-prompt/environment.ts +5 -3
  238. package/extensions/system-prompt/git-status.ts +93 -0
  239. package/extensions/system-prompt/index.ts +23 -1
  240. package/extensions/system-prompt/template.ts +34 -4
  241. package/extensions/system-prompt/tiers/common.ts +31 -6
  242. package/extensions/system-prompt/tiers/low.ts +13 -3
  243. package/extensions/system-prompt/tiers/mid.ts +21 -8
  244. package/extensions/system-reminder/index.ts +53 -9
  245. package/extensions/tasks/index.ts +65 -16
  246. package/extensions/tasks/store.ts +76 -3
  247. package/extensions/tool-search/announce.ts +42 -0
  248. package/extensions/tool-search/index.ts +136 -13
  249. package/extensions/turn-duration/index.ts +60 -0
  250. package/extensions/turn-duration/line.ts +22 -0
  251. package/extensions/turn-duration/span.ts +41 -0
  252. package/extensions/turn-duration/verbs.ts +24 -0
  253. package/extensions/web/backends.ts +421 -0
  254. package/extensions/web/index.ts +178 -21
  255. package/extensions/web-fetch/index.ts +74 -18
  256. package/extensions/web-fetch/summarize.ts +1 -1
  257. package/extensions/workflow/agent-session.ts +182 -112
  258. package/extensions/workflow/globals.ts +129 -106
  259. package/extensions/workflow/index.ts +343 -28
  260. package/extensions/workflow/records.ts +118 -0
  261. package/extensions/workflow/run-manager.ts +80 -31
  262. package/extensions/workflow/saved-workflows.ts +2 -1
  263. package/extensions/workflow/script-worker.mjs +110 -0
  264. package/extensions/workflow/types.ts +74 -8
  265. package/extensions/workflow/viewer.ts +562 -0
  266. package/extensions/workflow/vm-runtime.ts +118 -22
  267. package/extensions/workflow/widget.ts +126 -18
  268. package/extensions/worktree/guards.ts +240 -0
  269. package/extensions/worktree/index.ts +30 -5
  270. package/extensions/worktree/rewrite.ts +32 -16
  271. package/package.json +20 -10
  272. package/skills/code-review/SKILL.md +165 -0
  273. package/skills/fewer-permission-prompts/SKILL.md +61 -0
  274. package/skills/security-review/SKILL.md +178 -0
  275. package/skills/simplify/SKILL.md +70 -0
  276. package/themes/{one-code-light.json → onecode-light.json} +6 -1
  277. package/themes/{one-code.json → onecode.json} +6 -1
  278. package/types/app.d.ts +8 -1
  279. package/types/community-packages.d.ts +45 -2
  280. package/docs/decisions/auto-mode.md +0 -526
  281. package/docs/decisions/branding.md +0 -127
  282. package/docs/decisions/code-review-remediation.md +0 -106
  283. package/docs/decisions/compaction.md +0 -140
  284. package/docs/decisions/distribution.md +0 -168
  285. package/docs/decisions/hooks.md +0 -64
  286. package/docs/decisions/lsp.md +0 -58
  287. package/docs/decisions/mcp.md +0 -104
  288. package/docs/decisions/memory-state.md +0 -170
  289. package/docs/decisions/model-policy.md +0 -126
  290. package/docs/decisions/modes.md +0 -178
  291. package/docs/decisions/skills-plugins.md +0 -56
  292. package/docs/decisions/subagents-workflows.md +0 -225
  293. package/docs/decisions/system-prompt.md +0 -98
  294. package/docs/decisions/tools.md +0 -407
  295. package/docs/decisions/tui.md +0 -217
  296. package/docs/decisions.md +0 -148
  297. package/extensions/subagents/child.ts +0 -414
  298. package/extensions/subagents/rpc-turns.ts +0 -123
  299. package/extensions/todo/index.ts +0 -133
  300. package/extensions/workflow/permission-gate.ts +0 -71
@@ -11,11 +11,13 @@
11
11
  * invisibly, so the fallback is loud, never silent.
12
12
  */
13
13
 
14
- import type { Api, Model } from "@earendil-works/pi-ai";
14
+ import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai";
15
15
  import { completeSimple } from "@earendil-works/pi-ai/compat";
16
16
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
17
+ import { recordUsage } from "../lib/usage-bus.ts";
17
18
  import { Type } from "typebox";
18
19
  import { DEFER_CHANNEL } from "../lib/deferred.ts";
20
+ import { withReasoningFallback } from "../lib/model-policy.ts";
19
21
  import { htmlToMarkdown, isSameHost, normalizeUrl, paginate } from "./extract.ts";
20
22
  import { pickReaderModel, READER_MAX_TOKENS, readerMessages } from "./summarize.ts";
21
23
  import { ccToolRenderers } from "../lib/tui-render.ts";
@@ -23,6 +25,12 @@ import { ccToolRenderers } from "../lib/tui-render.ts";
23
25
  const DEFAULT_MAX_CHARS = 30_000;
24
26
  const CACHE_TTL_MS = 15 * 60 * 1000;
25
27
  const FETCH_TIMEOUT_MS = 30_000;
28
+ /** Same-host redirect hops followed before giving up (a redirect loop is otherwise unbounded recursion). */
29
+ const MAX_REDIRECTS = 5;
30
+ /** Response bytes read before the body is cut off — a multi-GB "page" must not be buffered whole. */
31
+ const MAX_BODY_BYTES = 5_000_000;
32
+ /** Cache entries kept; the oldest is evicted past this (the TTL alone never evicted). */
33
+ const MAX_CACHE_ENTRIES = 50;
26
34
  /** Longer than the classifier's cap: the reader ingests whole pages. */
27
35
  const READER_TIMEOUT_MS = 60_000;
28
36
  const USER_AGENT = "one-code/0.1 (+https://github.com/IsuruMaduranga/one-code)";
@@ -60,6 +68,11 @@ async function answerFromPage(
60
68
  entry: { markdown: string; title?: string },
61
69
  url: string,
62
70
  signal: AbortSignal | undefined,
71
+ /** Per-session memo of a reader model's required thinking level, so a reader that
72
+ * cannot disable thinking pays the mandatory-thinking 400 once, not per fetch. */
73
+ learnedReasoning: Map<string, ThinkingLevel>,
74
+ /** Report the reader call's usage for the all-in footer cost. */
75
+ recordCall: (usage: unknown) => void,
63
76
  ): Promise<{ answer?: string; reader?: string; truncated?: boolean; error?: string }> {
64
77
  const choice = pickReaderModel(ctx.modelRegistry.getAvailable(), ctx.model);
65
78
  if (!choice) return { error: "no model available to read the page" };
@@ -71,20 +84,31 @@ async function answerFromPage(
71
84
  const baseUrl = (auth as { baseUrl?: string }).baseUrl;
72
85
 
73
86
  const messages = readerMessages({ prompt, markdown: entry.markdown, url, title: entry.title });
74
- const timeout = AbortSignal.timeout(READER_TIMEOUT_MS);
75
- const result = await completeSimple(
76
- baseUrl ? ({ ...choice.model, baseUrl } as Model<Api>) : choice.model,
77
- {
78
- systemPrompt: messages.system,
79
- messages: [{ role: "user", content: messages.user, timestamp: Date.now() }],
80
- },
81
- {
82
- apiKey: auth.apiKey,
83
- headers: auth.headers,
84
- env: auth.env,
85
- signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
86
- maxTokens: READER_MAX_TOKENS,
87
+ // Thinking off unless the model cannot disable it; withReasoningFallback sends
88
+ // a level up front for catalog-marked models and retries on the 400 for the
89
+ // rest, memoizing the result so repeated fetches don't re-pay the failure.
90
+ const result = await withReasoningFallback(
91
+ choice.model,
92
+ (reasoning) => {
93
+ const timeout = AbortSignal.timeout(READER_TIMEOUT_MS);
94
+ return completeSimple(
95
+ baseUrl ? ({ ...choice.model, baseUrl } as Model<Api>) : choice.model,
96
+ {
97
+ systemPrompt: messages.system,
98
+ messages: [{ role: "user", content: messages.user, timestamp: Date.now() }],
99
+ },
100
+ {
101
+ apiKey: auth.apiKey,
102
+ headers: auth.headers,
103
+ env: auth.env,
104
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
105
+ maxTokens: READER_MAX_TOKENS,
106
+ ...(reasoning ? { reasoning } : {}),
107
+ },
108
+ );
87
109
  },
110
+ learnedReasoning,
111
+ recordCall,
88
112
  );
89
113
  const answer = result.content
90
114
  .filter((block): block is { type: "text"; text: string } => block.type === "text")
@@ -98,12 +122,35 @@ async function answerFromPage(
98
122
  }
99
123
  }
100
124
 
125
+ /** Read a response body as text, stopping after `cap` bytes (the stream is cancelled, not drained). */
126
+ async function readBodyCapped(response: Response, cap: number): Promise<{ text: string; truncated: boolean }> {
127
+ const reader = response.body?.getReader();
128
+ if (!reader) return { text: await response.text(), truncated: false };
129
+ const decoder = new TextDecoder();
130
+ let text = "";
131
+ let bytes = 0;
132
+ while (true) {
133
+ const { done, value } = await reader.read();
134
+ if (done) break;
135
+ bytes += value.byteLength;
136
+ if (bytes > cap) {
137
+ text += decoder.decode(value.subarray(0, Math.max(0, value.byteLength - (bytes - cap))), { stream: true });
138
+ await reader.cancel().catch(() => {});
139
+ return { text: text + decoder.decode(), truncated: true };
140
+ }
141
+ text += decoder.decode(value, { stream: true });
142
+ }
143
+ return { text: text + decoder.decode(), truncated: false };
144
+ }
145
+
101
146
  export default function webFetchExtension(pi: ExtensionAPI) {
102
147
  // Same 15-minute window Claude Code documents, so repeated reads of one page
103
148
  // during a task don't re-download it.
104
149
  const cache = new Map<string, CacheEntry>();
150
+ // Per-session: a reader model's learned required thinking level (see answerFromPage).
151
+ const learnedReasoning = new Map<string, ThinkingLevel>();
105
152
 
106
- const load = async (url: string, signal: AbortSignal | undefined): Promise<CacheEntry> => {
153
+ const load = async (url: string, signal: AbortSignal | undefined, redirects = 0): Promise<CacheEntry> => {
107
154
  const cached = cache.get(url);
108
155
  if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) return cached;
109
156
 
@@ -127,7 +174,8 @@ export default function webFetchExtension(pi: ExtensionAPI) {
127
174
  if (location) {
128
175
  const target = new URL(location, url).toString();
129
176
  if (isSameHost(target, url)) {
130
- return await load(target, signal);
177
+ if (redirects >= MAX_REDIRECTS) throw new Error(`Too many redirects (${MAX_REDIRECTS}); last target ${target}`);
178
+ return await load(target, signal, redirects + 1);
131
179
  }
132
180
  throw new Error(`Redirects to a different host: ${target}\nCall web_fetch again with that URL if you want it.`);
133
181
  }
@@ -138,7 +186,7 @@ export default function webFetchExtension(pi: ExtensionAPI) {
138
186
  }
139
187
 
140
188
  const contentType = response.headers.get("content-type") ?? "";
141
- const body = await response.text();
189
+ const { text: body, truncated } = await readBodyCapped(response, MAX_BODY_BYTES);
142
190
 
143
191
  let entry: CacheEntry;
144
192
  if (contentType.includes("html")) {
@@ -155,7 +203,13 @@ export default function webFetchExtension(pi: ExtensionAPI) {
155
203
  entry = { markdown: body.trim(), fetchedAt: Date.now() };
156
204
  }
157
205
 
206
+ if (truncated) entry.note = [entry.note, `The response was cut at ${MAX_BODY_BYTES / 1_000_000} MB.`].filter(Boolean).join(" ");
158
207
  cache.set(url, entry);
208
+ while (cache.size > MAX_CACHE_ENTRIES) {
209
+ const oldest = cache.keys().next().value;
210
+ if (oldest === undefined) break;
211
+ cache.delete(oldest);
212
+ }
159
213
  return entry;
160
214
  } finally {
161
215
  clearTimeout(timer);
@@ -206,7 +260,9 @@ export default function webFetchExtension(pi: ExtensionAPI) {
206
260
  // readerNote, never silently.
207
261
  let readerNote: string | undefined;
208
262
  if (params.prompt) {
209
- const answered = await answerFromPage(ctx, params.prompt, entry, target, signal);
263
+ const answered = await answerFromPage(ctx, params.prompt, entry, target, signal, learnedReasoning, (usage) =>
264
+ recordUsage(pi, "reader", usage),
265
+ );
210
266
  if (answered.answer !== undefined) {
211
267
  const header = [
212
268
  entry.title ? `# ${entry.title}` : undefined,
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { Api, Model } from "@earendil-works/pi-ai";
17
- import { type EconomicalModelChoice, pickEconomicalContainedModel } from "../lib/model-policy.ts";
17
+ import { type EconomicalModelChoice, pickEconomicalContainedModel } from "../lib/model-tier.ts";
18
18
 
19
19
  /**
20
20
  * How much of the page the reader sees. Far past the 30k-char window the main
@@ -1,34 +1,39 @@
1
1
  /**
2
2
  * In-process subagent execution — the one place the workflow extension
3
- * touches pi's SDK. Each agent() call becomes a real `createAgentSession()`
4
- * with an in-memory session; the run shares a single ModelRuntime and
5
- * DefaultResourceLoader across all its agents (building either per agent is
6
- * expensive and, for the loader, re-runs every extension factory).
3
+ * touches pi's SDK. Each agent() call becomes a real agent session with an
4
+ * in-memory session file; the run shares a single ModelRuntime across all its
5
+ * agents (building one per agent is expensive). The resource loader is NOT
6
+ * shared: disposing one session on a loader invalidates the loader's runtime
7
+ * for every other session built on it (lib/agent-loader.ts, LIFECYCLE-REVIEW
8
+ * H3), so each agent() call opens its own through `openChildSession`, which
9
+ * also emits the `session_start` pi's SDK never sends (H2).
7
10
  *
8
- * The shared loader uses `noExtensions: true`, which structurally blocks
9
- * recursive orchestration (no workflow tool inside subagents) — and would
10
- * also drop One Code's permission gate, so `permissionGateFactory` is passed
11
- * via `extensionFactories`, which DefaultResourceLoader always loads.
11
+ * Each loader uses `noExtensions: true`, which structurally blocks recursive
12
+ * orchestration (no workflow tool inside subagents) — and would also drop One
13
+ * Code's permission gate, so `permissionGateFactory` is passed via
14
+ * `extensionFactories`, which DefaultResourceLoader always loads.
12
15
  */
13
16
 
14
17
  import os from "node:os";
15
- import { join } from "node:path";
16
- import {
17
- createAgentSession,
18
- DefaultResourceLoader,
19
- getAgentDir,
20
- ModelRuntime,
21
- SessionManager,
22
- type ToolDefinition,
23
- } from "@earendil-works/pi-coding-agent";
18
+ import { getAgentDir, type ModelRuntime, SessionManager, type ToolDefinition } from "@earendil-works/pi-coding-agent";
24
19
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
25
20
  import type { Api, Model } from "@earendil-works/pi-ai";
26
21
  import { Type } from "typebox";
22
+ import { whenAborted } from "../lib/abort.ts";
23
+ import { createSharedModelRuntime, finalAssistantText, openChildSession } from "../lib/agent-loader.ts";
24
+ import { modelSpec as modelSpecOf } from "../lib/model-policy.ts";
25
+ import { isModelUnavailableError } from "../auto-mode/model-select.ts";
26
+ import { agentPromptIdentity, PrefixWarmGate, prefixWarmKey } from "../lib/prefix-warm-gate.ts";
27
+ import type { PermissionBridge } from "../permissions/subagent-gate.ts";
28
+ import { localGateMode, MODE_ENV } from "../lib/permission-gate.ts";
29
+ import type { HookBridge } from "../hooks/subagent-bridge.ts";
30
+ import { summarizeArgs } from "../lib/tui-render.ts";
27
31
  import { agentDirs, type AgentDefinition, discoverAgents } from "../subagents/agents.ts";
32
+ import type { SubagentDefault } from "../subagents/default-model.ts";
28
33
  import { expensiveModelGate, resolveSubagentModel, subagentModelMenu } from "../subagents/model-select.ts";
34
+ import { withoutUnusable } from "../lib/model-unusable.ts";
29
35
  import { cleanupWorktree, createWorktree, isGitRepo, type Worktree } from "../subagents/worktree.ts";
30
- import { permissionGateFactory } from "./permission-gate.ts";
31
- import type { AgentCallOptions, AgentCallResult, AgentEffort } from "./types.ts";
36
+ import type { AgentCallOptions, AgentCallResult, AgentEffort, AgentRunUpdate } from "./types.ts";
32
37
  import { WorkflowScriptError } from "./types.ts";
33
38
 
34
39
  const MAX_SCHEMA_RETRIES = 2;
@@ -38,11 +43,69 @@ export interface AgentRunnerOptions {
38
43
  cwd: string;
39
44
  /** Session model, retained as the final fallback. */
40
45
  defaultModel: unknown;
41
- /** `/subagent` user/managed default; automatic role selection follows it. */
42
- configuredDefaultModel?: string;
46
+ /** `/subagent` user/managed default (with its staleness stamp); automatic role selection follows it. */
47
+ configuredDefault?: SubagentDefault;
43
48
  defaultEffort?: AgentEffort | string;
44
49
  /** Surface model-resolution notices (fallbacks, provider crossings) in the run log. */
45
50
  onNotice?: (message: string) => void;
51
+ /** Models this account cannot run (`provider/id`), learned this session — dropped from the catalog before resolving (lib/model-unusable.ts). */
52
+ unusableModels?: () => ReadonlySet<string>;
53
+ /** An agent's provider refused its model as not usable on this account (`isModelUnavailableError`): report it so every picker skips it. */
54
+ onModelUnusable?: (model: string, reason: string) => void;
55
+ /**
56
+ * The parent permissions extension's decision closure (same bridge the
57
+ * subagent runner uses): when present, a workflow agent's tool calls route
58
+ * through the parent's real permission pipeline — mode inheritance, the
59
+ * auto-mode classifier, prompts bubbled to the user. Without it the child
60
+ * gate falls back to fail-closed local rules, which deny every bash call
61
+ * that would normally ask — unusable outside auto/bypass modes.
62
+ */
63
+ getPermissionBridge?: () => PermissionBridge | undefined;
64
+ /** The parent hooks extension's bridge, so the user's tool hooks run for workflow agents too. */
65
+ getHookBridge?: () => HookBridge | undefined;
66
+ /** Each finished agent's dollar cost (review S13: workflow agents never reached the footer). */
67
+ onUsage?: (cost: number) => void;
68
+ }
69
+
70
+ /**
71
+ * Judge one `agent()` prompt as a delegation before the child starts — the
72
+ * same `Agent` check the main conversation's spawns and a subagent's nested
73
+ * spawns go through (matcher DELEGATION_TOOLS; subagents/index.ts). The one
74
+ * `workflow` call was classified when the script was submitted, but every
75
+ * prompt is computed inside the vm afterwards, often from an earlier agent's
76
+ * output, and until 2026-09-05 none of them was judged
77
+ * (PERMISSIONS-REVIEW-2026-09-05 L4). Outside auto mode the bridge allows at
78
+ * once (Agent is auto-allowed there unless a deny rule names it). Without a
79
+ * bridge — no publishing parent — the run is allowed unless the live mode is
80
+ * auto, whose classifier is only reachable through the parent; then it fails
81
+ * closed. A refusal is thrown as a plain Error, i.e. an agent failure (the
82
+ * call resolves null, CC semantics), not a WorkflowScriptError: the script's
83
+ * other agents still run.
84
+ */
85
+ export async function judgeWorkflowDelegation(input: {
86
+ bridge: PermissionBridge | undefined;
87
+ liveMode: string;
88
+ prompt: string;
89
+ agentType?: string;
90
+ label?: string;
91
+ cwd: string;
92
+ signal?: AbortSignal;
93
+ }): Promise<void> {
94
+ if (!input.bridge) {
95
+ if (input.liveMode === "auto") {
96
+ throw new Error(
97
+ "agent() refused: auto mode's classifier is only reachable through the parent session, which this run has no link to",
98
+ );
99
+ }
100
+ return;
101
+ }
102
+ const verdict = await input.bridge({
103
+ toolName: "Agent",
104
+ input: { subagent_type: input.agentType, prompt: input.prompt, description: input.label },
105
+ cwd: input.cwd,
106
+ signal: input.signal,
107
+ });
108
+ if (verdict?.block) throw new Error(`agent() refused by the permission gate: ${verdict.reason}`);
46
109
  }
47
110
 
48
111
  /**
@@ -54,7 +117,7 @@ const THINKING_SUFFIX = /^(off|minimal|low|medium|high|xhigh|max)$/i;
54
117
  export interface WorkflowModelInput {
55
118
  opts: AgentCallOptions;
56
119
  agentModel?: string;
57
- configuredDefaultModel?: string;
120
+ configuredDefault?: SubagentDefault;
58
121
  sessionModel?: Model<Api>;
59
122
  available: Model<Api>[];
60
123
  defaultEffort?: string;
@@ -79,13 +142,13 @@ export function resolveWorkflowAgentModel(input: WorkflowModelInput): {
79
142
  const resolution = resolveSubagentModel({
80
143
  requested,
81
144
  agentModel: input.agentModel,
82
- configuredDefault: input.configuredDefaultModel,
145
+ configuredDefault: input.configuredDefault,
83
146
  sessionModel: input.sessionModel,
84
147
  available: input.available,
85
148
  });
86
149
  if (resolution.unresolved) {
87
150
  const fallback = resolveSubagentModel({
88
- configuredDefault: input.configuredDefaultModel,
151
+ configuredDefault: input.configuredDefault,
89
152
  sessionModel: input.sessionModel,
90
153
  available: input.available,
91
154
  });
@@ -115,43 +178,28 @@ export function resolveWorkflowAgentModel(input: WorkflowModelInput): {
115
178
  export class AgentRunner {
116
179
  private readonly options: AgentRunnerOptions;
117
180
  private readonly modelRuntime: ModelRuntime;
118
- private readonly loader: DefaultResourceLoader;
119
181
  private readonly agentCatalog: AgentDefinition[];
120
182
  private readonly availableModels: Model<Api>[];
121
- /** Per-agentType loader cache — building one re-runs every extension factory, so each
122
- * agentType's loader is built once (lazily, on first use) and reused for the rest of the run. */
123
- private readonly loadersByAgentType = new Map<string, Promise<DefaultResourceLoader>>();
183
+ /**
184
+ * `parallel()` children of one agent type share a request prefix; the first
185
+ * starts streaming before the rest go, so the fan-out writes the prefix once
186
+ * and reads it N-1 times (lib/prefix-warm-gate.ts).
187
+ */
188
+ private readonly warmGate = new PrefixWarmGate();
124
189
 
125
- private constructor(
126
- options: AgentRunnerOptions,
127
- modelRuntime: ModelRuntime,
128
- loader: DefaultResourceLoader,
129
- agentCatalog: AgentDefinition[],
130
- availableModels: Model<Api>[],
131
- ) {
190
+ private constructor(options: AgentRunnerOptions, modelRuntime: ModelRuntime, agentCatalog: AgentDefinition[], availableModels: Model<Api>[]) {
132
191
  this.options = options;
133
192
  this.modelRuntime = modelRuntime;
134
- this.loader = loader;
135
193
  this.agentCatalog = agentCatalog;
136
194
  this.availableModels = availableModels;
137
195
  }
138
196
 
139
197
  static async create(options: AgentRunnerOptions): Promise<AgentRunner> {
140
198
  const agentDir = getAgentDir();
141
- const modelRuntime = await ModelRuntime.create({
142
- authPath: join(agentDir, "auth.json"),
143
- modelsPath: join(agentDir, "models.json"),
144
- });
145
- const loader = new DefaultResourceLoader({
146
- cwd: options.cwd,
147
- agentDir,
148
- noExtensions: true,
149
- extensionFactories: [permissionGateFactory(options.cwd, os.homedir())],
150
- });
151
- await loader.reload();
199
+ const modelRuntime = await createSharedModelRuntime(agentDir);
152
200
  const agentCatalog = discoverAgents(agentDirs(options.cwd, os.homedir()));
153
201
  const availableModels = [...(await modelRuntime.getAvailable())];
154
- return new AgentRunner(options, modelRuntime, loader, agentCatalog, availableModels);
202
+ return new AgentRunner(options, modelRuntime, agentCatalog, availableModels);
155
203
  }
156
204
 
157
205
  /**
@@ -165,16 +213,21 @@ export class AgentRunner {
165
213
  const resolution = resolveWorkflowAgentModel({
166
214
  opts,
167
215
  agentModel,
168
- configuredDefaultModel: this.options.configuredDefaultModel,
216
+ configuredDefault: this.options.configuredDefault,
169
217
  sessionModel: this.options.defaultModel as Model<Api> | undefined,
170
- available: this.availableModels,
218
+ available: withoutUnusable(this.availableModels, this.options.unusableModels?.() ?? new Set()),
171
219
  defaultEffort: this.options.defaultEffort as string | undefined,
172
220
  });
173
221
  for (const notice of resolution.notices) this.options.onNotice?.(notice);
174
222
  return { model: resolution.model ?? this.options.defaultModel, thinkingLevel: resolution.thinkingLevel };
175
223
  }
176
224
 
177
- async run(prompt: string, opts: AgentCallOptions, signal: AbortSignal): Promise<AgentCallResult> {
225
+ async run(
226
+ prompt: string,
227
+ opts: AgentCallOptions,
228
+ signal: AbortSignal,
229
+ onUpdate?: (update: AgentRunUpdate) => void,
230
+ ): Promise<AgentCallResult> {
178
231
  let agentDef: AgentDefinition | undefined;
179
232
  if (opts.agentType) {
180
233
  agentDef = this.agentCatalog.find((a) => a.name === opts.agentType);
@@ -185,6 +238,10 @@ export class AgentRunner {
185
238
  }
186
239
 
187
240
  const modelSpec = this.resolveModel(opts, agentDef?.model);
241
+ const resolvedModel = modelSpec.model as Model<Api> | undefined;
242
+ if (resolvedModel?.id) onUpdate?.({ model: resolvedModel.id });
243
+
244
+ await this.judgeDelegation(prompt, opts, agentDef, signal);
188
245
 
189
246
  let worktree: Worktree | undefined;
190
247
  let cwd = this.options.cwd;
@@ -199,22 +256,58 @@ export class AgentRunner {
199
256
  const capture: { called: boolean; value: unknown } = { called: false, value: undefined };
200
257
  const customTools: ToolDefinition[] = opts.schema ? [buildStructuredOutputTool(opts.schema, capture)] : [];
201
258
 
202
- const loader = agentDef ? await this.loaderForAgentType(agentDef) : this.loader;
203
-
204
- const { session } = await createAgentSession({
205
- cwd,
206
- agentDir: getAgentDir(),
207
- modelRuntime: this.modelRuntime,
208
- model: modelSpec.model as never,
209
- thinkingLevel: modelSpec.thinkingLevel as never,
210
- tools: agentDef?.tools,
211
- customTools,
212
- resourceLoader: loader,
213
- sessionManager: SessionManager.inMemory(cwd),
259
+ const session = await openChildSession({
260
+ loader: {
261
+ cwd: this.options.cwd,
262
+ agentDir: getAgentDir(),
263
+ systemPrompt: agentDef?.systemPrompt,
264
+ getPermissionBridge: this.options.getPermissionBridge,
265
+ getHookBridge: this.options.getHookBridge,
266
+ },
267
+ session: {
268
+ cwd,
269
+ agentDir: getAgentDir(),
270
+ modelRuntime: this.modelRuntime,
271
+ model: modelSpec.model as never,
272
+ thinkingLevel: modelSpec.thinkingLevel as never,
273
+ tools: agentDef?.tools,
274
+ customTools,
275
+ sessionManager: SessionManager.inMemory(cwd),
276
+ },
277
+ onError: (error) => this.options.onNotice?.(`${opts.label ?? "agent"}: extension error in ${error.event}: ${error.error}`),
214
278
  });
215
279
 
216
- const onAbort = () => void session.abort();
217
- signal.addEventListener("abort", onAbort, { once: true });
280
+ const unhookAbort = whenAborted(signal, () => void session.abort());
281
+ // Forward tool calls for the viewer's Activity pane, and report a provider
282
+ // refusing the agent's model (the same check the subagent runner makes, so
283
+ // the refusal reaches every automatic picker — lib/model-unusable.ts).
284
+ // Never let a bad event shape kill the agent.
285
+ const unsubscribe = session.subscribe((event) => {
286
+ try {
287
+ if (event.type === "message_end") {
288
+ const reply = (event as { message?: { role?: string; stopReason?: string; errorMessage?: string; provider?: string; model?: string } }).message;
289
+ if (reply?.role === "assistant" && reply.stopReason === "error" && reply.errorMessage && reply.provider && reply.model && isModelUnavailableError(reply.errorMessage)) {
290
+ this.options.onModelUnusable?.(modelSpecOf({ provider: reply.provider, id: reply.model }), reply.errorMessage);
291
+ }
292
+ return;
293
+ }
294
+ if (event.type !== "tool_execution_start" || !onUpdate) return;
295
+ try {
296
+ const argsSummary = summarizeArgs((event as { args?: unknown }).args);
297
+ onUpdate({ tool: { name: event.toolName, argsSummary } });
298
+ } catch {
299
+ onUpdate({ tool: { name: event.toolName } });
300
+ }
301
+ } catch {
302
+ // observer only
303
+ }
304
+ });
305
+ // Same prompt identity + cwd + model = same request prefix (see warmGate).
306
+ const model = session.model;
307
+ const releasePrefix = await this.warmGate.admitOnFirstToken(
308
+ prefixWarmKey(agentPromptIdentity(agentDef?.name), cwd, model ? modelSpecOf(model) : undefined),
309
+ session,
310
+ );
218
311
  try {
219
312
  await session.prompt(this.buildPrompt(prompt, Boolean(opts.schema)));
220
313
  if (signal.aborted) throw new WorkflowScriptError("aborted");
@@ -245,12 +338,31 @@ export class AgentRunner {
245
338
  worktreePath,
246
339
  };
247
340
  } finally {
248
- signal.removeEventListener("abort", onAbort);
341
+ // Whatever the outcome — success, failure, or an abort (a user stop, or
342
+ // the run finishing with this agent un-awaited) — the provider was
343
+ // billed for what ran, so the footer's all-in cost sees it.
344
+ const cost = session.getSessionStats().cost;
345
+ if (cost > 0) this.options.onUsage?.(cost);
346
+ releasePrefix(false);
347
+ unsubscribe?.();
348
+ unhookAbort();
249
349
  session.dispose();
250
350
  if (worktree) await cleanupWorktree(this.options.cwd, worktree);
251
351
  }
252
352
  }
253
353
 
354
+ private judgeDelegation(prompt: string, opts: AgentCallOptions, agentDef: AgentDefinition | undefined, signal: AbortSignal): Promise<void> {
355
+ return judgeWorkflowDelegation({
356
+ bridge: this.options.getPermissionBridge?.(),
357
+ liveMode: localGateMode(process.env[MODE_ENV], undefined),
358
+ prompt,
359
+ agentType: agentDef?.name,
360
+ label: opts.label,
361
+ cwd: this.options.cwd,
362
+ signal,
363
+ });
364
+ }
365
+
254
366
  private buildPrompt(prompt: string, structured: boolean): string {
255
367
  if (!structured) {
256
368
  return `${prompt}\n\nYour final message is returned verbatim to an orchestration script, not shown to a human — reply with the requested data/report only.`;
@@ -275,35 +387,10 @@ export class AgentRunner {
275
387
  throw new Error("subagent never produced structured output");
276
388
  }
277
389
 
278
- /** Builds (once) and reuses the loader for this agentType, keyed by agent name. */
279
- private loaderForAgentType(agentDef: AgentDefinition): Promise<DefaultResourceLoader> {
280
- let pending = this.loadersByAgentType.get(agentDef.name);
281
- if (!pending) {
282
- pending = this.buildLoaderWithSystemPrompt(agentDef.systemPrompt);
283
- // A failed build must not poison the cache — let the next call retry.
284
- pending.catch(() => this.loadersByAgentType.delete(agentDef.name));
285
- this.loadersByAgentType.set(agentDef.name, pending);
286
- }
287
- return pending;
288
- }
289
-
290
- private async buildLoaderWithSystemPrompt(systemPrompt: string): Promise<DefaultResourceLoader> {
291
- const loader = new DefaultResourceLoader({
292
- cwd: this.options.cwd,
293
- agentDir: getAgentDir(),
294
- noExtensions: true,
295
- extensionFactories: [permissionGateFactory(this.options.cwd, os.homedir())],
296
- systemPrompt,
297
- });
298
- await loader.reload();
299
- return loader;
300
- }
301
-
302
390
  dispose(): void {
303
- // ModelRuntime/loader(s) hold no OS resources that need explicit teardown today;
304
- // this hook exists so run-manager can stay correct if that changes. The
305
- // per-agentType cache is dropped alongside the base loader.
306
- this.loadersByAgentType.clear();
391
+ // The ModelRuntime holds no OS resources that need explicit teardown today
392
+ // (each agent()'s loader dies with its session); this hook exists so
393
+ // run-manager can stay correct if that changes.
307
394
  }
308
395
  }
309
396
 
@@ -329,25 +416,8 @@ function buildStructuredOutputTool(schema: Record<string, unknown>, capture: { c
329
416
  } as ToolDefinition;
330
417
  }
331
418
 
332
- export function finalAssistantText(messages: AgentMessage[]): string {
333
- for (let i = messages.length - 1; i >= 0; i--) {
334
- const message = messages[i];
335
- if (message.role !== "assistant") continue;
336
- const content = (message as { content: unknown }).content;
337
- if (typeof content === "string") return content;
338
- if (Array.isArray(content)) {
339
- const text = content
340
- .filter((b): b is { type: "text"; text: string } => (b as { type?: string }).type === "text")
341
- .map((b) => b.text)
342
- .join("");
343
- if (text.trim()) return text;
344
- }
345
- }
346
- return "";
347
- }
348
-
349
419
  /** Pull the first parseable JSON object/array out of free text (```json fences first). */
350
- export function extractJsonObject(text: string): unknown {
420
+ function extractJsonObject(text: string): unknown {
351
421
  const fenced = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
352
422
  const braceIndex = text.indexOf("{");
353
423
  const bracketIndex = text.indexOf("[");