@towles/tool 0.0.108 → 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.
Files changed (190) hide show
  1. package/package.json +9 -4
  2. package/{plugins/tt-agentboard → packages/agentboard}/README.md +1 -1
  3. package/{plugins/tt-agentboard → packages/agentboard}/apps/server/package.json +2 -1
  4. package/{plugins/tt-agentboard → packages/agentboard}/apps/server/src/main.ts +6 -20
  5. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/package.json +4 -0
  6. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DetailPanel.tsx +3 -2
  7. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/StatusBar.tsx +35 -0
  8. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/constants.ts +1 -0
  9. package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/index.tsx +204 -225
  10. package/packages/agentboard/apps/tui/src/session-status.test.ts +70 -0
  11. package/packages/agentboard/apps/tui/src/session-status.ts +19 -0
  12. package/{plugins/tt-agentboard → packages/agentboard}/package.json +2 -6
  13. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/package.json +3 -0
  14. package/{plugins/tt-agentboard/packages/runtime/test → packages/agentboard/packages/runtime/src/agents}/tracker.test.ts +2 -2
  15. package/packages/agentboard/packages/runtime/src/agents/watchers/claude-code.test.ts +63 -0
  16. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/claude-code.ts +26 -2
  17. package/packages/agentboard/packages/runtime/src/config.test.ts +107 -0
  18. package/packages/agentboard/packages/runtime/src/config.ts +80 -0
  19. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/index.ts +1 -1
  20. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/plugins/loader.ts +1 -33
  21. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/git-info.ts +3 -2
  22. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/index.ts +23 -37
  23. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/launcher.ts +6 -18
  24. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/pane-scanner.ts +6 -0
  25. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/shared.ts +2 -0
  26. package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/tsconfig.json +1 -1
  27. package/{plugins/tt-core → packages/core}/.claude-plugin/plugin.json +1 -1
  28. package/packages/shared/package.json +15 -0
  29. package/packages/shared/src/git/exec.ts +41 -0
  30. package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.ts +13 -18
  31. package/packages/shared/src/index.ts +8 -0
  32. package/packages/shared/tsconfig.json +16 -0
  33. package/src/cli.ts +3 -2
  34. package/src/commands/agentboard.ts +42 -59
  35. package/src/{lib → commands}/auto-claude/claude-cli.ts +1 -1
  36. package/src/commands/auto-claude/config-init-helpers.ts +79 -0
  37. package/src/commands/auto-claude/config-init.test.ts +137 -0
  38. package/src/commands/auto-claude/config-init.ts +159 -0
  39. package/src/{lib → commands}/auto-claude/config.ts +4 -8
  40. package/src/{lib → commands}/auto-claude/e2e.test.ts +6 -6
  41. package/src/commands/auto-claude/explain.test.ts +58 -0
  42. package/src/commands/auto-claude/explain.ts +97 -0
  43. package/src/commands/auto-claude/index.ts +37 -14
  44. package/src/{lib → commands}/auto-claude/labels.ts +1 -1
  45. package/src/commands/auto-claude/list.ts +5 -4
  46. package/src/{lib → commands}/auto-claude/pipeline-execution.test.ts +1 -1
  47. package/src/{lib → commands}/auto-claude/pipeline.ts +1 -3
  48. package/src/commands/auto-claude/retry.test.ts +2 -2
  49. package/src/commands/auto-claude/retry.ts +5 -5
  50. package/src/commands/auto-claude/shell.ts +3 -0
  51. package/src/commands/auto-claude/status.test.ts +2 -2
  52. package/src/commands/auto-claude/status.ts +4 -4
  53. package/src/{lib → commands}/auto-claude/steps/create-pr.ts +1 -3
  54. package/src/{lib → commands}/auto-claude/steps/fetch-issues.ts +1 -1
  55. package/src/{lib → commands}/auto-claude/steps/implement.ts +1 -2
  56. package/src/{lib → commands}/auto-claude/utils-execution.test.ts +6 -6
  57. package/src/{lib → commands}/auto-claude/utils.ts +10 -4
  58. package/src/{lib/install → commands}/claude-settings.ts +1 -1
  59. package/src/commands/config/config.test.ts +129 -0
  60. package/src/commands/config/index.ts +11 -0
  61. package/src/commands/config/reset.ts +53 -0
  62. package/src/commands/config/schema.ts +19 -0
  63. package/src/commands/{config.ts → config/show.ts} +2 -2
  64. package/src/commands/config/validate.ts +51 -0
  65. package/src/commands/doctor/checks.ts +167 -0
  66. package/src/commands/doctor/format.test.ts +63 -0
  67. package/src/commands/doctor/format.ts +5 -0
  68. package/src/commands/doctor/history.test.ts +161 -0
  69. package/src/commands/doctor/history.ts +130 -0
  70. package/src/commands/doctor.ts +80 -151
  71. package/src/commands/gh/branch-clean.ts +4 -4
  72. package/src/commands/gh/branch.test.ts +4 -5
  73. package/src/commands/gh/branch.ts +10 -5
  74. package/src/commands/gh/pr.ts +6 -7
  75. package/src/{lib → commands}/graph/analyzer.test.ts +4 -4
  76. package/src/commands/graph/format.test.ts +130 -0
  77. package/src/commands/graph/format.ts +94 -0
  78. package/src/commands/graph/index.ts +69 -41
  79. package/src/{lib → commands}/graph/labels.ts +4 -4
  80. package/src/{lib → commands}/graph/server.ts +2 -2
  81. package/src/{lib → commands}/graph/types.ts +2 -0
  82. package/src/commands/graph.test.ts +1 -1
  83. package/src/commands/install.ts +6 -6
  84. package/src/commands/journal/daily-notes.ts +4 -7
  85. package/src/{lib → commands}/journal/fs.ts +1 -1
  86. package/src/commands/journal/index.ts +2 -0
  87. package/src/commands/journal/list.test.ts +174 -0
  88. package/src/commands/journal/list.ts +213 -0
  89. package/src/commands/journal/meeting.ts +4 -7
  90. package/src/commands/journal/note.ts +4 -7
  91. package/src/{lib → commands}/journal/paths.ts +1 -1
  92. package/src/commands/journal/search.test.ts +156 -0
  93. package/src/commands/journal/search.ts +256 -0
  94. package/src/{lib → commands}/journal/templates.ts +1 -1
  95. package/src/config/settings.ts +35 -26
  96. package/plugins/tt-agentboard/bun.lock +0 -444
  97. package/plugins/tt-agentboard/packages/runtime/src/config.ts +0 -70
  98. package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +0 -83
  99. package/plugins/tt-auto-claude/.claude-plugin/plugin.json +0 -8
  100. package/plugins/tt-auto-claude/commands/create-issue.md +0 -20
  101. package/plugins/tt-auto-claude/commands/list.md +0 -21
  102. package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +0 -71
  103. package/plugins/tt-core/promptfooconfig.interview-me.yaml +0 -155
  104. package/plugins/tt-core/promptfooconfig.refine-text.yaml +0 -242
  105. package/plugins/tt-core/promptfooconfig.tdd.yaml +0 -144
  106. package/plugins/tt-core/promptfooconfig.write-prd.yaml +0 -145
  107. package/src/commands/config.test.ts +0 -9
  108. package/src/lib/auto-claude/index.ts +0 -15
  109. package/src/lib/auto-claude/shell.ts +0 -6
  110. package/src/lib/graph/index.ts +0 -24
  111. package/src/lib/journal/index.ts +0 -11
  112. package/src/utils/git/exec.ts +0 -18
  113. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/build.ts +0 -0
  114. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/bunfig.toml +0 -0
  115. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/scripts/sessionizer.sh +0 -0
  116. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DiffStats.tsx +0 -0
  117. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/SessionCard.tsx +0 -0
  118. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/detail-panel-height.ts +0 -0
  119. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/mux-context.ts +0 -0
  120. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/tsconfig.json +0 -0
  121. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/package.json +0 -0
  122. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/client.ts +0 -0
  123. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/index.ts +0 -0
  124. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/provider.ts +0 -0
  125. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/tsconfig.json +0 -0
  126. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/tracker.ts +0 -0
  127. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/amp.ts +0 -0
  128. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/codex.ts +0 -0
  129. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/opencode.ts +0 -0
  130. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent-watcher.ts +0 -0
  131. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent.ts +0 -0
  132. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/index.ts +0 -0
  133. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/mux.ts +0 -0
  134. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/debug.ts +0 -0
  135. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/detect.ts +0 -0
  136. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/registry.ts +0 -0
  137. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/context.ts +0 -0
  138. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/metadata-store.ts +0 -0
  139. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/port-scanner.ts +0 -0
  140. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/session-order.ts +0 -0
  141. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-manager.ts +0 -0
  142. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-width-sync.ts +0 -0
  143. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/themes.ts +0 -0
  144. /package/{plugins/tt-agentboard → packages/agentboard}/tsconfig.json +0 -0
  145. /package/{plugins/tt-core → packages/core}/README.md +0 -0
  146. /package/{plugins/tt-core → packages/core}/commands/improve-architecture.md +0 -0
  147. /package/{plugins/tt-core → packages/core}/commands/interview-me.md +0 -0
  148. /package/{plugins/tt-core → packages/core}/commands/prd-to-issues.md +0 -0
  149. /package/{plugins/tt-core → packages/core}/commands/refine-text.md +0 -0
  150. /package/{plugins/tt-core → packages/core}/commands/task.md +0 -0
  151. /package/{plugins/tt-core → packages/core}/commands/tdd.md +0 -0
  152. /package/{plugins/tt-core → packages/core}/commands/write-prd.md +0 -0
  153. /package/{plugins/tt-core → packages/core}/skills/towles-tool/SKILL.md +0 -0
  154. /package/{src/utils → packages/shared/src}/date-utils.test.ts +0 -0
  155. /package/{src/utils → packages/shared/src}/date-utils.ts +0 -0
  156. /package/{src/utils → packages/shared/src}/fs.ts +0 -0
  157. /package/{src/utils → packages/shared/src}/git/branch-name.test.ts +0 -0
  158. /package/{src/utils → packages/shared/src}/git/branch-name.ts +0 -0
  159. /package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.test.ts +0 -0
  160. /package/{src/utils → packages/shared/src}/render.test.ts +0 -0
  161. /package/{src/utils → packages/shared/src}/render.ts +0 -0
  162. /package/src/{lib → commands}/auto-claude/config.test.ts +0 -0
  163. /package/src/{lib → commands}/auto-claude/labels.test.ts +0 -0
  164. /package/src/{lib → commands}/auto-claude/pipeline.test.ts +0 -0
  165. /package/src/{lib → commands}/auto-claude/prompt-templates/01_plan.prompt.md +0 -0
  166. /package/src/{lib → commands}/auto-claude/prompt-templates/02_implement.prompt.md +0 -0
  167. /package/src/{lib → commands}/auto-claude/prompt-templates/03_simplify.prompt.md +0 -0
  168. /package/src/{lib → commands}/auto-claude/prompt-templates/04_review.prompt.md +0 -0
  169. /package/src/{lib → commands}/auto-claude/prompt-templates/CLAUDE.md +0 -0
  170. /package/src/{lib → commands}/auto-claude/prompt-templates/index.test.ts +0 -0
  171. /package/src/{lib → commands}/auto-claude/prompt-templates/index.ts +0 -0
  172. /package/src/{lib → commands}/auto-claude/run-claude.test.ts +0 -0
  173. /package/src/{lib → commands}/auto-claude/spawn-claude.ts +0 -0
  174. /package/src/{lib → commands}/auto-claude/steps/simple-steps.ts +0 -0
  175. /package/src/{lib → commands}/auto-claude/steps/steps.test.ts +0 -0
  176. /package/src/{lib → commands}/auto-claude/stream-parser.test.ts +0 -0
  177. /package/src/{lib → commands}/auto-claude/stream-parser.ts +0 -0
  178. /package/src/{lib → commands}/auto-claude/templates.test.ts +0 -0
  179. /package/src/{lib → commands}/auto-claude/templates.ts +0 -0
  180. /package/src/{lib → commands}/auto-claude/test-helpers.ts +0 -0
  181. /package/src/{lib → commands}/auto-claude/utils.test.ts +0 -0
  182. /package/src/{lib → commands}/graph/analyzer.ts +0 -0
  183. /package/src/{lib → commands}/graph/graph-template.html +0 -0
  184. /package/src/{lib → commands}/graph/parser.test.ts +0 -0
  185. /package/src/{lib → commands}/graph/parser.ts +0 -0
  186. /package/src/{lib → commands}/graph/render.ts +0 -0
  187. /package/src/{lib → commands}/graph/sessions.ts +0 -0
  188. /package/src/{lib → commands}/graph/tools.ts +0 -0
  189. /package/src/{lib → commands}/graph/treemap.ts +0 -0
  190. /package/src/{lib → commands}/journal/editor.ts +0 -0
@@ -1,10 +1,9 @@
1
1
  import { defineCommand } from "citty";
2
- import { x } from "tinyexec";
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 x("git", ["branch", "--show-current"]);
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 x("git", ["log", `${args.base}..HEAD`, "--pretty=format:%s"]);
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 x("git", ["status", "-sb"]);
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 x("git", ["push", "-u", "origin", currentBranch]);
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 x("gh", prArgs);
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" }) as any];
170
- (entries[0] as any).gitBranch = "feat/new-feature";
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() as any];
213
- (entries[0] as any).slug = "my-slug";
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 { x } from "tinyexec";
6
+ import { run } from "@towles/shared";
7
7
  import consola from "consola";
8
8
 
9
9
  import { debugArg } from "../shared.js";
10
- import {
11
- buildAllSessionsTreemap,
12
- buildBarChartData,
13
- buildSessionTreemap,
14
- findRecentSessions,
15
- findSessionPath,
16
- generateTreemapHtml,
17
- openInBrowser,
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
- analyzeSession,
26
- buildAllSessionsTreemap,
27
- buildBarChartData,
28
- buildSessionTreemap,
29
- calculateCutoffMs,
30
- extractSessionLabel,
31
- filterByDays,
32
- findRecentSessions,
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: [] as any[] };
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 x(openCmd, [outputPath]);
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 && (entry as any).gitBranch) {
15
- gitBranch = (entry as any).gitBranch;
14
+ if (!gitBranch && entry.gitBranch) {
15
+ gitBranch = entry.gitBranch;
16
16
  }
17
- if (!slug && (entry as any).slug) {
18
- slug = (entry as any).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 { x } from "tinyexec";
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
- x(openCmd, [url]);
57
+ run(openCmd, [url]);
58
58
  }
59
59
 
60
60
  /**
@@ -23,6 +23,8 @@ export interface JournalEntry {
23
23
  content?: ContentBlock[] | string;
24
24
  };
25
25
  uuid?: string;
26
+ gitBranch?: string;
27
+ slug?: string;
26
28
  }
27
29
 
28
30
  export interface ToolData {
@@ -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 "../lib/graph/index.js";
5
+ import { analyzeSession, calculateCutoffMs, filterByDays } from "./graph/index.js";
6
6
 
7
7
  describe("graph --days filtering", () => {
8
8
  describe("calculateCutoffMs", () => {
@@ -7,7 +7,7 @@ import {
7
7
  loadClaudeSettings,
8
8
  applyRecommendedSettings,
9
9
  saveClaudeSettings,
10
- } from "../lib/install/claude-settings.js";
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 { x } = await import("tinyexec");
67
+ const { run } = await import("@towles/shared");
68
68
 
69
69
  const requiredPlugins = [
70
70
  {
71
71
  id: "tt@towles-tool",
72
- name: "tt-core",
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 x("claude", ["plugin", "list", "--json"]);
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 x("claude", ["plugin", "marketplace", "add", plugin.marketplaceUrl]);
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 x("claude", ["plugin", "install", plugin.id, "--scope", "user"]);
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
- createJournalContent,
11
- ensureDirectoryExists,
12
- ensureTemplatesExist,
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: {
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import consola from "consola";
3
3
  import { colors } from "consola/utils";
4
- import { ensureDir } from "../../utils/fs.js";
4
+ import { ensureDir } from "@towles/shared";
5
5
 
6
6
  /**
7
7
  * Create journal directory if it doesn't exist
@@ -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
  });