@dungle-scrubs/tallow 0.8.7 → 0.8.8

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 (76) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +244 -54
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -1
  5. package/dist/config.js +1 -1
  6. package/dist/sdk.d.ts +39 -0
  7. package/dist/sdk.d.ts.map +1 -1
  8. package/dist/sdk.js +322 -13
  9. package/dist/sdk.js.map +1 -1
  10. package/dist/session-utils.d.ts +8 -0
  11. package/dist/session-utils.d.ts.map +1 -1
  12. package/dist/session-utils.js +38 -1
  13. package/dist/session-utils.js.map +1 -1
  14. package/extensions/__integration__/plan-rejection-feedback.test.ts +272 -0
  15. package/extensions/__integration__/worktree.test.ts +88 -0
  16. package/extensions/_icons/index.ts +2 -3
  17. package/extensions/_shared/inline-preview.ts +2 -4
  18. package/extensions/_shared/interop-events.ts +48 -0
  19. package/extensions/_shared/shell-policy.ts +2 -1
  20. package/extensions/_shared/tallow-paths.ts +48 -0
  21. package/extensions/agent-commands-tool/__tests__/parsing.test.ts +40 -1
  22. package/extensions/agent-commands-tool/index.ts +38 -6
  23. package/extensions/background-task-tool/index.ts +2 -2
  24. package/extensions/bash-tool-enhanced/index.ts +3 -4
  25. package/extensions/cd-tool/index.ts +3 -3
  26. package/extensions/claude-bridge/index.ts +2 -1
  27. package/extensions/command-prompt/__tests__/plugin-sources.test.ts +49 -0
  28. package/extensions/command-prompt/index.ts +36 -0
  29. package/extensions/context-files/index.ts +2 -1
  30. package/extensions/context-fork/index.ts +2 -1
  31. package/extensions/context-usage/__tests__/tool-result-memory.test.ts +62 -0
  32. package/extensions/context-usage/index.ts +169 -8
  33. package/extensions/debug/__tests__/analysis.test.ts +35 -2
  34. package/extensions/debug/__tests__/diagnostics-commands.test.ts +11 -2
  35. package/extensions/debug/analysis.ts +53 -16
  36. package/extensions/debug/index.ts +84 -10
  37. package/extensions/debug/logger.ts +2 -2
  38. package/extensions/health/index.ts +2 -2
  39. package/extensions/hooks/__tests__/claude-compat.test.ts +31 -0
  40. package/extensions/hooks/extension.json +3 -1
  41. package/extensions/hooks/hooks.schema.json +17 -1
  42. package/extensions/hooks/index.ts +52 -7
  43. package/extensions/lsp/index.ts +2 -7
  44. package/extensions/output-styles-tool/index.ts +2 -4
  45. package/extensions/plan-mode-tool/extension.json +1 -0
  46. package/extensions/plan-mode-tool/index.ts +119 -4
  47. package/extensions/prompt-suggestions/index.ts +2 -3
  48. package/extensions/random-spinner/index.ts +2 -3
  49. package/extensions/read-tool-enhanced/__tests__/notebook-read.test.ts +100 -0
  50. package/extensions/read-tool-enhanced/__tests__/notebook.test.ts +136 -0
  51. package/extensions/read-tool-enhanced/extension.json +4 -4
  52. package/extensions/read-tool-enhanced/index.ts +112 -10
  53. package/extensions/read-tool-enhanced/notebook.ts +526 -0
  54. package/extensions/rewind/__tests__/snapshots.test.ts +43 -1
  55. package/extensions/rewind/snapshots.ts +18 -6
  56. package/extensions/session-memory/index.ts +3 -2
  57. package/extensions/stats/stats-log.ts +2 -3
  58. package/extensions/subagent-tool/__tests__/discovery-defaults.test.ts +23 -0
  59. package/extensions/subagent-tool/__tests__/isolation-frontmatter.test.ts +100 -0
  60. package/extensions/subagent-tool/__tests__/model-router.test.ts +8 -0
  61. package/extensions/subagent-tool/agents.ts +77 -16
  62. package/extensions/subagent-tool/index.ts +213 -48
  63. package/extensions/subagent-tool/model-router.ts +3 -3
  64. package/extensions/subagent-tool/process.ts +233 -22
  65. package/extensions/subagent-tool/schema.ts +11 -0
  66. package/extensions/subagent-tool/widget.ts +15 -2
  67. package/extensions/tasks/__tests__/widget-subagents.test.ts +28 -7
  68. package/extensions/tasks/commands/register-tasks-extension.ts +15 -33
  69. package/extensions/tasks/state/index.ts +2 -2
  70. package/extensions/theme-selector/index.ts +3 -7
  71. package/extensions/worktree/__tests__/lifecycle.test.ts +115 -0
  72. package/extensions/worktree/extension.json +31 -0
  73. package/extensions/worktree/index.ts +149 -0
  74. package/extensions/worktree/lifecycle.ts +372 -0
  75. package/package.json +1 -1
  76. package/skills/tallow-expert/SKILL.md +1 -1
@@ -101,6 +101,7 @@ export default function planModeExtension(pi: ExtensionAPI): void {
101
101
  let planModeEnabled = false;
102
102
  let executionMode = false;
103
103
  let todoItems: TodoItem[] = [];
104
+ let currentStepIndex: number | null = null;
104
105
  let normalModeTools: string[] = [];
105
106
 
106
107
  /**
@@ -149,6 +150,45 @@ export default function planModeExtension(pi: ExtensionAPI): void {
149
150
  return normalModeTools;
150
151
  }
151
152
 
153
+ /**
154
+ * Computes the current step index from todo completion state.
155
+ *
156
+ * @param items - Ordered todo items
157
+ * @returns Zero-based index of first incomplete step, or null when complete
158
+ */
159
+ function computeCurrentStepIndex(items: readonly TodoItem[]): number | null {
160
+ const nextIndex = items.findIndex((item) => !item.completed);
161
+ return nextIndex >= 0 ? nextIndex : null;
162
+ }
163
+
164
+ /**
165
+ * Synchronizes currentStepIndex with todo completion state.
166
+ *
167
+ * @returns Updated current step index
168
+ */
169
+ function syncCurrentStepIndex(): number | null {
170
+ currentStepIndex = computeCurrentStepIndex(todoItems);
171
+ return currentStepIndex;
172
+ }
173
+
174
+ /**
175
+ * Gets the current step item for execution guidance.
176
+ *
177
+ * @returns Current step todo item, or null if all steps are complete
178
+ */
179
+ function getCurrentStep(): TodoItem | null {
180
+ if (todoItems.length === 0) return null;
181
+ if (
182
+ currentStepIndex === null ||
183
+ currentStepIndex < 0 ||
184
+ currentStepIndex >= todoItems.length ||
185
+ todoItems[currentStepIndex]?.completed
186
+ ) {
187
+ syncCurrentStepIndex();
188
+ }
189
+ return currentStepIndex === null ? null : (todoItems[currentStepIndex] ?? null);
190
+ }
191
+
152
192
  pi.registerFlag("plan", {
153
193
  description: "Start in plan mode (read-only exploration)",
154
194
  type: "boolean",
@@ -227,6 +267,7 @@ export default function planModeExtension(pi: ExtensionAPI): void {
227
267
  planModeEnabled = !planModeEnabled;
228
268
  executionMode = false;
229
269
  todoItems = [];
270
+ currentStepIndex = null;
230
271
 
231
272
  if (planModeEnabled) {
232
273
  captureNormalModeTools();
@@ -249,6 +290,7 @@ export default function planModeExtension(pi: ExtensionAPI): void {
249
290
  executing: executionMode,
250
291
  normalTools: normalModeTools,
251
292
  todos: todoItems,
293
+ currentStepIndex,
252
294
  });
253
295
  }
254
296
 
@@ -347,6 +389,7 @@ Use action "enable" to enter plan mode, "disable" to exit, or "status" to check
347
389
  planModeEnabled = shouldEnable;
348
390
  executionMode = false;
349
391
  todoItems = [];
392
+ currentStepIndex = null;
350
393
 
351
394
  let activeTools: string[];
352
395
  if (planModeEnabled) {
@@ -455,18 +498,27 @@ Do NOT attempt to make changes - just describe what you would do.`,
455
498
  }
456
499
 
457
500
  if (executionMode && todoItems.length > 0) {
501
+ syncCurrentStepIndex();
502
+ const currentStep = getCurrentStep();
458
503
  const remaining = todoItems.filter((t) => !t.completed);
459
504
  const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join("\n");
505
+ const currentStepText = currentStep
506
+ ? `${currentStep.step}. ${currentStep.text}`
507
+ : "(all steps completed)";
460
508
  return {
461
509
  message: {
462
510
  customType: "plan-execution-context",
463
511
  content: `[EXECUTING PLAN - Full tool access enabled]
464
512
 
513
+ Current step:
514
+ ${currentStepText}
515
+
465
516
  Remaining steps:
466
517
  ${todoList}
467
518
 
468
519
  Execute each step in order.
469
- After completing a step, include a [DONE:n] tag in your response.`,
520
+ After completing a step, include a [DONE:n] tag in your response.
521
+ If you receive [PLAN GUIDANCE — Step n: ...], treat it as user steering for that step and apply it before continuing.`,
470
522
  display: false,
471
523
  },
472
524
  };
@@ -479,12 +531,59 @@ After completing a step, include a [DONE:n] tag in your response.`,
479
531
  if (!isAssistantMessage(event.message)) return;
480
532
 
481
533
  const text = getTextContent(event.message);
482
- if (markCompletedSteps(text, todoItems) > 0) {
534
+ const completedCount = markCompletedSteps(text, todoItems);
535
+ if (completedCount > 0) {
536
+ syncCurrentStepIndex();
483
537
  updateStatus(ctx);
484
538
  }
485
539
  persistState();
486
540
  });
487
541
 
542
+ // Offer user steering when a tool is blocked during tracked execution.
543
+ pi.on("tool_result", async (event, ctx) => {
544
+ if (!executionMode || !event.isError || !ctx.hasUI || todoItems.length === 0) return;
545
+
546
+ const currentStep = getCurrentStep();
547
+ if (!currentStep) return;
548
+
549
+ const errorText = Array.isArray(event.content)
550
+ ? event.content
551
+ .filter((block): block is TextContent => block.type === "text")
552
+ .map((block) => block.text)
553
+ .join("\n")
554
+ .trim()
555
+ : "";
556
+
557
+ const shouldProvideGuidance = await ctx.ui.confirm(
558
+ "Plan step blocked",
559
+ [
560
+ `Tool "${event.toolName}" was blocked while executing step ${currentStep.step}.`,
561
+ "",
562
+ currentStep.text,
563
+ "",
564
+ `Error: ${errorText || "No error details provided."}`,
565
+ "",
566
+ "Provide guidance before execution continues?",
567
+ ].join("\n")
568
+ );
569
+ if (!shouldProvideGuidance) return;
570
+
571
+ const guidance = await ctx.ui.editor(`Guidance for step ${currentStep.step}:`, "");
572
+ const trimmedGuidance = guidance?.trim();
573
+ if (!trimmedGuidance) return;
574
+
575
+ pi.sendUserMessage(
576
+ [
577
+ `[PLAN GUIDANCE — Step ${currentStep.step}: ${currentStep.text}]`,
578
+ `Tool "${event.toolName}" was blocked.`,
579
+ "",
580
+ "User guidance:",
581
+ trimmedGuidance,
582
+ ].join("\n"),
583
+ { deliverAs: "steer" }
584
+ );
585
+ });
586
+
488
587
  // Handle plan completion and plan mode UI
489
588
  pi.on("agent_end", async (event, ctx) => {
490
589
  // Check if execution is complete
@@ -501,6 +600,7 @@ After completing a step, include a [DONE:n] tag in your response.`,
501
600
  );
502
601
  executionMode = false;
503
602
  todoItems = [];
603
+ currentStepIndex = null;
504
604
  restoreNormalModeTools();
505
605
  updateStatus(ctx);
506
606
  persistState(); // Save cleared state so resume doesn't restore old execution mode
@@ -543,12 +643,19 @@ After completing a step, include a [DONE:n] tag in your response.`,
543
643
  if (choice?.startsWith("Execute")) {
544
644
  planModeEnabled = false;
545
645
  executionMode = todoItems.length > 0;
646
+ if (executionMode) {
647
+ syncCurrentStepIndex();
648
+ } else {
649
+ currentStepIndex = null;
650
+ }
546
651
  restoreNormalModeTools();
547
652
  updateStatus(ctx);
653
+ persistState();
548
654
 
655
+ const currentStep = getCurrentStep();
549
656
  const execMessage =
550
- todoItems.length > 0
551
- ? `Execute the plan. Start with: ${todoItems[0].text}`
657
+ currentStep !== null
658
+ ? `Execute the plan. Start with step ${currentStep.step}: ${currentStep.text}`
552
659
  : "Execute the plan you just created.";
553
660
  pi.sendMessage(
554
661
  { customType: "plan-mode-execute", content: execMessage, display: true },
@@ -587,6 +694,7 @@ After completing a step, include a [DONE:n] tag in your response.`,
587
694
  executing?: boolean;
588
695
  normalTools?: string[];
589
696
  todos?: TodoItem[];
697
+ currentStepIndex?: number | null;
590
698
  };
591
699
  }
592
700
  | undefined;
@@ -596,6 +704,7 @@ After completing a step, include a [DONE:n] tag in your response.`,
596
704
  executionMode = planModeEntry.data.executing ?? executionMode;
597
705
  normalModeTools = planModeEntry.data.normalTools ?? normalModeTools;
598
706
  todoItems = planModeEntry.data.todos ?? todoItems;
707
+ currentStepIndex = planModeEntry.data.currentStepIndex ?? currentStepIndex;
599
708
  }
600
709
 
601
710
  // On resume: re-scan messages to rebuild completion state
@@ -628,6 +737,12 @@ After completing a step, include a [DONE:n] tag in your response.`,
628
737
  markCompletedSteps(allText, todoItems);
629
738
  }
630
739
 
740
+ if (executionMode && todoItems.length > 0) {
741
+ syncCurrentStepIndex();
742
+ } else {
743
+ currentStepIndex = null;
744
+ }
745
+
631
746
  if (normalModeTools.length === 0) {
632
747
  captureNormalModeTools();
633
748
  }
@@ -12,8 +12,6 @@
12
12
  */
13
13
 
14
14
  import { readFileSync } from "node:fs";
15
- import { homedir } from "node:os";
16
- import { join } from "node:path";
17
15
  import type { Api, Model } from "@mariozechner/pi-ai";
18
16
  import { completeSimple } from "@mariozechner/pi-ai";
19
17
  import {
@@ -22,6 +20,7 @@ import {
22
20
  type ExtensionContext,
23
21
  } from "@mariozechner/pi-coding-agent";
24
22
  import type { EditorTheme } from "@mariozechner/pi-tui";
23
+ import { getTallowSettingsPath } from "../_shared/tallow-paths.js";
25
24
  import { AutocompleteEngine, type ConversationContext } from "./autocomplete.js";
26
25
  import { GENERAL_TEMPLATES, type PromptTemplate } from "./templates.js";
27
26
 
@@ -30,7 +29,7 @@ import { GENERAL_TEMPLATES, type PromptTemplate } from "./templates.js";
30
29
  /** Read a setting from ~/.tallow/settings.json with a typed fallback. */
31
30
  function readSetting<T>(key: string, fallback: T): T {
32
31
  try {
33
- const raw = readFileSync(join(homedir(), ".tallow", "settings.json"), "utf-8");
32
+ const raw = readFileSync(getTallowSettingsPath(), "utf-8");
34
33
  const settings = JSON.parse(raw);
35
34
  return key in settings ? (settings[key] as T) : fallback;
36
35
  } catch {
@@ -12,10 +12,9 @@
12
12
  */
13
13
 
14
14
  import * as fs from "node:fs";
15
- import * as os from "node:os";
16
- import * as path from "node:path";
17
15
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
18
16
  import { Loader } from "@mariozechner/pi-tui";
17
+ import { getTallowSettingsPath } from "../_shared/tallow-paths.js";
19
18
 
20
19
  // ─── Types ───────────────────────────────────────────────────────────────────
21
20
 
@@ -599,7 +598,7 @@ function resolve(name: string): SpinnerPreset | undefined {
599
598
  * @returns Parsed spinner settings with defaults
600
599
  */
601
600
  function readSettings(): SpinnerSettings {
602
- const settingsPath = path.join(os.homedir(), ".tallow", "settings.json");
601
+ const settingsPath = getTallowSettingsPath();
603
602
  try {
604
603
  const raw = fs.readFileSync(settingsPath, "utf-8");
605
604
  return JSON.parse(raw) as SpinnerSettings;
@@ -0,0 +1,100 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
6
+ import { ExtensionHarness } from "../../../test-utils/extension-harness.js";
7
+ import readSummary from "../index.js";
8
+
9
+ /**
10
+ * Build a small notebook fixture for end-to-end read-tool execution.
11
+ *
12
+ * @returns Serialized notebook JSON string
13
+ */
14
+ function createNotebookFixture(): string {
15
+ return JSON.stringify({
16
+ metadata: {
17
+ kernelspec: {
18
+ language: "python",
19
+ },
20
+ },
21
+ cells: [
22
+ {
23
+ cell_type: "markdown",
24
+ source: ["# Header\n", "Notebook body\n"],
25
+ },
26
+ {
27
+ cell_type: "code",
28
+ execution_count: 1,
29
+ source: ["print('ok')\n"],
30
+ outputs: [
31
+ {
32
+ output_type: "stream",
33
+ name: "stdout",
34
+ text: ["ok\n"],
35
+ },
36
+ ],
37
+ },
38
+ ],
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Build a minimal extension context stub for tool execution.
44
+ *
45
+ * @param cwd - Working directory for tool execution
46
+ * @returns Partial context cast to ExtensionContext
47
+ */
48
+ function stubContext(cwd: string): ExtensionContext {
49
+ return {
50
+ hasUI: false,
51
+ ui: { setWorkingMessage() {} },
52
+ cwd,
53
+ } as unknown as ExtensionContext;
54
+ }
55
+
56
+ let notebookPath: string;
57
+ let tmpDir: string;
58
+
59
+ beforeAll(() => {
60
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "notebook-read-test-"));
61
+ notebookPath = path.join(tmpDir, "sample.ipynb");
62
+ fs.writeFileSync(notebookPath, createNotebookFixture(), "utf-8");
63
+ });
64
+
65
+ afterAll(() => {
66
+ fs.rmSync(tmpDir, { recursive: true, force: true });
67
+ });
68
+
69
+ describe("read tool notebook integration", () => {
70
+ test("reads notebook with summarized result and full text details", async () => {
71
+ const harness = ExtensionHarness.create();
72
+ await harness.loadExtension(readSummary);
73
+
74
+ const tool = harness.tools.get("read");
75
+ expect(tool).toBeDefined();
76
+ if (!tool) return;
77
+
78
+ const updates: Array<{ details?: Record<string, unknown> }> = [];
79
+ const result = await tool.execute(
80
+ "test-id",
81
+ { path: notebookPath },
82
+ new AbortController().signal,
83
+ (partial) => {
84
+ updates.push(partial as { details?: Record<string, unknown> });
85
+ },
86
+ stubContext(tmpDir)
87
+ );
88
+
89
+ const summary = result.content[0]?.type === "text" ? result.content[0].text : "";
90
+ const details = result.details as Record<string, unknown>;
91
+
92
+ expect(summary).toContain("sample.ipynb (2 cells");
93
+ expect(details.__notebook_read__).toBe(true);
94
+ expect(details._cellCount).toBe(2);
95
+ expect(typeof details._fullText).toBe("string");
96
+ expect(String(details._fullText)).toContain("Cell 1 [markdown]");
97
+ expect(String(details._fullText)).toContain("```python");
98
+ expect(updates.some((update) => Boolean(update.details?._preview))).toBe(true);
99
+ });
100
+ });
@@ -0,0 +1,136 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ formatNotebookOutput,
4
+ isNotebook,
5
+ NotebookParseError,
6
+ parseNotebook,
7
+ summarizeNotebookCounts,
8
+ } from "../notebook.js";
9
+
10
+ /**
11
+ * Build a representative notebook fixture with markdown, code, and rich outputs.
12
+ *
13
+ * @returns Serialized notebook JSON string
14
+ */
15
+ function createSampleNotebook(): string {
16
+ return JSON.stringify({
17
+ metadata: {
18
+ kernelspec: {
19
+ language: "python",
20
+ },
21
+ },
22
+ cells: [
23
+ {
24
+ cell_type: "markdown",
25
+ source: ["# Notebook title\n", "Some markdown context\n"],
26
+ },
27
+ {
28
+ cell_type: "code",
29
+ execution_count: 5,
30
+ source: ["print('hello')\n"],
31
+ outputs: [
32
+ {
33
+ output_type: "stream",
34
+ name: "stdout",
35
+ text: ["hello\n"],
36
+ },
37
+ ],
38
+ },
39
+ {
40
+ cell_type: "code",
41
+ execution_count: 6,
42
+ source: ["1 / 0\n"],
43
+ outputs: [
44
+ {
45
+ output_type: "error",
46
+ ename: "ZeroDivisionError",
47
+ evalue: "division by zero",
48
+ traceback: [
49
+ "Traceback (most recent call last):",
50
+ "ZeroDivisionError: division by zero",
51
+ ],
52
+ },
53
+ ],
54
+ },
55
+ {
56
+ cell_type: "code",
57
+ execution_count: 7,
58
+ source: ["plot()\n"],
59
+ outputs: [
60
+ {
61
+ output_type: "display_data",
62
+ data: {
63
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAA",
64
+ "text/html": "<img src='plot.png' />",
65
+ },
66
+ metadata: {
67
+ "image/png": {
68
+ height: 600,
69
+ width: 800,
70
+ },
71
+ },
72
+ },
73
+ ],
74
+ },
75
+ ],
76
+ });
77
+ }
78
+
79
+ describe("isNotebook", () => {
80
+ test("detects .ipynb path", () => {
81
+ expect(isNotebook("/tmp/analysis.ipynb")).toBe(true);
82
+ expect(isNotebook("/tmp/analysis.IPYNB")).toBe(true);
83
+ expect(isNotebook("/tmp/script.py")).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe("parseNotebook", () => {
88
+ test("parses notebook cells and language", () => {
89
+ const parsed = parseNotebook(createSampleNotebook());
90
+ expect(parsed.language).toBe("python");
91
+ expect(parsed.cells).toHaveLength(4);
92
+ expect(parsed.cells[0]?.cell_type).toBe("markdown");
93
+ expect(parsed.cells[1]?.cell_type).toBe("code");
94
+ expect(parsed.cells[1]?.execution_count).toBe(5);
95
+ expect(parsed.cells[1]?.outputs?.[0]?.output_type).toBe("stream");
96
+ });
97
+
98
+ test("throws NotebookParseError for invalid JSON", () => {
99
+ expect(() => parseNotebook("not-json")).toThrow(NotebookParseError);
100
+ });
101
+
102
+ test("throws NotebookParseError when cells are missing", () => {
103
+ expect(() => parseNotebook(JSON.stringify({ metadata: {} }))).toThrow("missing cells array");
104
+ });
105
+ });
106
+
107
+ describe("formatNotebookOutput", () => {
108
+ test("formats code cells, outputs, errors, and image placeholders", () => {
109
+ const parsed = parseNotebook(createSampleNotebook());
110
+ const output = formatNotebookOutput(parsed);
111
+
112
+ expect(output).toContain("[Notebook: python | 4 cells");
113
+ expect(output).toContain("Cell 2 [code]");
114
+ expect(output).toContain("# [5]");
115
+ expect(output).toContain("```python");
116
+ expect(output).toContain("[Output 1: stream]");
117
+ expect(output).toContain("hello");
118
+ expect(output).toContain("[Output 1: error]");
119
+ expect(output).toContain("ZeroDivisionError: division by zero");
120
+ expect(output).toContain("[Image output: PNG 800x600]");
121
+ expect(output).toContain("[HTML output]");
122
+ expect(output).not.toContain("iVBORw0KGgoAAAANSUhEUgAA");
123
+ });
124
+ });
125
+
126
+ describe("summarizeNotebookCounts", () => {
127
+ test("returns code/markdown/raw/output totals", () => {
128
+ const parsed = parseNotebook(createSampleNotebook());
129
+ const counts = summarizeNotebookCounts(parsed);
130
+
131
+ expect(counts.codeCells).toBe(3);
132
+ expect(counts.markdownCells).toBe(1);
133
+ expect(counts.rawCells).toBe(0);
134
+ expect(counts.outputCount).toBe(3);
135
+ });
136
+ });
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "read-tool-enhanced",
3
3
  "version": "0.3.0",
4
- "description": "Enhanced read tool with live truncated preview, compact summary, and PDF text extraction",
5
- "whenToUse": "Use when you need improved `read` previews, truncation, and PDF extraction.",
4
+ "description": "Enhanced read tool with live truncated preview plus PDF and notebook extraction",
5
+ "whenToUse": "Use when you need improved `read` previews, truncation, and PDF/notebook extraction.",
6
6
  "capabilities": {
7
7
  "tools": ["read"],
8
8
  "events": ["context", "session_start"]
@@ -14,8 +14,8 @@
14
14
  "subprocess": false
15
15
  },
16
16
  "category": "utility",
17
- "tags": ["context", "optimization", "display", "pdf"],
18
- "files": ["index.ts", "pdf.ts"],
17
+ "tags": ["context", "optimization", "display", "pdf", "notebook"],
18
+ "files": ["index.ts", "notebook.ts", "pdf.ts"],
19
19
  "relationships": [
20
20
  {
21
21
  "name": "tool-display",