@towles/tool 0.0.119 → 0.0.120

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towles/tool",
3
- "version": "0.0.119",
3
+ "version": "0.0.120",
4
4
  "description": "One off quality of life scripts that I use on a daily basis.",
5
5
  "homepage": "https://github.com/ChrisTowles/towles-tool#readme",
6
6
  "bugs": {
@@ -25,6 +25,7 @@ import { JOURNAL_IDLE_TIMEOUT_MS } from "../../shared";
25
25
  interface ContentItem {
26
26
  type?: string;
27
27
  text?: string;
28
+ name?: string;
28
29
  }
29
30
 
30
31
  interface JournalEntry {
@@ -61,7 +62,7 @@ export function determineStatus(entry: JournalEntry): AgentStatus | null {
61
62
  if (msg.role === "assistant") {
62
63
  const toolUses = items.filter((c) => c.type === "tool_use");
63
64
  if (toolUses.length === 0) return "done";
64
- const allAsking = toolUses.every((c) => (c as any).name === "AskUserQuestion");
65
+ const allAsking = toolUses.every((c) => c.name === "AskUserQuestion");
65
66
  return allAsking ? "question" : "running";
66
67
  }
67
68
 
@@ -83,7 +83,7 @@ describe("runClaude (injected spawnFn)", () => {
83
83
  type: "stream_event",
84
84
  event: {
85
85
  type: "content_block_start",
86
- content_block: { type: "thinking" },
86
+ content_block: { type: "thinking", thinking: "Let me consider this" },
87
87
  },
88
88
  };
89
89
  const toolEvent = {
@@ -263,7 +263,7 @@ describe("parseStreamLine", () => {
263
263
  }
264
264
  });
265
265
 
266
- it("handles empty thinking", () => {
266
+ it("rejects thinking block without string thinking field", () => {
267
267
  const line = JSON.stringify({
268
268
  type: "stream_event",
269
269
  event: {
@@ -272,10 +272,7 @@ describe("parseStreamLine", () => {
272
272
  },
273
273
  });
274
274
 
275
- const event = parseStreamLine(line);
276
- if (event?.kind === "thinking") {
277
- expect(event.summary).toBe("");
278
- }
275
+ expect(parseStreamLine(line)).toBeNull();
279
276
  });
280
277
  });
281
278
 
@@ -1,3 +1,25 @@
1
+ import type {
2
+ TextBlock,
3
+ ThinkingBlock,
4
+ ToolUseBlock,
5
+ } from "@anthropic-ai/sdk/resources/messages/messages";
6
+
7
+ function isToolUseBlock(
8
+ block: Record<string, unknown>,
9
+ ): block is ToolUseBlock & Record<string, unknown> {
10
+ return block.type === "tool_use" && typeof block.name === "string";
11
+ }
12
+
13
+ function isThinkingBlock(
14
+ block: Record<string, unknown>,
15
+ ): block is ThinkingBlock & Record<string, unknown> {
16
+ return block.type === "thinking" && typeof block.thinking === "string";
17
+ }
18
+
19
+ function isTextBlock(block: Record<string, unknown>): block is TextBlock & Record<string, unknown> {
20
+ return block.type === "text" && typeof block.text === "string";
21
+ }
22
+
1
23
  export interface AgentToolEvent {
2
24
  kind: "tool_use";
3
25
  name: string;
@@ -57,7 +79,7 @@ function toolDetail(block: Record<string, unknown>): string {
57
79
  }
58
80
 
59
81
  function parseContentBlock(block: Record<string, unknown>): AgentActivityEvent | null {
60
- if (block.type === "tool_use" && typeof block.name === "string") {
82
+ if (isToolUseBlock(block)) {
61
83
  return {
62
84
  kind: "tool_use",
63
85
  name: block.name,
@@ -69,15 +91,14 @@ function parseContentBlock(block: Record<string, unknown>): AgentActivityEvent |
69
91
  };
70
92
  }
71
93
 
72
- if (block.type === "thinking") {
73
- const text = typeof block.thinking === "string" ? block.thinking : "";
94
+ if (isThinkingBlock(block)) {
74
95
  return {
75
96
  kind: "thinking",
76
- summary: truncate(text.split("\n")[0].trim(), 120),
97
+ summary: truncate(block.thinking.split("\n")[0].trim(), 120),
77
98
  };
78
99
  }
79
100
 
80
- if (block.type === "text" && typeof block.text === "string") {
101
+ if (isTextBlock(block)) {
81
102
  return {
82
103
  kind: "text",
83
104
  content: block.text,
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import type { ContentBlock, JournalEntry } from "./types";
3
+ import type { Usage } from "@anthropic-ai/sdk/resources/messages/messages";
3
4
  import {
4
5
  aggregateSessionTools,
5
6
  analyzeSession,
@@ -12,6 +13,26 @@ import { extractToolData, extractToolDetail, sanitizeString, truncateDetail } fr
12
13
 
13
14
  // ── Helpers ──
14
15
 
16
+ function textBlock(text: string): ContentBlock {
17
+ return { type: "text" as const, text, citations: null };
18
+ }
19
+
20
+ function toolUseBlock(name: string, input: Record<string, unknown>): ContentBlock {
21
+ return { type: "tool_use" as const, id: "tool-stub", name, input };
22
+ }
23
+
24
+ function makeUsage(overrides: Partial<Usage> = {}): Usage {
25
+ return {
26
+ input_tokens: 0,
27
+ output_tokens: 0,
28
+ cache_read_input_tokens: null,
29
+ cache_creation_input_tokens: null,
30
+ server_tool_use: null,
31
+ service_tier: null,
32
+ ...overrides,
33
+ };
34
+ }
35
+
15
36
  function makeEntry(overrides: Partial<JournalEntry> = {}): JournalEntry {
16
37
  return {
17
38
  type: "assistant",
@@ -33,8 +54,8 @@ function makeAssistantEntry(
33
54
  message: {
34
55
  role: "assistant",
35
56
  model,
36
- usage: { input_tokens: inputTokens, output_tokens: outputTokens },
37
- content: content ?? [{ type: "text", text: "response" }],
57
+ usage: makeUsage({ input_tokens: inputTokens, output_tokens: outputTokens }),
58
+ content: content ?? [textBlock("response")],
38
59
  ...extra,
39
60
  },
40
61
  });
@@ -76,8 +97,8 @@ describe("analyzeSession", () => {
76
97
  message: {
77
98
  role: "assistant",
78
99
  model: "claude-opus-4",
79
- usage: { input_tokens: 1000, output_tokens: 0, cache_read_input_tokens: 800 },
80
- content: [{ type: "text", text: "hi" }],
100
+ usage: makeUsage({ input_tokens: 1000, output_tokens: 0, cache_read_input_tokens: 800 }),
101
+ content: [textBlock("hi")],
81
102
  },
82
103
  }),
83
104
  ];
@@ -87,9 +108,9 @@ describe("analyzeSession", () => {
87
108
 
88
109
  it("counts repeated file reads", () => {
89
110
  const content: ContentBlock[] = [
90
- { type: "tool_use", name: "Read", input: { file_path: "/a.ts" } },
91
- { type: "tool_use", name: "Read", input: { file_path: "/a.ts" } },
92
- { type: "tool_use", name: "Read", input: { file_path: "/b.ts" } },
111
+ toolUseBlock("Read", { file_path: "/a.ts" }),
112
+ toolUseBlock("Read", { file_path: "/a.ts" }),
113
+ toolUseBlock("Read", { file_path: "/b.ts" }),
93
114
  ];
94
115
  const entries = [makeAssistantEntry("claude-opus-4", 100, 50, content)];
95
116
  const result = analyzeSession(entries);
@@ -145,7 +166,7 @@ describe("extractSessionLabel", () => {
145
166
  type: "user",
146
167
  message: {
147
168
  role: "user",
148
- content: [{ type: "text", text: "Array content message" }],
169
+ content: [textBlock("Array content message")],
149
170
  },
150
171
  }),
151
172
  ];
@@ -158,7 +179,7 @@ describe("extractSessionLabel", () => {
158
179
  type: "assistant",
159
180
  message: {
160
181
  role: "assistant",
161
- content: [{ type: "text", text: "I'll help you with that" }],
182
+ content: [textBlock("I'll help you with that")],
162
183
  },
163
184
  }),
164
185
  ];
@@ -314,14 +335,14 @@ describe("extractToolData", () => {
314
335
  });
315
336
 
316
337
  it("returns empty when no tool_use blocks", () => {
317
- const content: ContentBlock[] = [{ type: "text", text: "hello" }];
338
+ const content: ContentBlock[] = [textBlock("hello")];
318
339
  expect(extractToolData(content, 100, 50)).toEqual([]);
319
340
  });
320
341
 
321
342
  it("extracts tool calls and distributes tokens", () => {
322
343
  const content: ContentBlock[] = [
323
- { type: "tool_use", name: "Read", input: { file_path: "/a.ts" } },
324
- { type: "tool_use", name: "Bash", input: { command: "ls" } },
344
+ toolUseBlock("Read", { file_path: "/a.ts" }),
345
+ toolUseBlock("Bash", { command: "ls" }),
325
346
  ];
326
347
  const result = extractToolData(content, 200, 100);
327
348
  expect(result).toHaveLength(2);
@@ -342,12 +363,10 @@ describe("aggregateSessionTools", () => {
342
363
 
343
364
  it("aggregates tools across entries", () => {
344
365
  const entries = [
345
- makeAssistantEntry("claude-opus-4", 100, 50, [
346
- { type: "tool_use", name: "Read", input: { file_path: "/a.ts" } },
347
- ]),
366
+ makeAssistantEntry("claude-opus-4", 100, 50, [toolUseBlock("Read", { file_path: "/a.ts" })]),
348
367
  makeAssistantEntry("claude-opus-4", 200, 100, [
349
- { type: "tool_use", name: "Read", input: { file_path: "/b.ts" } },
350
- { type: "tool_use", name: "Bash", input: { command: "ls" } },
368
+ toolUseBlock("Read", { file_path: "/b.ts" }),
369
+ toolUseBlock("Bash", { command: "ls" }),
351
370
  ]),
352
371
  ];
353
372
  const result = aggregateSessionTools(entries);
@@ -358,11 +377,9 @@ describe("aggregateSessionTools", () => {
358
377
 
359
378
  it("sorts by total token usage descending", () => {
360
379
  const entries = [
361
- makeAssistantEntry("claude-opus-4", 100, 50, [
362
- { type: "tool_use", name: "Bash", input: { command: "ls" } },
363
- ]),
380
+ makeAssistantEntry("claude-opus-4", 100, 50, [toolUseBlock("Bash", { command: "ls" })]),
364
381
  makeAssistantEntry("claude-opus-4", 1000, 500, [
365
- { type: "tool_use", name: "Read", input: { file_path: "/a.ts" } },
382
+ toolUseBlock("Read", { file_path: "/a.ts" }),
366
383
  ]),
367
384
  ];
368
385
  const result = aggregateSessionTools(entries);
@@ -68,7 +68,7 @@ export function extractToolData(
68
68
  const toolBlocks: Array<{ name: string; detail?: string }> = [];
69
69
  for (const block of content) {
70
70
  if (block.type === "tool_use" && block.name) {
71
- const detail = extractToolDetail(block.name, block.input);
71
+ const detail = extractToolDetail(block.name, block.input as Record<string, unknown>);
72
72
  toolBlocks.push({ name: block.name, detail });
73
73
  }
74
74
  }
@@ -1,11 +1,7 @@
1
1
  // Types for parsing Claude Code session JSONL files
2
- export interface ContentBlock {
3
- type: string;
4
- text?: string;
5
- id?: string;
6
- name?: string;
7
- input?: Record<string, unknown>;
8
- }
2
+ import type { ContentBlock, Usage } from "@anthropic-ai/sdk/resources/messages/messages";
3
+
4
+ export type { ContentBlock };
9
5
 
10
6
  export interface JournalEntry {
11
7
  type: string;
@@ -14,12 +10,7 @@ export interface JournalEntry {
14
10
  message?: {
15
11
  role: "user" | "assistant";
16
12
  model?: string;
17
- usage?: {
18
- input_tokens?: number;
19
- output_tokens?: number;
20
- cache_read_input_tokens?: number;
21
- cache_creation_input_tokens?: number;
22
- };
13
+ usage?: Usage;
23
14
  content?: ContentBlock[] | string;
24
15
  };
25
16
  uuid?: string;
@@ -2,8 +2,21 @@
2
2
  * Tests for graph command --days filtering and bar chart data
3
3
  */
4
4
  import { describe, it, expect } from "vitest";
5
+ import type { Usage } from "@anthropic-ai/sdk/resources/messages/messages";
5
6
  import { analyzeSession, calculateCutoffMs, filterByDays } from "./graph/index.js";
6
7
 
8
+ function makeUsage(overrides: Partial<Usage> = {}): Usage {
9
+ return {
10
+ input_tokens: 0,
11
+ output_tokens: 0,
12
+ cache_read_input_tokens: null,
13
+ cache_creation_input_tokens: null,
14
+ server_tool_use: null,
15
+ service_tier: null,
16
+ ...overrides,
17
+ };
18
+ }
19
+
7
20
  describe("graph --days filtering", () => {
8
21
  describe("calculateCutoffMs", () => {
9
22
  it("returns 0 when days <= 0", () => {
@@ -98,7 +111,7 @@ describe("analyzeSession (bar chart token aggregation)", () => {
98
111
  message: {
99
112
  role: "assistant" as const,
100
113
  model,
101
- usage: { input_tokens: inputTokens, output_tokens: outputTokens },
114
+ usage: makeUsage({ input_tokens: inputTokens, output_tokens: outputTokens }),
102
115
  },
103
116
  };
104
117
  }