@towles/tool 0.0.109 → 0.0.110
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 +9 -4
- package/{plugins/tt-agentboard → packages/agentboard}/README.md +1 -1
- package/{plugins/tt-agentboard → packages/agentboard}/apps/server/package.json +2 -1
- package/{plugins/tt-agentboard → packages/agentboard}/apps/server/src/main.ts +6 -20
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/package.json +4 -0
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DetailPanel.tsx +3 -2
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/StatusBar.tsx +35 -0
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/constants.ts +1 -0
- package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/index.tsx +204 -225
- package/packages/agentboard/apps/tui/src/session-status.test.ts +70 -0
- package/packages/agentboard/apps/tui/src/session-status.ts +19 -0
- package/{plugins/tt-agentboard → packages/agentboard}/package.json +2 -6
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/package.json +3 -0
- package/{plugins/tt-agentboard/packages/runtime/test → packages/agentboard/packages/runtime/src/agents}/tracker.test.ts +2 -2
- package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.test.ts +63 -0
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/claude-code.ts +26 -2
- package/packages/agentboard/packages/runtime/src/config.test.ts +107 -0
- package/packages/agentboard/packages/runtime/src/config.ts +80 -0
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/index.ts +1 -1
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/plugins/loader.ts +1 -33
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/git-info.ts +3 -2
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/index.ts +23 -37
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/launcher.ts +6 -18
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/pane-scanner.ts +6 -0
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/shared.ts +2 -0
- package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/tsconfig.json +1 -1
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/git/exec.ts +41 -0
- package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.ts +13 -18
- package/packages/shared/src/index.ts +8 -0
- package/packages/shared/tsconfig.json +16 -0
- package/src/cli.ts +1 -1
- package/src/commands/agentboard.ts +42 -59
- package/src/{lib → commands}/auto-claude/claude-cli.ts +1 -1
- package/src/commands/auto-claude/config-init-helpers.ts +79 -0
- package/src/commands/auto-claude/config-init.test.ts +137 -0
- package/src/commands/auto-claude/config-init.ts +159 -0
- package/src/{lib → commands}/auto-claude/config.ts +4 -8
- package/src/{lib → commands}/auto-claude/e2e.test.ts +6 -6
- package/src/commands/auto-claude/explain.test.ts +58 -0
- package/src/commands/auto-claude/explain.ts +97 -0
- package/src/commands/auto-claude/index.ts +37 -14
- package/src/{lib → commands}/auto-claude/labels.ts +1 -1
- package/src/commands/auto-claude/list.ts +5 -4
- package/src/{lib → commands}/auto-claude/pipeline-execution.test.ts +1 -1
- package/src/{lib → commands}/auto-claude/pipeline.ts +1 -3
- package/src/commands/auto-claude/retry.test.ts +2 -2
- package/src/commands/auto-claude/retry.ts +5 -5
- package/src/commands/auto-claude/shell.ts +3 -0
- package/src/commands/auto-claude/status.test.ts +2 -2
- package/src/commands/auto-claude/status.ts +4 -4
- package/src/{lib → commands}/auto-claude/steps/create-pr.ts +1 -3
- package/src/{lib → commands}/auto-claude/steps/fetch-issues.ts +1 -1
- package/src/{lib → commands}/auto-claude/steps/implement.ts +1 -2
- package/src/{lib → commands}/auto-claude/utils-execution.test.ts +6 -6
- package/src/{lib → commands}/auto-claude/utils.ts +10 -4
- package/src/{lib/install → commands}/claude-settings.ts +1 -1
- package/src/commands/config/config.test.ts +129 -0
- package/src/commands/config/index.ts +11 -0
- package/src/commands/config/reset.ts +53 -0
- package/src/commands/config/schema.ts +19 -0
- package/src/commands/{config.ts → config/show.ts} +2 -2
- package/src/commands/config/validate.ts +51 -0
- package/src/commands/doctor/checks.ts +167 -0
- package/src/commands/doctor/format.test.ts +63 -0
- package/src/commands/doctor/format.ts +5 -0
- package/src/commands/doctor/history.test.ts +161 -0
- package/src/commands/doctor/history.ts +130 -0
- package/src/commands/doctor.ts +80 -151
- package/src/commands/gh/branch-clean.ts +4 -4
- package/src/commands/gh/branch.test.ts +4 -5
- package/src/commands/gh/branch.ts +10 -5
- package/src/commands/gh/pr.ts +6 -7
- package/src/{lib → commands}/graph/analyzer.test.ts +4 -4
- package/src/commands/graph/format.test.ts +130 -0
- package/src/commands/graph/format.ts +94 -0
- package/src/commands/graph/index.ts +69 -41
- package/src/{lib → commands}/graph/labels.ts +4 -4
- package/src/{lib → commands}/graph/server.ts +2 -2
- package/src/{lib → commands}/graph/types.ts +2 -0
- package/src/commands/graph.test.ts +1 -1
- package/src/commands/install.ts +6 -6
- package/src/commands/journal/daily-notes.ts +4 -7
- package/src/{lib → commands}/journal/fs.ts +1 -1
- package/src/commands/journal/index.ts +2 -0
- package/src/commands/journal/list.test.ts +174 -0
- package/src/commands/journal/list.ts +213 -0
- package/src/commands/journal/meeting.ts +4 -7
- package/src/commands/journal/note.ts +4 -7
- package/src/{lib → commands}/journal/paths.ts +1 -1
- package/src/commands/journal/search.test.ts +156 -0
- package/src/commands/journal/search.ts +256 -0
- package/src/{lib → commands}/journal/templates.ts +1 -1
- package/src/config/settings.ts +35 -26
- package/plugins/tt-agentboard/bun.lock +0 -444
- package/plugins/tt-agentboard/packages/runtime/src/config.ts +0 -70
- package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +0 -83
- package/plugins/tt-auto-claude/.claude-plugin/plugin.json +0 -8
- package/plugins/tt-auto-claude/commands/create-issue.md +0 -20
- package/plugins/tt-auto-claude/commands/list.md +0 -21
- package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +0 -71
- package/plugins/tt-core/promptfooconfig.interview-me.yaml +0 -155
- package/plugins/tt-core/promptfooconfig.refine-text.yaml +0 -242
- package/plugins/tt-core/promptfooconfig.tdd.yaml +0 -144
- package/plugins/tt-core/promptfooconfig.write-prd.yaml +0 -145
- package/src/commands/config.test.ts +0 -9
- package/src/lib/auto-claude/index.ts +0 -15
- package/src/lib/auto-claude/shell.ts +0 -6
- package/src/lib/graph/index.ts +0 -24
- package/src/lib/journal/index.ts +0 -11
- package/src/utils/git/exec.ts +0 -18
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/build.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/bunfig.toml +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/scripts/sessionizer.sh +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DiffStats.tsx +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/SessionCard.tsx +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/detail-panel-height.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/mux-context.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/tsconfig.json +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/package.json +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/client.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/index.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/provider.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/tsconfig.json +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/tracker.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/amp.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/codex.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/opencode.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent-watcher.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/index.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/mux.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/debug.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/detect.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/registry.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/context.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/metadata-store.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/port-scanner.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/session-order.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-manager.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-width-sync.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/themes.ts +0 -0
- /package/{plugins/tt-agentboard → packages/agentboard}/tsconfig.json +0 -0
- /package/{plugins/tt-core → packages/core}/.claude-plugin/plugin.json +0 -0
- /package/{plugins/tt-core → packages/core}/README.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/improve-architecture.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/interview-me.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/prd-to-issues.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/refine-text.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/task.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/tdd.md +0 -0
- /package/{plugins/tt-core → packages/core}/commands/write-prd.md +0 -0
- /package/{plugins/tt-core → packages/core}/skills/towles-tool/SKILL.md +0 -0
- /package/{src/utils → packages/shared/src}/date-utils.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/date-utils.ts +0 -0
- /package/{src/utils → packages/shared/src}/fs.ts +0 -0
- /package/{src/utils → packages/shared/src}/git/branch-name.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/git/branch-name.ts +0 -0
- /package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/render.test.ts +0 -0
- /package/{src/utils → packages/shared/src}/render.ts +0 -0
- /package/src/{lib → commands}/auto-claude/config.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/labels.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/pipeline.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/01_plan.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/02_implement.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/03_simplify.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/04_review.prompt.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/CLAUDE.md +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/index.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/prompt-templates/index.ts +0 -0
- /package/src/{lib → commands}/auto-claude/run-claude.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/spawn-claude.ts +0 -0
- /package/src/{lib → commands}/auto-claude/steps/simple-steps.ts +0 -0
- /package/src/{lib → commands}/auto-claude/steps/steps.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/stream-parser.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/stream-parser.ts +0 -0
- /package/src/{lib → commands}/auto-claude/templates.test.ts +0 -0
- /package/src/{lib → commands}/auto-claude/templates.ts +0 -0
- /package/src/{lib → commands}/auto-claude/test-helpers.ts +0 -0
- /package/src/{lib → commands}/auto-claude/utils.test.ts +0 -0
- /package/src/{lib → commands}/graph/analyzer.ts +0 -0
- /package/src/{lib → commands}/graph/graph-template.html +0 -0
- /package/src/{lib → commands}/graph/parser.test.ts +0 -0
- /package/src/{lib → commands}/graph/parser.ts +0 -0
- /package/src/{lib → commands}/graph/render.ts +0 -0
- /package/src/{lib → commands}/graph/sessions.ts +0 -0
- /package/src/{lib → commands}/graph/tools.ts +0 -0
- /package/src/{lib → commands}/graph/treemap.ts +0 -0
- /package/src/{lib → commands}/journal/editor.ts +0 -0
package/src/commands/gh/pr.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
|
-
import {
|
|
2
|
+
import { run, isGithubCliInstalled } from "@towles/shared";
|
|
3
3
|
import consola from "consola";
|
|
4
4
|
import { colors } from "consola/utils";
|
|
5
5
|
|
|
6
6
|
import { debugArg } from "../shared.js";
|
|
7
|
-
import { isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
|
|
8
7
|
|
|
9
8
|
function generatePrContent(branch: string, commits: string[]): { title: string; body: string } {
|
|
10
9
|
// Extract issue number from branch name if present (e.g., feature/123-some-feature)
|
|
@@ -85,7 +84,7 @@ export default defineCommand({
|
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
// Get current branch
|
|
88
|
-
const branchResult = await
|
|
87
|
+
const branchResult = await run("git", ["branch", "--show-current"]);
|
|
89
88
|
const currentBranch = branchResult.stdout.trim();
|
|
90
89
|
|
|
91
90
|
if (!currentBranch) {
|
|
@@ -102,7 +101,7 @@ export default defineCommand({
|
|
|
102
101
|
consola.info(`Base branch: ${colors.cyan(args.base)}`);
|
|
103
102
|
|
|
104
103
|
// Get commits between base and current branch
|
|
105
|
-
const logResult = await
|
|
104
|
+
const logResult = await run("git", ["log", `${args.base}..HEAD`, "--pretty=format:%s"]);
|
|
106
105
|
|
|
107
106
|
const commits = logResult.stdout.trim().split("\n").filter(Boolean);
|
|
108
107
|
|
|
@@ -135,12 +134,12 @@ export default defineCommand({
|
|
|
135
134
|
}
|
|
136
135
|
|
|
137
136
|
// Push branch if needed
|
|
138
|
-
const statusResult = await
|
|
137
|
+
const statusResult = await run("git", ["status", "-sb"]);
|
|
139
138
|
const needsPush = !statusResult.stdout.includes("origin/");
|
|
140
139
|
|
|
141
140
|
if (needsPush) {
|
|
142
141
|
consola.info("Pushing branch to remote...");
|
|
143
|
-
await
|
|
142
|
+
await run("git", ["push", "-u", "origin", currentBranch]);
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
// Create PR
|
|
@@ -150,7 +149,7 @@ export default defineCommand({
|
|
|
150
149
|
prArgs.push("--draft");
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
const prResult = await
|
|
152
|
+
const prResult = await run("gh", prArgs);
|
|
154
153
|
const prUrl = prResult.stdout.trim();
|
|
155
154
|
|
|
156
155
|
consola.success(`PR created: ${colors.cyan(prUrl)}`);
|
|
@@ -166,8 +166,8 @@ describe("extractSessionLabel", () => {
|
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
it("falls back to gitBranch", () => {
|
|
169
|
-
const entries = [makeEntry({ type: "user" })
|
|
170
|
-
|
|
169
|
+
const entries = [makeEntry({ type: "user" })];
|
|
170
|
+
entries[0].gitBranch = "feat/new-feature";
|
|
171
171
|
expect(extractSessionLabel(entries, "abc12345")).toBe("feat/new-feature");
|
|
172
172
|
});
|
|
173
173
|
|
|
@@ -209,8 +209,8 @@ describe("extractSessionLabel", () => {
|
|
|
209
209
|
});
|
|
210
210
|
|
|
211
211
|
it("uses slug fallback for short labels after cleanup", () => {
|
|
212
|
-
const entries = [makeEntry()
|
|
213
|
-
|
|
212
|
+
const entries = [makeEntry()];
|
|
213
|
+
entries[0].slug = "my-slug";
|
|
214
214
|
expect(extractSessionLabel(entries, "abc12345")).toBe("my-slug");
|
|
215
215
|
});
|
|
216
216
|
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatCsv, formatJson } from "./format";
|
|
3
|
+
import type { SessionRow } from "./format";
|
|
4
|
+
|
|
5
|
+
// ── formatJson ──
|
|
6
|
+
|
|
7
|
+
describe("formatJson", () => {
|
|
8
|
+
it("returns valid JSON for empty array", () => {
|
|
9
|
+
const result = formatJson([]);
|
|
10
|
+
expect(JSON.parse(result)).toEqual([]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("serializes session rows with all fields", () => {
|
|
14
|
+
const rows: SessionRow[] = [
|
|
15
|
+
{
|
|
16
|
+
sessionPath: "/home/user/.claude/projects/test/abc123.jsonl",
|
|
17
|
+
project: "my-project",
|
|
18
|
+
model: "Opus",
|
|
19
|
+
inputTokens: 1000,
|
|
20
|
+
outputTokens: 500,
|
|
21
|
+
totalTokens: 1500,
|
|
22
|
+
cost: 0.0525,
|
|
23
|
+
date: "2025-06-15",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
const parsed = JSON.parse(formatJson(rows));
|
|
27
|
+
expect(parsed).toHaveLength(1);
|
|
28
|
+
expect(parsed[0].sessionPath).toBe("/home/user/.claude/projects/test/abc123.jsonl");
|
|
29
|
+
expect(parsed[0].project).toBe("my-project");
|
|
30
|
+
expect(parsed[0].model).toBe("Opus");
|
|
31
|
+
expect(parsed[0].inputTokens).toBe(1000);
|
|
32
|
+
expect(parsed[0].outputTokens).toBe(500);
|
|
33
|
+
expect(parsed[0].totalTokens).toBe(1500);
|
|
34
|
+
expect(parsed[0].cost).toBe(0.0525);
|
|
35
|
+
expect(parsed[0].date).toBe("2025-06-15");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("serializes multiple rows", () => {
|
|
39
|
+
const rows: SessionRow[] = [
|
|
40
|
+
{
|
|
41
|
+
sessionPath: "/a.jsonl",
|
|
42
|
+
project: "proj-a",
|
|
43
|
+
model: "Opus",
|
|
44
|
+
inputTokens: 100,
|
|
45
|
+
outputTokens: 50,
|
|
46
|
+
totalTokens: 150,
|
|
47
|
+
cost: 0.005,
|
|
48
|
+
date: "2025-06-15",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
sessionPath: "/b.jsonl",
|
|
52
|
+
project: "proj-b",
|
|
53
|
+
model: "Sonnet",
|
|
54
|
+
inputTokens: 200,
|
|
55
|
+
outputTokens: 100,
|
|
56
|
+
totalTokens: 300,
|
|
57
|
+
cost: 0.002,
|
|
58
|
+
date: "2025-06-16",
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
const parsed = JSON.parse(formatJson(rows));
|
|
62
|
+
expect(parsed).toHaveLength(2);
|
|
63
|
+
expect(parsed[0].project).toBe("proj-a");
|
|
64
|
+
expect(parsed[1].project).toBe("proj-b");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── formatCsv ──
|
|
69
|
+
|
|
70
|
+
describe("formatCsv", () => {
|
|
71
|
+
it("returns header only for empty array", () => {
|
|
72
|
+
const result = formatCsv([]);
|
|
73
|
+
expect(result).toBe(
|
|
74
|
+
"session_path,project,model,input_tokens,output_tokens,total_tokens,cost,date",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("formats rows with proper CSV quoting", () => {
|
|
79
|
+
const rows: SessionRow[] = [
|
|
80
|
+
{
|
|
81
|
+
sessionPath: "/home/user/.claude/projects/test/abc123.jsonl",
|
|
82
|
+
project: "my-project",
|
|
83
|
+
model: "Opus",
|
|
84
|
+
inputTokens: 1000,
|
|
85
|
+
outputTokens: 500,
|
|
86
|
+
totalTokens: 1500,
|
|
87
|
+
cost: 0.0525,
|
|
88
|
+
date: "2025-06-15",
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
const lines = formatCsv(rows).split("\n");
|
|
92
|
+
expect(lines).toHaveLength(2);
|
|
93
|
+
expect(lines[0]).toBe(
|
|
94
|
+
"session_path,project,model,input_tokens,output_tokens,total_tokens,cost,date",
|
|
95
|
+
);
|
|
96
|
+
expect(lines[1]).toContain('"my-project"');
|
|
97
|
+
expect(lines[1]).toContain("1000");
|
|
98
|
+
expect(lines[1]).toContain("500");
|
|
99
|
+
expect(lines[1]).toContain("1500");
|
|
100
|
+
expect(lines[1]).toContain("0.0525");
|
|
101
|
+
expect(lines[1]).toContain("2025-06-15");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("formats multiple rows", () => {
|
|
105
|
+
const rows: SessionRow[] = [
|
|
106
|
+
{
|
|
107
|
+
sessionPath: "/a.jsonl",
|
|
108
|
+
project: "proj-a",
|
|
109
|
+
model: "Opus",
|
|
110
|
+
inputTokens: 100,
|
|
111
|
+
outputTokens: 50,
|
|
112
|
+
totalTokens: 150,
|
|
113
|
+
cost: 0.005,
|
|
114
|
+
date: "2025-06-15",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
sessionPath: "/b.jsonl",
|
|
118
|
+
project: "proj-b",
|
|
119
|
+
model: "Sonnet",
|
|
120
|
+
inputTokens: 200,
|
|
121
|
+
outputTokens: 100,
|
|
122
|
+
totalTokens: 300,
|
|
123
|
+
cost: 0.002,
|
|
124
|
+
date: "2025-06-16",
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
const lines = formatCsv(rows).split("\n");
|
|
128
|
+
expect(lines).toHaveLength(3);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { analyzeSession, extractProjectName, getPrimaryModel } from "./analyzer.js";
|
|
2
|
+
import { parseJsonl } from "./parser.js";
|
|
3
|
+
import type { SessionResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export type OutputFormat = "html" | "json" | "csv";
|
|
6
|
+
|
|
7
|
+
export interface SessionRow {
|
|
8
|
+
sessionPath: string;
|
|
9
|
+
project: string;
|
|
10
|
+
model: string;
|
|
11
|
+
inputTokens: number;
|
|
12
|
+
outputTokens: number;
|
|
13
|
+
totalTokens: number;
|
|
14
|
+
cost: number;
|
|
15
|
+
date: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Approximate pricing per million tokens (as of 2025)
|
|
19
|
+
const COST_PER_MILLION: Record<string, { input: number; output: number }> = {
|
|
20
|
+
opus: { input: 15, output: 75 },
|
|
21
|
+
sonnet: { input: 3, output: 15 },
|
|
22
|
+
haiku: { input: 0.8, output: 4 },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function estimateCost(analysis: {
|
|
26
|
+
opusTokens: number;
|
|
27
|
+
sonnetTokens: number;
|
|
28
|
+
haikuTokens: number;
|
|
29
|
+
inputTokens: number;
|
|
30
|
+
outputTokens: number;
|
|
31
|
+
}): number {
|
|
32
|
+
const total = analysis.opusTokens + analysis.sonnetTokens + analysis.haikuTokens;
|
|
33
|
+
if (total === 0) return 0;
|
|
34
|
+
|
|
35
|
+
// Distribute input/output proportionally across models
|
|
36
|
+
let cost = 0;
|
|
37
|
+
for (const [model, tokens] of [
|
|
38
|
+
["opus", analysis.opusTokens],
|
|
39
|
+
["sonnet", analysis.sonnetTokens],
|
|
40
|
+
["haiku", analysis.haikuTokens],
|
|
41
|
+
] as const) {
|
|
42
|
+
if (tokens === 0) continue;
|
|
43
|
+
const fraction = tokens / total;
|
|
44
|
+
const inputShare = analysis.inputTokens * fraction;
|
|
45
|
+
const outputShare = analysis.outputTokens * fraction;
|
|
46
|
+
const rates = COST_PER_MILLION[model];
|
|
47
|
+
cost += (inputShare * rates.input + outputShare * rates.output) / 1_000_000;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return Math.round(cost * 10000) / 10000; // 4 decimal places
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build flat session rows from session results by parsing and analyzing each session.
|
|
55
|
+
*/
|
|
56
|
+
export function buildSessionRows(sessions: SessionResult[]): SessionRow[] {
|
|
57
|
+
return sessions.map((session) => {
|
|
58
|
+
const entries = parseJsonl(session.path);
|
|
59
|
+
const analysis = analyzeSession(entries);
|
|
60
|
+
const model = getPrimaryModel(analysis);
|
|
61
|
+
const project = extractProjectName(session.project);
|
|
62
|
+
const cost = estimateCost(analysis);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
sessionPath: session.path,
|
|
66
|
+
project,
|
|
67
|
+
model,
|
|
68
|
+
inputTokens: analysis.inputTokens,
|
|
69
|
+
outputTokens: analysis.outputTokens,
|
|
70
|
+
totalTokens: analysis.inputTokens + analysis.outputTokens,
|
|
71
|
+
cost,
|
|
72
|
+
date: session.date,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format session rows as JSON string.
|
|
79
|
+
*/
|
|
80
|
+
export function formatJson(rows: SessionRow[]): string {
|
|
81
|
+
return JSON.stringify(rows, null, 2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Format session rows as CSV string.
|
|
86
|
+
*/
|
|
87
|
+
export function formatCsv(rows: SessionRow[]): string {
|
|
88
|
+
const header = "session_path,project,model,input_tokens,output_tokens,total_tokens,cost,date";
|
|
89
|
+
const lines = rows.map(
|
|
90
|
+
(r) =>
|
|
91
|
+
`"${r.sessionPath}","${r.project}",${r.model},${r.inputTokens},${r.outputTokens},${r.totalTokens},${r.cost},${r.date}`,
|
|
92
|
+
);
|
|
93
|
+
return [header, ...lines].join("\n");
|
|
94
|
+
}
|
|
@@ -3,44 +3,29 @@ import { DateTime } from "luxon";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { run } from "@towles/shared";
|
|
7
7
|
import consola from "consola";
|
|
8
8
|
|
|
9
9
|
import { debugArg } from "../shared.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
parseJsonl,
|
|
19
|
-
startServer,
|
|
20
|
-
waitForShutdown,
|
|
21
|
-
} from "../../lib/graph/index.js";
|
|
10
|
+
import { buildAllSessionsTreemap, buildSessionTreemap } from "./treemap.js";
|
|
11
|
+
import type { BarChartData } from "./types.js";
|
|
12
|
+
import { buildBarChartData, findRecentSessions, findSessionPath } from "./sessions.js";
|
|
13
|
+
import { buildSessionRows, formatCsv, formatJson } from "./format.js";
|
|
14
|
+
import type { OutputFormat } from "./format.js";
|
|
15
|
+
import { generateTreemapHtml } from "./render.js";
|
|
16
|
+
import { openInBrowser, startServer, waitForShutdown } from "./server.js";
|
|
17
|
+
import { parseJsonl } from "./parser.js";
|
|
22
18
|
|
|
23
19
|
// Re-export public API for consumers and tests
|
|
24
|
-
export {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
findSessionPath,
|
|
34
|
-
generateTreemapHtml,
|
|
35
|
-
parseJsonl,
|
|
36
|
-
} from "../../lib/graph/index.js";
|
|
37
|
-
export type {
|
|
38
|
-
BarChartData,
|
|
39
|
-
BarChartDay,
|
|
40
|
-
ProjectBar,
|
|
41
|
-
SessionResult,
|
|
42
|
-
TreemapNode,
|
|
43
|
-
} from "../../lib/graph/index.js";
|
|
20
|
+
export { analyzeSession } from "./analyzer.js";
|
|
21
|
+
export { buildAllSessionsTreemap, buildSessionTreemap } from "./treemap.js";
|
|
22
|
+
export { buildBarChartData, findRecentSessions, findSessionPath } from "./sessions.js";
|
|
23
|
+
export { calculateCutoffMs, filterByDays, parseJsonl } from "./parser.js";
|
|
24
|
+
export { extractSessionLabel } from "./labels.js";
|
|
25
|
+
export { generateTreemapHtml } from "./render.js";
|
|
26
|
+
export { buildSessionRows, formatCsv, formatJson } from "./format.js";
|
|
27
|
+
export type { BarChartData, BarChartDay, ProjectBar, SessionResult, TreemapNode } from "./types.js";
|
|
28
|
+
export type { OutputFormat, SessionRow } from "./format.js";
|
|
44
29
|
|
|
45
30
|
export default defineCommand({
|
|
46
31
|
meta: { name: "graph", description: "Generate interactive HTML treemap from session token data" },
|
|
@@ -73,10 +58,22 @@ export default defineCommand({
|
|
|
73
58
|
description: "Filter to sessions from last N days (0=no limit, default: 7)",
|
|
74
59
|
default: "7",
|
|
75
60
|
},
|
|
61
|
+
format: {
|
|
62
|
+
type: "string" as const,
|
|
63
|
+
alias: "f",
|
|
64
|
+
description: "Output format: html (default), json, csv",
|
|
65
|
+
default: "html",
|
|
66
|
+
},
|
|
76
67
|
},
|
|
77
68
|
async run({ args }) {
|
|
78
69
|
const port = Number(args.port);
|
|
79
70
|
const days = Number(args.days);
|
|
71
|
+
const format = args.format as OutputFormat;
|
|
72
|
+
|
|
73
|
+
if (!["html", "json", "csv"].includes(format)) {
|
|
74
|
+
consola.error(`Invalid format "${format}". Use: html, json, csv`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
80
77
|
|
|
81
78
|
const projectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
82
79
|
if (!fs.existsSync(projectsDir)) {
|
|
@@ -85,11 +82,47 @@ export default defineCommand({
|
|
|
85
82
|
}
|
|
86
83
|
|
|
87
84
|
const sessionId = args.session;
|
|
85
|
+
|
|
86
|
+
// JSON/CSV output: flat session rows to stdout
|
|
87
|
+
if (format === "json" || format === "csv") {
|
|
88
|
+
const sessions = sessionId
|
|
89
|
+
? (() => {
|
|
90
|
+
const sessionPath = findSessionPath(projectsDir, sessionId);
|
|
91
|
+
if (!sessionPath) {
|
|
92
|
+
consola.error(`Session ${sessionId} not found`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
const stat = fs.statSync(sessionPath);
|
|
96
|
+
const project = path.basename(path.dirname(sessionPath));
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
sessionId,
|
|
100
|
+
path: sessionPath,
|
|
101
|
+
date: stat.mtime.toLocaleDateString("en-CA"),
|
|
102
|
+
tokens: 0,
|
|
103
|
+
project,
|
|
104
|
+
mtime: stat.mtimeMs,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
})()
|
|
108
|
+
: findRecentSessions(projectsDir, 500, days);
|
|
109
|
+
|
|
110
|
+
if (sessions.length === 0) {
|
|
111
|
+
consola.error("No sessions found");
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rows = buildSessionRows(sessions);
|
|
116
|
+
const output = format === "json" ? formatJson(rows) : formatCsv(rows);
|
|
117
|
+
process.stdout.write(output + "\n");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// HTML output (existing behavior)
|
|
88
122
|
let treemapData;
|
|
89
|
-
let barChartData = { days: []
|
|
123
|
+
let barChartData: BarChartData = { days: [] };
|
|
90
124
|
|
|
91
125
|
if (!sessionId) {
|
|
92
|
-
// All sessions mode
|
|
93
126
|
const sessions = findRecentSessions(projectsDir, 500, days);
|
|
94
127
|
if (sessions.length === 0) {
|
|
95
128
|
consola.error("No sessions found");
|
|
@@ -101,7 +134,6 @@ export default defineCommand({
|
|
|
101
134
|
treemapData = buildAllSessionsTreemap(sessions);
|
|
102
135
|
barChartData = buildBarChartData(sessions);
|
|
103
136
|
} else {
|
|
104
|
-
// Single session mode
|
|
105
137
|
const sessionPath = findSessionPath(projectsDir, sessionId);
|
|
106
138
|
if (!sessionPath) {
|
|
107
139
|
consola.error(`Session ${sessionId} not found`);
|
|
@@ -111,13 +143,10 @@ export default defineCommand({
|
|
|
111
143
|
consola.info(`📊 Generating treemap for session ${sessionId}...`);
|
|
112
144
|
const entries = parseJsonl(sessionPath);
|
|
113
145
|
treemapData = buildSessionTreemap(sessionId, entries);
|
|
114
|
-
// Bar chart not meaningful for single session, leave empty
|
|
115
146
|
}
|
|
116
147
|
|
|
117
|
-
// Generate HTML
|
|
118
148
|
const html = generateTreemapHtml(treemapData, barChartData);
|
|
119
149
|
|
|
120
|
-
// Write output file
|
|
121
150
|
const reportsDir = path.join(os.homedir(), ".claude", "reports");
|
|
122
151
|
if (!fs.existsSync(reportsDir)) {
|
|
123
152
|
fs.mkdirSync(reportsDir, { recursive: true });
|
|
@@ -146,13 +175,12 @@ export default defineCommand({
|
|
|
146
175
|
openInBrowser(url);
|
|
147
176
|
}
|
|
148
177
|
|
|
149
|
-
// Keep server running until Ctrl+C
|
|
150
178
|
await waitForShutdown(server);
|
|
151
179
|
consola.info("\n👋 Stopping server...");
|
|
152
180
|
} else if (args.open) {
|
|
153
181
|
consola.info("\n📈 Opening treemap...");
|
|
154
182
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
155
|
-
await
|
|
183
|
+
await run(openCmd, [outputPath]);
|
|
156
184
|
}
|
|
157
185
|
},
|
|
158
186
|
});
|
|
@@ -11,11 +11,11 @@ export function extractSessionLabel(entries: JournalEntry[], sessionId: string):
|
|
|
11
11
|
|
|
12
12
|
for (const entry of entries) {
|
|
13
13
|
// Extract metadata from any entry
|
|
14
|
-
if (!gitBranch &&
|
|
15
|
-
gitBranch =
|
|
14
|
+
if (!gitBranch && entry.gitBranch) {
|
|
15
|
+
gitBranch = entry.gitBranch;
|
|
16
16
|
}
|
|
17
|
-
if (!slug &&
|
|
18
|
-
slug =
|
|
17
|
+
if (!slug && entry.slug) {
|
|
18
|
+
slug = entry.slug;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
if (!entry.message) continue;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
|
-
import {
|
|
2
|
+
import { run } from "@towles/shared";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Start a local HTTP server to serve the generated HTML.
|
|
@@ -54,7 +54,7 @@ export async function startServer(
|
|
|
54
54
|
*/
|
|
55
55
|
export function openInBrowser(url: string): void {
|
|
56
56
|
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
57
|
-
|
|
57
|
+
run(openCmd, [url]);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Tests for graph command --days filtering and bar chart data
|
|
3
3
|
*/
|
|
4
4
|
import { describe, it, expect } from "vitest";
|
|
5
|
-
import { analyzeSession, calculateCutoffMs, filterByDays } from "
|
|
5
|
+
import { analyzeSession, calculateCutoffMs, filterByDays } from "./graph/index.js";
|
|
6
6
|
|
|
7
7
|
describe("graph --days filtering", () => {
|
|
8
8
|
describe("calculateCutoffMs", () => {
|
package/src/commands/install.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
loadClaudeSettings,
|
|
8
8
|
applyRecommendedSettings,
|
|
9
9
|
saveClaudeSettings,
|
|
10
|
-
} from "
|
|
10
|
+
} from "./claude-settings.js";
|
|
11
11
|
|
|
12
12
|
export default defineCommand({
|
|
13
13
|
meta: {
|
|
@@ -64,12 +64,12 @@ export default defineCommand({
|
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
async function ensureClaudePlugins(): Promise<void> {
|
|
67
|
-
const {
|
|
67
|
+
const { run } = await import("@towles/shared");
|
|
68
68
|
|
|
69
69
|
const requiredPlugins = [
|
|
70
70
|
{
|
|
71
71
|
id: "tt@towles-tool",
|
|
72
|
-
name: "
|
|
72
|
+
name: "core",
|
|
73
73
|
marketplaceUrl: "https://github.com/ChrisTowles/towles-tool",
|
|
74
74
|
marketplace: "towles-tool",
|
|
75
75
|
},
|
|
@@ -81,7 +81,7 @@ async function ensureClaudePlugins(): Promise<void> {
|
|
|
81
81
|
|
|
82
82
|
let installedIds = new Set<string>();
|
|
83
83
|
try {
|
|
84
|
-
const result = await
|
|
84
|
+
const result = await run("claude", ["plugin", "list", "--json"]);
|
|
85
85
|
const plugins: { id: string }[] = JSON.parse(result.stdout);
|
|
86
86
|
installedIds = new Set(plugins.map((p) => p.id));
|
|
87
87
|
} catch {
|
|
@@ -91,7 +91,7 @@ async function ensureClaudePlugins(): Promise<void> {
|
|
|
91
91
|
for (const plugin of requiredPlugins) {
|
|
92
92
|
if (plugin.marketplaceUrl && !installedIds.has(plugin.id)) {
|
|
93
93
|
try {
|
|
94
|
-
await
|
|
94
|
+
await run("claude", ["plugin", "marketplace", "add", plugin.marketplaceUrl]);
|
|
95
95
|
consola.log(colors.dim(` Added marketplace: ${plugin.marketplace}`));
|
|
96
96
|
} catch {
|
|
97
97
|
// marketplace may already be added
|
|
@@ -111,7 +111,7 @@ async function ensureClaudePlugins(): Promise<void> {
|
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
if (answer) {
|
|
114
|
-
const result = await
|
|
114
|
+
const result = await run("claude", ["plugin", "install", plugin.id, "--scope", "user"]);
|
|
115
115
|
if (result.exitCode === 0) {
|
|
116
116
|
consola.log(colors.green(`✓ ${plugin.name} installed`));
|
|
117
117
|
} else {
|
|
@@ -6,13 +6,10 @@ import consola from "consola";
|
|
|
6
6
|
import { colors } from "consola/utils";
|
|
7
7
|
import { withSettings, debugArg } from "../shared.js";
|
|
8
8
|
import { JOURNAL_TYPES } from "../../types/journal.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
generateJournalFileInfoByType,
|
|
14
|
-
openInEditor,
|
|
15
|
-
} from "../../lib/journal/index.js";
|
|
9
|
+
import { ensureDirectoryExists } from "./fs.js";
|
|
10
|
+
import { openInEditor } from "./editor.js";
|
|
11
|
+
import { createJournalContent, ensureTemplatesExist } from "./templates.js";
|
|
12
|
+
import { generateJournalFileInfoByType } from "./paths.js";
|
|
16
13
|
|
|
17
14
|
export default defineCommand({
|
|
18
15
|
meta: {
|
|
@@ -6,5 +6,7 @@ export default defineCommand({
|
|
|
6
6
|
"daily-notes": () => import("./daily-notes.js").then((m) => m.default),
|
|
7
7
|
note: () => import("./note.js").then((m) => m.default),
|
|
8
8
|
meeting: () => import("./meeting.js").then((m) => m.default),
|
|
9
|
+
search: () => import("./search.js").then((m) => m.default),
|
|
10
|
+
list: () => import("./list.js").then((m) => m.default),
|
|
9
11
|
},
|
|
10
12
|
});
|