@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.
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/packages/shared/package.json +15 -0
  28. package/packages/shared/src/git/exec.ts +41 -0
  29. package/{src/utils → packages/shared/src}/git/gh-cli-wrapper.ts +13 -18
  30. package/packages/shared/src/index.ts +8 -0
  31. package/packages/shared/tsconfig.json +16 -0
  32. package/src/cli.ts +1 -1
  33. package/src/commands/agentboard.ts +42 -59
  34. package/src/{lib → commands}/auto-claude/claude-cli.ts +1 -1
  35. package/src/commands/auto-claude/config-init-helpers.ts +79 -0
  36. package/src/commands/auto-claude/config-init.test.ts +137 -0
  37. package/src/commands/auto-claude/config-init.ts +159 -0
  38. package/src/{lib → commands}/auto-claude/config.ts +4 -8
  39. package/src/{lib → commands}/auto-claude/e2e.test.ts +6 -6
  40. package/src/commands/auto-claude/explain.test.ts +58 -0
  41. package/src/commands/auto-claude/explain.ts +97 -0
  42. package/src/commands/auto-claude/index.ts +37 -14
  43. package/src/{lib → commands}/auto-claude/labels.ts +1 -1
  44. package/src/commands/auto-claude/list.ts +5 -4
  45. package/src/{lib → commands}/auto-claude/pipeline-execution.test.ts +1 -1
  46. package/src/{lib → commands}/auto-claude/pipeline.ts +1 -3
  47. package/src/commands/auto-claude/retry.test.ts +2 -2
  48. package/src/commands/auto-claude/retry.ts +5 -5
  49. package/src/commands/auto-claude/shell.ts +3 -0
  50. package/src/commands/auto-claude/status.test.ts +2 -2
  51. package/src/commands/auto-claude/status.ts +4 -4
  52. package/src/{lib → commands}/auto-claude/steps/create-pr.ts +1 -3
  53. package/src/{lib → commands}/auto-claude/steps/fetch-issues.ts +1 -1
  54. package/src/{lib → commands}/auto-claude/steps/implement.ts +1 -2
  55. package/src/{lib → commands}/auto-claude/utils-execution.test.ts +6 -6
  56. package/src/{lib → commands}/auto-claude/utils.ts +10 -4
  57. package/src/{lib/install → commands}/claude-settings.ts +1 -1
  58. package/src/commands/config/config.test.ts +129 -0
  59. package/src/commands/config/index.ts +11 -0
  60. package/src/commands/config/reset.ts +53 -0
  61. package/src/commands/config/schema.ts +19 -0
  62. package/src/commands/{config.ts → config/show.ts} +2 -2
  63. package/src/commands/config/validate.ts +51 -0
  64. package/src/commands/doctor/checks.ts +167 -0
  65. package/src/commands/doctor/format.test.ts +63 -0
  66. package/src/commands/doctor/format.ts +5 -0
  67. package/src/commands/doctor/history.test.ts +161 -0
  68. package/src/commands/doctor/history.ts +130 -0
  69. package/src/commands/doctor.ts +80 -151
  70. package/src/commands/gh/branch-clean.ts +4 -4
  71. package/src/commands/gh/branch.test.ts +4 -5
  72. package/src/commands/gh/branch.ts +10 -5
  73. package/src/commands/gh/pr.ts +6 -7
  74. package/src/{lib → commands}/graph/analyzer.test.ts +4 -4
  75. package/src/commands/graph/format.test.ts +130 -0
  76. package/src/commands/graph/format.ts +94 -0
  77. package/src/commands/graph/index.ts +69 -41
  78. package/src/{lib → commands}/graph/labels.ts +4 -4
  79. package/src/{lib → commands}/graph/server.ts +2 -2
  80. package/src/{lib → commands}/graph/types.ts +2 -0
  81. package/src/commands/graph.test.ts +1 -1
  82. package/src/commands/install.ts +6 -6
  83. package/src/commands/journal/daily-notes.ts +4 -7
  84. package/src/{lib → commands}/journal/fs.ts +1 -1
  85. package/src/commands/journal/index.ts +2 -0
  86. package/src/commands/journal/list.test.ts +174 -0
  87. package/src/commands/journal/list.ts +213 -0
  88. package/src/commands/journal/meeting.ts +4 -7
  89. package/src/commands/journal/note.ts +4 -7
  90. package/src/{lib → commands}/journal/paths.ts +1 -1
  91. package/src/commands/journal/search.test.ts +156 -0
  92. package/src/commands/journal/search.ts +256 -0
  93. package/src/{lib → commands}/journal/templates.ts +1 -1
  94. package/src/config/settings.ts +35 -26
  95. package/plugins/tt-agentboard/bun.lock +0 -444
  96. package/plugins/tt-agentboard/packages/runtime/src/config.ts +0 -70
  97. package/plugins/tt-agentboard/packages/runtime/test/config.test.ts +0 -83
  98. package/plugins/tt-auto-claude/.claude-plugin/plugin.json +0 -8
  99. package/plugins/tt-auto-claude/commands/create-issue.md +0 -20
  100. package/plugins/tt-auto-claude/commands/list.md +0 -21
  101. package/plugins/tt-auto-claude/skills/auto-claude/SKILL.md +0 -71
  102. package/plugins/tt-core/promptfooconfig.interview-me.yaml +0 -155
  103. package/plugins/tt-core/promptfooconfig.refine-text.yaml +0 -242
  104. package/plugins/tt-core/promptfooconfig.tdd.yaml +0 -144
  105. package/plugins/tt-core/promptfooconfig.write-prd.yaml +0 -145
  106. package/src/commands/config.test.ts +0 -9
  107. package/src/lib/auto-claude/index.ts +0 -15
  108. package/src/lib/auto-claude/shell.ts +0 -6
  109. package/src/lib/graph/index.ts +0 -24
  110. package/src/lib/journal/index.ts +0 -11
  111. package/src/utils/git/exec.ts +0 -18
  112. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/build.ts +0 -0
  113. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/bunfig.toml +0 -0
  114. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/scripts/sessionizer.sh +0 -0
  115. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/DiffStats.tsx +0 -0
  116. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/components/SessionCard.tsx +0 -0
  117. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/detail-panel-height.ts +0 -0
  118. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/src/mux-context.ts +0 -0
  119. /package/{plugins/tt-agentboard → packages/agentboard}/apps/tui/tsconfig.json +0 -0
  120. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/package.json +0 -0
  121. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/client.ts +0 -0
  122. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/index.ts +0 -0
  123. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/src/provider.ts +0 -0
  124. /package/{plugins/tt-agentboard → packages/agentboard}/packages/mux-tmux/tsconfig.json +0 -0
  125. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/tracker.ts +0 -0
  126. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/amp.ts +0 -0
  127. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/codex.ts +0 -0
  128. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/agents/watchers/opencode.ts +0 -0
  129. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent-watcher.ts +0 -0
  130. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/agent.ts +0 -0
  131. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/index.ts +0 -0
  132. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/contracts/mux.ts +0 -0
  133. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/debug.ts +0 -0
  134. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/detect.ts +0 -0
  135. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/mux/registry.ts +0 -0
  136. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/context.ts +0 -0
  137. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/metadata-store.ts +0 -0
  138. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/port-scanner.ts +0 -0
  139. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/session-order.ts +0 -0
  140. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-manager.ts +0 -0
  141. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/server/sidebar-width-sync.ts +0 -0
  142. /package/{plugins/tt-agentboard → packages/agentboard}/packages/runtime/src/themes.ts +0 -0
  143. /package/{plugins/tt-agentboard → packages/agentboard}/tsconfig.json +0 -0
  144. /package/{plugins/tt-core → packages/core}/.claude-plugin/plugin.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
@@ -0,0 +1,174 @@
1
+ import { mkdirSync, writeFileSync, rmSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
5
+ import {
6
+ collectJournalEntries,
7
+ filterAndSortEntries,
8
+ formatSize,
9
+ formatDate,
10
+ renderTable,
11
+ } from "./list.js";
12
+ import type { JournalEntry } from "./list.js";
13
+ import { JOURNAL_TYPES } from "../../types/journal.js";
14
+
15
+ describe("journal list", () => {
16
+ let tmpDir: string;
17
+
18
+ beforeEach(() => {
19
+ tmpDir = path.join(tmpdir(), `journal-list-test-${Date.now()}`);
20
+ mkdirSync(tmpDir, { recursive: true });
21
+ });
22
+
23
+ afterEach(() => {
24
+ rmSync(tmpDir, { recursive: true, force: true });
25
+ });
26
+
27
+ describe("collectJournalEntries", () => {
28
+ it("collects entries with metadata", () => {
29
+ const dailyDir = path.join(tmpDir, "daily-notes");
30
+ mkdirSync(dailyDir, { recursive: true });
31
+ writeFileSync(path.join(dailyDir, "2026-03-15-saturday.md"), "hello world");
32
+
33
+ const files = [path.join(dailyDir, "2026-03-15-saturday.md")];
34
+ const entries = collectJournalEntries(files, tmpDir);
35
+
36
+ expect(entries).toHaveLength(1);
37
+ expect(entries[0].relativePath).toBe(path.join("daily-notes", "2026-03-15-saturday.md"));
38
+ expect(entries[0].type).toBe(JOURNAL_TYPES.DAILY_NOTES);
39
+ expect(entries[0].date).not.toBeNull();
40
+ expect(entries[0].date!.getFullYear()).toBe(2026);
41
+ expect(entries[0].size).toBeGreaterThan(0);
42
+ });
43
+
44
+ it("skips files that cannot be stat'd", () => {
45
+ const entries = collectJournalEntries(["/nonexistent/file.md"], tmpDir);
46
+ expect(entries).toHaveLength(0);
47
+ });
48
+ });
49
+
50
+ describe("filterAndSortEntries", () => {
51
+ const makeEntry = (overrides: Partial<JournalEntry>): JournalEntry => ({
52
+ filePath: "/tmp/file.md",
53
+ relativePath: "file.md",
54
+ type: null,
55
+ date: null,
56
+ size: 100,
57
+ ...overrides,
58
+ });
59
+
60
+ it("filters by type", () => {
61
+ const entries = [
62
+ makeEntry({ type: JOURNAL_TYPES.DAILY_NOTES, relativePath: "a.md" }),
63
+ makeEntry({ type: JOURNAL_TYPES.MEETING, relativePath: "b.md" }),
64
+ makeEntry({ type: JOURNAL_TYPES.NOTE, relativePath: "c.md" }),
65
+ ];
66
+
67
+ const result = filterAndSortEntries(entries, {
68
+ type: JOURNAL_TYPES.MEETING,
69
+ limit: 20,
70
+ sort: "date",
71
+ });
72
+ expect(result).toHaveLength(1);
73
+ expect(result[0].type).toBe(JOURNAL_TYPES.MEETING);
74
+ });
75
+
76
+ it("sorts by date descending", () => {
77
+ const entries = [
78
+ makeEntry({ date: new Date(2026, 0, 1), relativePath: "jan.md" }),
79
+ makeEntry({ date: new Date(2026, 2, 15), relativePath: "mar.md" }),
80
+ makeEntry({ date: new Date(2026, 1, 10), relativePath: "feb.md" }),
81
+ ];
82
+
83
+ const result = filterAndSortEntries(entries, { limit: 20, sort: "date" });
84
+ expect(result[0].relativePath).toBe("mar.md");
85
+ expect(result[1].relativePath).toBe("feb.md");
86
+ expect(result[2].relativePath).toBe("jan.md");
87
+ });
88
+
89
+ it("sorts by name alphabetically", () => {
90
+ const entries = [
91
+ makeEntry({ relativePath: "c.md" }),
92
+ makeEntry({ relativePath: "a.md" }),
93
+ makeEntry({ relativePath: "b.md" }),
94
+ ];
95
+
96
+ const result = filterAndSortEntries(entries, { limit: 20, sort: "name" });
97
+ expect(result[0].relativePath).toBe("a.md");
98
+ expect(result[1].relativePath).toBe("b.md");
99
+ expect(result[2].relativePath).toBe("c.md");
100
+ });
101
+
102
+ it("respects limit", () => {
103
+ const entries = [
104
+ makeEntry({ relativePath: "a.md" }),
105
+ makeEntry({ relativePath: "b.md" }),
106
+ makeEntry({ relativePath: "c.md" }),
107
+ ];
108
+
109
+ const result = filterAndSortEntries(entries, { limit: 2, sort: "name" });
110
+ expect(result).toHaveLength(2);
111
+ });
112
+
113
+ it("puts entries without dates last when sorting by date", () => {
114
+ const entries = [
115
+ makeEntry({ date: null, relativePath: "no-date.md" }),
116
+ makeEntry({ date: new Date(2026, 5, 1), relativePath: "with-date.md" }),
117
+ ];
118
+
119
+ const result = filterAndSortEntries(entries, { limit: 20, sort: "date" });
120
+ expect(result[0].relativePath).toBe("with-date.md");
121
+ expect(result[1].relativePath).toBe("no-date.md");
122
+ });
123
+ });
124
+
125
+ describe("formatSize", () => {
126
+ it("formats bytes", () => {
127
+ expect(formatSize(500)).toBe("500B");
128
+ });
129
+
130
+ it("formats kilobytes", () => {
131
+ expect(formatSize(2048)).toBe("2.0KB");
132
+ });
133
+
134
+ it("formats megabytes", () => {
135
+ expect(formatSize(1048576)).toBe("1.0MB");
136
+ });
137
+ });
138
+
139
+ describe("formatDate", () => {
140
+ it("formats a valid date", () => {
141
+ expect(formatDate(new Date(2026, 2, 15))).toBe("2026-03-15");
142
+ });
143
+
144
+ it("returns dash for null", () => {
145
+ expect(formatDate(null)).toBe("-");
146
+ });
147
+ });
148
+
149
+ describe("renderTable", () => {
150
+ it("renders a formatted table with header and rows", () => {
151
+ const entries: JournalEntry[] = [
152
+ {
153
+ filePath: "/tmp/daily-notes/2026-03-15.md",
154
+ relativePath: "daily-notes/2026-03-15.md",
155
+ type: JOURNAL_TYPES.DAILY_NOTES,
156
+ date: new Date(2026, 2, 15),
157
+ size: 1234,
158
+ },
159
+ ];
160
+
161
+ const table = renderTable(entries);
162
+ const lines = table.split("\n");
163
+ expect(lines).toHaveLength(2); // header + 1 row
164
+ expect(lines[0]).toContain("FILE");
165
+ expect(lines[0]).toContain("TYPE");
166
+ expect(lines[0]).toContain("DATE");
167
+ expect(lines[0]).toContain("SIZE");
168
+ expect(lines[1]).toContain("daily-notes/2026-03-15.md");
169
+ expect(lines[1]).toContain("daily-notes");
170
+ expect(lines[1]).toContain("2026-03-15");
171
+ expect(lines[1]).toContain("1.2KB");
172
+ });
173
+ });
174
+ });
@@ -0,0 +1,213 @@
1
+ import { statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { defineCommand } from "citty";
5
+ import consola from "consola";
6
+ import { colors } from "consola/utils";
7
+ import { withSettings, debugArg } from "../shared.js";
8
+ import type { JournalType } from "../../types/journal.js";
9
+ import { JOURNAL_TYPES } from "../../types/journal.js";
10
+ import { collectMarkdownFiles, inferTypeFromPath, extractDateFromFilename } from "./search.js";
11
+
12
+ export interface JournalEntry {
13
+ filePath: string;
14
+ relativePath: string;
15
+ type: JournalType | null;
16
+ date: Date | null;
17
+ size: number;
18
+ }
19
+
20
+ export interface ListOptions {
21
+ type?: JournalType;
22
+ limit: number;
23
+ sort: "date" | "name";
24
+ }
25
+
26
+ /**
27
+ * Collect journal entries with metadata from a list of file paths.
28
+ */
29
+ export function collectJournalEntries(files: string[], baseDir: string): JournalEntry[] {
30
+ const entries: JournalEntry[] = [];
31
+ for (const filePath of files) {
32
+ let size = 0;
33
+ try {
34
+ size = statSync(filePath).size;
35
+ } catch {
36
+ continue;
37
+ }
38
+ entries.push({
39
+ filePath,
40
+ relativePath: path.relative(baseDir, filePath),
41
+ type: inferTypeFromPath(filePath),
42
+ date: extractDateFromFilename(filePath),
43
+ size,
44
+ });
45
+ }
46
+ return entries;
47
+ }
48
+
49
+ /**
50
+ * Filter and sort journal entries based on options.
51
+ */
52
+ export function filterAndSortEntries(
53
+ entries: JournalEntry[],
54
+ options: ListOptions,
55
+ ): JournalEntry[] {
56
+ let filtered = entries;
57
+
58
+ if (options.type) {
59
+ filtered = filtered.filter((e) => e.type === options.type);
60
+ }
61
+
62
+ filtered.sort((a, b) => {
63
+ if (options.sort === "date") {
64
+ const dateA = a.date?.getTime() ?? 0;
65
+ const dateB = b.date?.getTime() ?? 0;
66
+ return dateB - dateA; // newest first
67
+ }
68
+ return a.relativePath.localeCompare(b.relativePath);
69
+ });
70
+
71
+ return filtered.slice(0, options.limit);
72
+ }
73
+
74
+ /**
75
+ * Format a file size in bytes to a human-readable string.
76
+ */
77
+ export function formatSize(bytes: number): string {
78
+ if (bytes < 1024) return `${bytes}B`;
79
+ const kb = bytes / 1024;
80
+ if (kb < 1024) return `${kb.toFixed(1)}KB`;
81
+ const mb = kb / 1024;
82
+ return `${mb.toFixed(1)}MB`;
83
+ }
84
+
85
+ /**
86
+ * Format a date as YYYY-MM-DD or return "-" if null.
87
+ */
88
+ export function formatDate(date: Date | null): string {
89
+ if (!date) return "-";
90
+ const y = date.getFullYear();
91
+ const m = String(date.getMonth() + 1).padStart(2, "0");
92
+ const d = String(date.getDate()).padStart(2, "0");
93
+ return `${y}-${m}-${d}`;
94
+ }
95
+
96
+ /**
97
+ * Render journal entries as a formatted table.
98
+ */
99
+ export function renderTable(entries: JournalEntry[]): string {
100
+ const header = { file: "FILE", type: "TYPE", date: "DATE", size: "SIZE" };
101
+ const rows = entries.map((e) => ({
102
+ file: e.relativePath,
103
+ type: e.type ?? "unknown",
104
+ date: formatDate(e.date),
105
+ size: formatSize(e.size),
106
+ }));
107
+
108
+ const allRows = [header, ...rows];
109
+ const colWidths = {
110
+ file: Math.max(...allRows.map((r) => r.file.length)),
111
+ type: Math.max(...allRows.map((r) => r.type.length)),
112
+ date: Math.max(...allRows.map((r) => r.date.length)),
113
+ size: Math.max(...allRows.map((r) => r.size.length)),
114
+ };
115
+
116
+ const lines: string[] = [];
117
+ for (const row of allRows) {
118
+ const line = [
119
+ row.file.padEnd(colWidths.file),
120
+ row.type.padEnd(colWidths.type),
121
+ row.date.padEnd(colWidths.date),
122
+ row.size.padStart(colWidths.size),
123
+ ].join(" ");
124
+ lines.push(line);
125
+ }
126
+
127
+ return lines.join("\n");
128
+ }
129
+
130
+ const VALID_TYPES = new Set<string>([
131
+ JOURNAL_TYPES.DAILY_NOTES,
132
+ JOURNAL_TYPES.MEETING,
133
+ JOURNAL_TYPES.NOTE,
134
+ ]);
135
+
136
+ export default defineCommand({
137
+ meta: {
138
+ name: "list",
139
+ description: "List recent journal entries",
140
+ },
141
+ args: {
142
+ debug: debugArg,
143
+ type: {
144
+ type: "string",
145
+ alias: "t",
146
+ description: "Filter by entry type: daily-notes, meeting, note",
147
+ },
148
+ limit: {
149
+ type: "string",
150
+ alias: "l",
151
+ description: "Maximum number of entries to show (default: 20)",
152
+ },
153
+ sort: {
154
+ type: "string",
155
+ alias: "s",
156
+ description: "Sort by: date, name (default: date)",
157
+ },
158
+ },
159
+ async run({ args }) {
160
+ const { settings } = await withSettings(args.debug);
161
+
162
+ try {
163
+ const baseFolder = settings.journalSettings.baseFolder;
164
+ const journalDir = path.join(baseFolder, "journal");
165
+
166
+ // Validate --type
167
+ let typeFilter: JournalType | undefined;
168
+ if (args.type) {
169
+ if (!VALID_TYPES.has(args.type)) {
170
+ consola.error(
171
+ `Invalid type "${args.type}". Must be one of: ${[...VALID_TYPES].join(", ")}`,
172
+ );
173
+ process.exit(1);
174
+ }
175
+ typeFilter = args.type as JournalType;
176
+ }
177
+
178
+ // Parse --limit
179
+ const limit = args.limit ? Number.parseInt(args.limit, 10) : 20;
180
+ if (Number.isNaN(limit) || limit < 1) {
181
+ consola.error(`Invalid limit "${args.limit}". Must be a positive integer.`);
182
+ process.exit(1);
183
+ }
184
+
185
+ // Validate --sort
186
+ const sort = (args.sort ?? "date") as "date" | "name";
187
+ if (sort !== "date" && sort !== "name") {
188
+ consola.error(`Invalid sort "${args.sort}". Must be one of: date, name`);
189
+ process.exit(1);
190
+ }
191
+
192
+ const files = collectMarkdownFiles(journalDir);
193
+ if (files.length === 0) {
194
+ consola.info(`No journal files found in ${colors.cyan(journalDir)}`);
195
+ return;
196
+ }
197
+
198
+ const entries = collectJournalEntries(files, baseFolder);
199
+ const result = filterAndSortEntries(entries, { type: typeFilter, limit, sort });
200
+
201
+ if (result.length === 0) {
202
+ consola.info("No matching journal entries found.");
203
+ return;
204
+ }
205
+
206
+ consola.info(`Showing ${colors.green(String(result.length))} journal entries:\n`);
207
+ console.log(renderTable(result));
208
+ } catch (error) {
209
+ consola.error("Failed to list journal entries:", error);
210
+ process.exit(1);
211
+ }
212
+ },
213
+ });
@@ -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
- createMeetingContent,
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 { createMeetingContent, ensureTemplatesExist } from "./templates.js";
12
+ import { generateJournalFileInfoByType } from "./paths.js";
16
13
 
17
14
  export default defineCommand({
18
15
  meta: { name: "meeting", description: "Structured meeting notes with agenda and action items" },
@@ -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
- createNoteContent,
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 { createNoteContent, ensureTemplatesExist } from "./templates.js";
12
+ import { generateJournalFileInfoByType } from "./paths.js";
16
13
 
17
14
  export default defineCommand({
18
15
  meta: { name: "note", description: "General-purpose notes with structured sections" },
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import consola from "consola";
3
3
  import { DateTime } from "luxon";
4
- import { getMondayOfWeek } from "../../utils/date-utils.js";
4
+ import { getMondayOfWeek } from "@towles/shared";
5
5
  import type { JournalSettings } from "../../config/settings.js";
6
6
  import { JOURNAL_TYPES } from "../../types/journal.js";
7
7
  import type { JournalType } from "../../types/journal.js";
@@ -0,0 +1,156 @@
1
+ import { mkdirSync, writeFileSync, rmSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
5
+ import {
6
+ collectMarkdownFiles,
7
+ inferTypeFromPath,
8
+ extractDateFromFilename,
9
+ searchJournalFiles,
10
+ parseDateRange,
11
+ } from "./search.js";
12
+ import { JOURNAL_TYPES } from "../../types/journal.js";
13
+
14
+ describe("journal search", () => {
15
+ let tmpDir: string;
16
+
17
+ beforeEach(() => {
18
+ tmpDir = path.join(tmpdir(), `journal-search-test-${Date.now()}`);
19
+ mkdirSync(tmpDir, { recursive: true });
20
+ });
21
+
22
+ afterEach(() => {
23
+ rmSync(tmpDir, { recursive: true, force: true });
24
+ });
25
+
26
+ describe("collectMarkdownFiles", () => {
27
+ it("collects .md files recursively", () => {
28
+ mkdirSync(path.join(tmpDir, "sub"), { recursive: true });
29
+ writeFileSync(path.join(tmpDir, "a.md"), "hello");
30
+ writeFileSync(path.join(tmpDir, "b.txt"), "ignored");
31
+ writeFileSync(path.join(tmpDir, "sub", "c.md"), "nested");
32
+
33
+ const files = collectMarkdownFiles(tmpDir);
34
+ expect(files).toHaveLength(2);
35
+ expect(files.some((f) => f.endsWith("a.md"))).toBe(true);
36
+ expect(files.some((f) => f.endsWith("c.md"))).toBe(true);
37
+ });
38
+
39
+ it("returns empty for nonexistent directory", () => {
40
+ expect(collectMarkdownFiles("/nonexistent-dir-12345")).toEqual([]);
41
+ });
42
+ });
43
+
44
+ describe("inferTypeFromPath", () => {
45
+ it("detects daily-notes", () => {
46
+ expect(inferTypeFromPath("/journal/2026/01/daily-notes/file.md")).toBe(
47
+ JOURNAL_TYPES.DAILY_NOTES,
48
+ );
49
+ });
50
+
51
+ it("detects meeting", () => {
52
+ expect(inferTypeFromPath("/journal/2026/01/meetings/standup.md")).toBe(JOURNAL_TYPES.MEETING);
53
+ });
54
+
55
+ it("detects note", () => {
56
+ expect(inferTypeFromPath("/journal/2026/01/notes/idea.md")).toBe(JOURNAL_TYPES.NOTE);
57
+ });
58
+
59
+ it("returns null for unknown", () => {
60
+ expect(inferTypeFromPath("/journal/2026/01/other/file.md")).toBeNull();
61
+ });
62
+ });
63
+
64
+ describe("extractDateFromFilename", () => {
65
+ it("extracts date from standard filename", () => {
66
+ const date = extractDateFromFilename("/path/2026-03-15-standup.md");
67
+ expect(date).not.toBeNull();
68
+ expect(date!.getFullYear()).toBe(2026);
69
+ expect(date!.getMonth()).toBe(2); // 0-indexed
70
+ expect(date!.getDate()).toBe(15);
71
+ });
72
+
73
+ it("returns null when no date pattern", () => {
74
+ expect(extractDateFromFilename("/path/readme.md")).toBeNull();
75
+ });
76
+ });
77
+
78
+ describe("searchJournalFiles", () => {
79
+ it("finds matching lines with context", () => {
80
+ const file = path.join(tmpDir, "2026-01-10-test.md");
81
+ writeFileSync(file, "line one\nline two\nfind me here\nline four\nline five");
82
+
83
+ const matches = searchJournalFiles([file], { query: "find me" });
84
+ expect(matches).toHaveLength(1);
85
+ expect(matches[0].lineNumber).toBe(3);
86
+ expect(matches[0].line).toBe("find me here");
87
+ expect(matches[0].context).toHaveLength(5); // 2 before + match + 2 after
88
+ });
89
+
90
+ it("is case-insensitive", () => {
91
+ const file = path.join(tmpDir, "2026-01-10-test.md");
92
+ writeFileSync(file, "Hello World\nfoo bar");
93
+
94
+ const matches = searchJournalFiles([file], { query: "hello" });
95
+ expect(matches).toHaveLength(1);
96
+ });
97
+
98
+ it("filters by type", () => {
99
+ const dailyDir = path.join(tmpDir, "daily-notes");
100
+ const notesDir = path.join(tmpDir, "notes");
101
+ mkdirSync(dailyDir, { recursive: true });
102
+ mkdirSync(notesDir, { recursive: true });
103
+
104
+ const dailyFile = path.join(dailyDir, "2026-01-10-daily.md");
105
+ const noteFile = path.join(notesDir, "2026-01-10-idea.md");
106
+ writeFileSync(dailyFile, "keyword here");
107
+ writeFileSync(noteFile, "keyword here too");
108
+
109
+ const matches = searchJournalFiles([dailyFile, noteFile], {
110
+ query: "keyword",
111
+ type: JOURNAL_TYPES.DAILY_NOTES,
112
+ });
113
+ expect(matches).toHaveLength(1);
114
+ expect(matches[0].filePath).toBe(dailyFile);
115
+ });
116
+
117
+ it("filters by date range", () => {
118
+ const oldFile = path.join(tmpDir, "2025-06-01-old.md");
119
+ const newFile = path.join(tmpDir, "2026-02-15-new.md");
120
+ writeFileSync(oldFile, "keyword");
121
+ writeFileSync(newFile, "keyword");
122
+
123
+ const matches = searchJournalFiles([oldFile, newFile], {
124
+ query: "keyword",
125
+ startDate: new Date(2026, 0, 1),
126
+ endDate: new Date(2026, 11, 31),
127
+ });
128
+ expect(matches).toHaveLength(1);
129
+ expect(matches[0].filePath).toBe(newFile);
130
+ });
131
+
132
+ it("returns empty when no matches", () => {
133
+ const file = path.join(tmpDir, "2026-01-10-test.md");
134
+ writeFileSync(file, "nothing relevant");
135
+
136
+ const matches = searchJournalFiles([file], { query: "xyznotfound" });
137
+ expect(matches).toHaveLength(0);
138
+ });
139
+ });
140
+
141
+ describe("parseDateRange", () => {
142
+ it("parses valid range", () => {
143
+ const { startDate, endDate } = parseDateRange("2026-01-01..2026-03-01");
144
+ expect(startDate.getFullYear()).toBe(2026);
145
+ expect(endDate.getMonth()).toBe(2); // March = 2 (0-indexed)
146
+ });
147
+
148
+ it("throws on invalid format", () => {
149
+ expect(() => parseDateRange("invalid")).toThrow("Invalid date range format");
150
+ });
151
+
152
+ it("throws TypeError on invalid dates", () => {
153
+ expect(() => parseDateRange("notadate..alsonot")).toThrow(TypeError);
154
+ });
155
+ });
156
+ });