one-code-extension 0.1.3 → 0.3.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 (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +200 -172
  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 +126 -0
  8. package/docs/guide/configuration.md +171 -0
  9. package/docs/guide/differences-from-claude-code.md +150 -0
  10. package/docs/guide/doctor.md +86 -0
  11. package/docs/guide/hooks.md +109 -0
  12. package/docs/guide/installation.md +124 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
  14. package/docs/guide/providers-and-models.md +194 -0
  15. package/docs/guide/reference.md +224 -0
  16. package/docs/guide/sessions-and-context.md +190 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +176 -0
  18. package/docs/guide/subagents-and-workflows.md +193 -0
  19. package/docs/guide/tasks-and-background-work.md +111 -0
  20. package/docs/guide/terminal-interface.md +155 -0
  21. package/docs/guide/tools.md +175 -0
  22. package/docs/guide/troubleshooting.md +175 -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 -202
  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
@@ -12,7 +12,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import { Type } from "typebox";
13
13
  import { DEFER_CHANNEL } from "../lib/deferred.ts";
14
14
  import { ccToolRenderers } from "../lib/tui-render.ts";
15
- import { applyEdit, type EditMode, parseNotebook } from "./notebook.ts";
15
+ import { applyEdit, type EditMode, type Notebook, notebookErrorHint, parseNotebook } from "./notebook.ts";
16
16
 
17
17
  export default function notebookExtension(pi: ExtensionAPI) {
18
18
  pi.registerTool({
@@ -20,52 +20,70 @@ export default function notebookExtension(pi: ExtensionAPI) {
20
20
  label: "Notebook Edit",
21
21
  ...ccToolRenderers("Notebook Edit"),
22
22
  description:
23
- "Edit a Jupyter notebook (.ipynb): replace a cell's source, insert a new cell, or delete a cell. Cells are addressed by their `id`; read the notebook first to get ids. Editing a code cell clears its outputs.",
23
+ "Edit a Jupyter notebook (.ipynb): replace a cell's source, insert or append a new cell, or delete a cell. Read the notebook first — like the other file tools, this one refuses to edit a file you have not read. Cells are addressed by their `id` or, for a notebook whose cells have none, by position — `cell-0` is the first cell. Use edit_mode `append` to add a cell at the end without naming one. Editing a code cell clears its outputs.",
24
24
  parameters: Type.Object({
25
- path: Type.String({ description: "Absolute or workspace-relative path to the .ipynb file" }),
26
- edit_mode: StringEnum(["replace", "insert", "delete"] as const),
25
+ path: Type.Optional(Type.String({ description: "Absolute or workspace-relative path to the .ipynb file" })),
26
+ // Claude Code's NotebookEdit spells the path `notebook_path` and defaults
27
+ // edit_mode to `replace`; a CC-trained model's `{notebook_path, cell_id,
28
+ // new_source}` used to fail schema validation twice over
29
+ // (TOOL-FIDELITY-REVIEW-2026-09-07 M2). Accept both.
30
+ notebook_path: Type.Optional(Type.String({ description: "Alias of `path` (Claude Code's name for it)." })),
31
+ edit_mode: Type.Optional(StringEnum(["replace", "insert", "append", "delete"] as const, { description: "The type of edit to make. Defaults to replace." })),
27
32
  cell_id: Type.Optional(
28
33
  Type.String({
29
34
  description:
30
- "Target cell id. Required for replace and delete; for insert the new cell goes after it (omit to insert at the top).",
35
+ "Target cell id, either the cell's own id or its position as `cell-0`, `cell-1`, … Required for replace and delete; for insert the new cell goes after it (omit to insert at the top); ignored for append.",
31
36
  }),
32
37
  ),
33
38
  new_source: Type.Optional(Type.String({ description: "New cell source. Required unless deleting." })),
34
39
  cell_type: Type.Optional(
35
40
  StringEnum(["code", "markdown"] as const, {
36
- description: "Cell type. Required when inserting; when replacing, changes the cell's type.",
41
+ description: "Cell type. Required when inserting or appending; when replacing, changes the cell's type.",
37
42
  }),
38
43
  ),
39
44
  }),
40
45
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
41
- const path = isAbsolute(params.path) ? params.path : resolve(ctx.cwd, params.path);
46
+ const rawPath = params.path ?? params.notebook_path;
47
+ if (!rawPath) {
48
+ return {
49
+ content: [{ type: "text", text: "`path` (or Claude Code's `notebook_path`) is required: the .ipynb file to edit." }],
50
+ details: {},
51
+ isError: true,
52
+ };
53
+ }
54
+ const editMode = (params.edit_mode ?? "replace") as EditMode;
55
+ const path = isAbsolute(rawPath) ? rawPath : resolve(ctx.cwd, rawPath);
56
+ // Held outside the try so a failure raised AFTER the parse can name the
57
+ // ids the notebook actually has — a bare "cell_id is required" left both
58
+ // weak tiers patching ids in by hand with bash (review M4).
59
+ let parsed: Notebook | undefined;
42
60
  try {
43
- const notebook = parseNotebook(readFileSync(path, "utf-8"));
61
+ const notebook = (parsed = parseNotebook(readFileSync(path, "utf-8")));
44
62
  const { notebook: updated, summary } = applyEdit(
45
63
  notebook,
46
64
  {
47
65
  cellId: params.cell_id,
48
66
  newSource: params.new_source,
49
67
  cellType: params.cell_type as "code" | "markdown" | undefined,
50
- editMode: params.edit_mode as EditMode,
68
+ editMode,
51
69
  },
52
70
  () => randomUUID().slice(0, 8),
53
71
  );
54
72
  writeFileSync(path, `${JSON.stringify(updated, null, 1)}\n`);
55
73
  return {
56
- content: [{ type: "text", text: `${summary} in ${params.path} (${updated.cells.length} cells).` }],
74
+ content: [{ type: "text", text: `${summary} in ${rawPath} (${updated.cells.length} cells).` }],
57
75
  details: { path, cellCount: updated.cells.length },
58
76
  };
59
77
  } catch (error) {
60
- const code = (error as NodeJS.ErrnoException).code;
61
- const hint =
62
- code === "ENOENT"
63
- ? " — no such file; check the path points to an existing .ipynb"
64
- : error instanceof SyntaxError
65
- ? " — the file is not valid notebook JSON"
66
- : "";
78
+ const message = (error as Error).message;
79
+ const hint = notebookErrorHint({
80
+ code: (error as NodeJS.ErrnoException).code,
81
+ syntax: error instanceof SyntaxError,
82
+ message,
83
+ parsed,
84
+ });
67
85
  return {
68
- content: [{ type: "text", text: `Notebook edit failed: ${(error as Error).message}${hint}` }],
86
+ content: [{ type: "text", text: `Notebook edit failed: ${message}${hint}` }],
69
87
  details: {},
70
88
  isError: true,
71
89
  };
@@ -1,5 +1,15 @@
1
1
  /**
2
2
  * Jupyter notebook editing (pure) — Claude Code's NotebookEdit semantics.
3
+ *
4
+ * Cells are addressed by `id`, but nbformat only made `id` mandatory in 4.5:
5
+ * a 4.4-era notebook has none, and a model handed one had no id to pass and no
6
+ * way to append. Both a cheap-tier and a tiny-tier model fell back to bash plus
7
+ * `nbformat`/`json` to patch ids in — the exact route the prompt had told them
8
+ * not to take — 6/6 runs (WEAK-MODEL-REVIEW-2026-09-06 M4). So cells are also
9
+ * addressable by POSITION, as Claude Code's Read renders them: `cell-0`,
10
+ * `cell-1`, … Positional ids are resolved against the notebook as it is when
11
+ * the call arrives, so they shift after an insert or a delete — which is why
12
+ * every failure enumerates the notebook's current cells.
3
13
  */
4
14
 
5
15
  export interface NotebookCell {
@@ -16,7 +26,7 @@ export interface Notebook {
16
26
  [key: string]: unknown;
17
27
  }
18
28
 
19
- export type EditMode = "replace" | "insert" | "delete";
29
+ export type EditMode = "replace" | "insert" | "append" | "delete";
20
30
 
21
31
  export interface EditRequest {
22
32
  cellId?: string;
@@ -37,12 +47,69 @@ export function toSourceLines(source: string): string[] {
37
47
  return lines.map((line, i) => (i === lines.length - 1 ? line : `${line}\n`)).filter((l, i) => l !== "" || i === 0);
38
48
  }
39
49
 
50
+ /** The positional id of a cell, Claude Code's spelling. */
51
+ export function positionalId(index: number): string {
52
+ return `cell-${index}`;
53
+ }
54
+
55
+ /** How a cell should be named back to the model: its own id, else its position. */
56
+ export function cellLabel(cell: NotebookCell, index: number): string {
57
+ return cell.id ?? positionalId(index);
58
+ }
59
+
60
+ /** `aaa (markdown), cell-1 (code)` — every id the notebook currently answers to. */
61
+ export function describeCells(notebook: Notebook): string {
62
+ if (notebook.cells.length === 0) return "(none — the notebook has no cells)";
63
+ return notebook.cells.map((cell, index) => `${cellLabel(cell, index)} (${cell.cell_type})`).join(", ");
64
+ }
65
+
66
+ /**
67
+ * Trailing hint appended to a notebook_edit failure message. A missing file or
68
+ * malformed JSON gets a targeted pointer; a cell-addressing failure (a missing or
69
+ * wrong `cell_id`) gets the list of ids the notebook actually has, so a weak tier
70
+ * picks a real one instead of inventing it by hand (review M4). A missing
71
+ * `new_source`/`cell_type` is not about a cell, so it gets no id listing — that
72
+ * would misdirect (code-review F3). A real "No cell with id" already lists them,
73
+ * so it is left untouched.
74
+ */
75
+ export function notebookErrorHint(opts: { code?: string; syntax: boolean; message: string; parsed: Notebook | undefined }): string {
76
+ if (opts.code === "ENOENT") return " — no such file; check the path points to an existing .ipynb";
77
+ if (opts.syntax) return " — the file is not valid notebook JSON";
78
+ if (opts.parsed && opts.message.includes("cell_id") && !opts.message.includes("cells are:")) {
79
+ return ` — this notebook's cells are: ${describeCells(opts.parsed)}. Address a cell by its own id or by position (cell-0 is the first); edit_mode "append" adds one at the end.`;
80
+ }
81
+ return "";
82
+ }
83
+
84
+ /** Index of a cell by its own id, or by its `cell-N` position. -1 when neither matches. */
40
85
  export function findCellIndex(notebook: Notebook, cellId: string): number {
41
- return notebook.cells.findIndex((cell) => cell.id === cellId);
86
+ const byId = notebook.cells.findIndex((cell) => cell.id === cellId);
87
+ if (byId !== -1) return byId;
88
+ const positional = /^cell-(\d+)$/.exec(cellId);
89
+ if (!positional) return -1;
90
+ const index = Number.parseInt(positional[1], 10);
91
+ return index < notebook.cells.length ? index : -1;
42
92
  }
43
93
 
44
- function newCell(cellType: "code" | "markdown", source: string, id: string): NotebookCell {
45
- const cell: NotebookCell = { cell_type: cellType, id, metadata: {}, source: toSourceLines(source) };
94
+ /**
95
+ * The "fail loud, name the fix" error for an id that resolves to nothing
96
+ * (docs/decisions/tools.md): which ids exist, and the two ways to add a cell
97
+ * without naming one.
98
+ */
99
+ export function noSuchCellError(notebook: Notebook, cellId: string): Error {
100
+ return new Error(
101
+ `No cell with id "${cellId}". This notebook's cells are: ${describeCells(notebook)}. ` +
102
+ "Cells can be addressed by their own id or by position (cell-0 is the first). " +
103
+ 'To add a cell without naming one, use edit_mode "append" (at the end) or omit cell_id with edit_mode "insert" (at the top).',
104
+ );
105
+ }
106
+
107
+ function newCell(cellType: "code" | "markdown", source: string, id: string | undefined): NotebookCell {
108
+ const cell: NotebookCell = { cell_type: cellType, metadata: {}, source: toSourceLines(source) };
109
+ // A 4.4 notebook has no legal `id` field, so a new cell must not carry one
110
+ // either — stamping one would leave a single id-bearing cell among id-less
111
+ // ones, which is exactly the shape withRepairedIds refuses to create.
112
+ if (id !== undefined) cell.id = id;
46
113
  if (cellType === "code") {
47
114
  cell.outputs = [];
48
115
  cell.execution_count = null;
@@ -50,6 +117,23 @@ function newCell(cellType: "code" | "markdown", source: string, id: string): Not
50
117
  return cell;
51
118
  }
52
119
 
120
+ /**
121
+ * nbformat 4.5 REQUIRES every cell to carry an id, so a 4.5+ notebook missing
122
+ * them is malformed and filling them in on the way out is a repair, not a
123
+ * rewrite. A 4.4 notebook is left alone: `id` is not a legal cell field there,
124
+ * and its cells stay addressable by position.
125
+ */
126
+ function supportsIds(notebook: Notebook): boolean {
127
+ const minor = typeof notebook.nbformat_minor === "number" ? notebook.nbformat_minor : 0;
128
+ const major = typeof notebook.nbformat === "number" ? notebook.nbformat : 4;
129
+ return major > 4 || (major === 4 && minor >= 5);
130
+ }
131
+
132
+ function withRepairedIds(notebook: Notebook, cells: NotebookCell[], makeId: () => string): NotebookCell[] {
133
+ if (!supportsIds(notebook)) return cells;
134
+ return cells.map((cell) => (cell.id ? cell : { ...cell, id: makeId() }));
135
+ }
136
+
53
137
  export interface EditResult {
54
138
  notebook: Notebook;
55
139
  summary: string;
@@ -61,35 +145,44 @@ export interface EditResult {
61
145
  */
62
146
  export function applyEdit(notebook: Notebook, request: EditRequest, makeId: () => string): EditResult {
63
147
  const cells = [...notebook.cells];
148
+ const finish = (updated: NotebookCell[], summary: string): EditResult => ({
149
+ notebook: { ...notebook, cells: withRepairedIds(notebook, updated, makeId) },
150
+ summary,
151
+ });
64
152
 
65
153
  if (request.editMode === "delete") {
66
154
  if (!request.cellId) throw new Error("cell_id is required for edit_mode 'delete'");
67
155
  const index = findCellIndex(notebook, request.cellId);
68
- if (index === -1) throw new Error(`No cell with id "${request.cellId}"`);
156
+ if (index === -1) throw noSuchCellError(notebook, request.cellId);
157
+ const label = cellLabel(cells[index], index);
69
158
  cells.splice(index, 1);
70
- return { notebook: { ...notebook, cells }, summary: `Deleted cell ${request.cellId}` };
159
+ return finish(cells, `Deleted cell ${label}`);
71
160
  }
72
161
 
73
162
  if (request.newSource === undefined) throw new Error("new_source is required unless edit_mode is 'delete'");
74
163
 
75
- if (request.editMode === "insert") {
76
- if (!request.cellType) throw new Error("cell_type is required for edit_mode 'insert'");
77
- const id = makeId();
78
- const cell = newCell(request.cellType, request.newSource, id);
164
+ if (request.editMode === "insert" || request.editMode === "append") {
165
+ if (!request.cellType) throw new Error(`cell_type is required for edit_mode '${request.editMode}'`);
166
+ const cell = newCell(request.cellType, request.newSource, supportsIds(notebook) ? makeId() : undefined);
167
+ if (request.editMode === "append") {
168
+ cells.push(cell);
169
+ // Named the way the model must address it next: its id, or its position
170
+ // in a notebook that has none.
171
+ return finish(cells, `Appended ${request.cellType} cell ${cellLabel(cell, cells.length - 1)} at the end`);
172
+ }
79
173
  // Claude Code inserts AFTER the given cell; no cell_id means insert first.
80
174
  const index = request.cellId ? findCellIndex(notebook, request.cellId) : -1;
81
- if (request.cellId && index === -1) throw new Error(`No cell with id "${request.cellId}"`);
175
+ if (request.cellId && index === -1) throw noSuchCellError(notebook, request.cellId);
176
+ const afterLabel = request.cellId ? cellLabel(notebook.cells[index], index) : undefined;
82
177
  cells.splice(index + 1, 0, cell);
83
- return {
84
- notebook: { ...notebook, cells },
85
- summary: `Inserted ${request.cellType} cell ${id}${request.cellId ? ` after ${request.cellId}` : " at the top"}`,
86
- };
178
+ return finish(cells, `Inserted ${request.cellType} cell ${cellLabel(cell, index + 1)}${afterLabel ? ` after ${afterLabel}` : " at the top"}`);
87
179
  }
88
180
 
89
181
  if (!request.cellId) throw new Error("cell_id is required for edit_mode 'replace'");
90
182
  const index = findCellIndex(notebook, request.cellId);
91
- if (index === -1) throw new Error(`No cell with id "${request.cellId}"`);
183
+ if (index === -1) throw noSuchCellError(notebook, request.cellId);
92
184
  const existing = cells[index];
185
+ const label = cellLabel(existing, index);
93
186
  const cellType = (request.cellType ?? existing.cell_type) as "code" | "markdown";
94
187
  const replacement: NotebookCell = {
95
188
  ...existing,
@@ -104,5 +197,5 @@ export function applyEdit(notebook: Notebook, request: EditRequest, makeId: () =
104
197
  delete replacement.execution_count;
105
198
  }
106
199
  cells[index] = replacement;
107
- return { notebook: { ...notebook, cells }, summary: `Replaced cell ${request.cellId}` };
200
+ return finish(cells, `Replaced cell ${label}`);
108
201
  }