@towles/tool 0.0.118 → 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 +1 -1
- package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.ts +2 -1
- package/packages/agentboard/packages/runtime/src/server/index.ts +21 -3
- package/src/commands/auto-claude/run-claude.test.ts +1 -1
- package/src/commands/auto-claude/stream-parser.test.ts +2 -5
- package/src/commands/auto-claude/stream-parser.ts +26 -5
- package/src/commands/graph/analyzer.test.ts +38 -21
- package/src/commands/graph/tools.ts +1 -1
- package/src/commands/graph/types.ts +4 -13
- package/src/commands/graph.test.ts +14 -1
package/package.json
CHANGED
|
@@ -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) =>
|
|
65
|
+
const allAsking = toolUses.every((c) => c.name === "AskUserQuestion");
|
|
65
66
|
return allAsking ? "question" : "running";
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -133,18 +133,36 @@ export function startServer(
|
|
|
133
133
|
const map = getDirSessionMap();
|
|
134
134
|
const direct = map.get(projectDir);
|
|
135
135
|
if (direct) return direct;
|
|
136
|
+
// Find the most specific (longest) matching session dir.
|
|
137
|
+
// Without this, a project dir like /a/b could match /a/b/c (session1)
|
|
138
|
+
// before /a/b/c/d (session2), assigning it to the wrong session.
|
|
139
|
+
let bestMatch: string | null = null;
|
|
140
|
+
let bestLen = 0;
|
|
136
141
|
for (const [dir, name] of map) {
|
|
137
|
-
if (projectDir.startsWith(dir + "/") || dir.startsWith(projectDir + "/"))
|
|
142
|
+
if (projectDir.startsWith(dir + "/") || dir.startsWith(projectDir + "/")) {
|
|
143
|
+
if (dir.length > bestLen) {
|
|
144
|
+
bestLen = dir.length;
|
|
145
|
+
bestMatch = name;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
138
148
|
}
|
|
149
|
+
if (bestMatch) return bestMatch;
|
|
139
150
|
// Fallback: the decoded projectDir may be wrong due to dash ambiguity.
|
|
140
151
|
// Re-encode each known session dir and check if the encoded form matches
|
|
141
152
|
// as a prefix of the (still-encoded) input.
|
|
142
153
|
const encoded = encodeProjectDir(projectDir);
|
|
154
|
+
bestMatch = null;
|
|
155
|
+
bestLen = 0;
|
|
143
156
|
for (const [dir, name] of map) {
|
|
144
157
|
const encodedDir = encodeProjectDir(dir);
|
|
145
|
-
if (encoded.startsWith(encodedDir) || encodedDir.startsWith(encoded))
|
|
158
|
+
if (encoded.startsWith(encodedDir) || encodedDir.startsWith(encoded)) {
|
|
159
|
+
if (encodedDir.length > bestLen) {
|
|
160
|
+
bestLen = encodedDir.length;
|
|
161
|
+
bestMatch = name;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
146
164
|
}
|
|
147
|
-
return
|
|
165
|
+
return bestMatch;
|
|
148
166
|
},
|
|
149
167
|
emit(event: AgentEvent) {
|
|
150
168
|
tracker.applyEvent(event, { seed: !watchersSeeded });
|
|
@@ -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("
|
|
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
|
-
|
|
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
|
|
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
|
|
73
|
-
const text = typeof block.thinking === "string" ? block.thinking : "";
|
|
94
|
+
if (isThinkingBlock(block)) {
|
|
74
95
|
return {
|
|
75
96
|
kind: "thinking",
|
|
76
|
-
summary: truncate(
|
|
97
|
+
summary: truncate(block.thinking.split("\n")[0].trim(), 120),
|
|
77
98
|
};
|
|
78
99
|
}
|
|
79
100
|
|
|
80
|
-
if (block
|
|
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 ?? [
|
|
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: [
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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: [
|
|
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: [
|
|
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[] = [
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
}
|