@towles/tool 0.0.62 → 0.0.64

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 (83) hide show
  1. package/package.json +50 -57
  2. package/src/commands/agentboard.ts +176 -0
  3. package/src/commands/{auto-claude.ts → auto-claude/index.ts} +18 -28
  4. package/src/commands/auto-claude/list.ts +114 -0
  5. package/src/commands/auto-claude/retry.test.ts +138 -0
  6. package/src/commands/auto-claude/retry.ts +139 -0
  7. package/src/commands/auto-claude/status.test.ts +147 -0
  8. package/src/commands/auto-claude/status.ts +123 -0
  9. package/src/commands/base.ts +7 -2
  10. package/src/commands/config.ts +5 -7
  11. package/src/commands/doctor.ts +111 -12
  12. package/src/commands/gh/branch.ts +4 -4
  13. package/src/commands/gh/pr.ts +1 -0
  14. package/src/commands/graph/index.ts +169 -0
  15. package/src/commands/graph.test.ts +1 -1
  16. package/src/commands/install.ts +40 -68
  17. package/src/commands/journal/daily-notes.ts +3 -3
  18. package/src/commands/journal/meeting.ts +3 -3
  19. package/src/commands/journal/note.ts +3 -3
  20. package/src/lib/auto-claude/claude-cli.ts +183 -0
  21. package/src/lib/auto-claude/config.test.ts +6 -8
  22. package/src/lib/auto-claude/config.ts +3 -4
  23. package/src/lib/auto-claude/index.ts +2 -3
  24. package/src/lib/auto-claude/labels.test.ts +85 -0
  25. package/src/lib/auto-claude/labels.ts +42 -0
  26. package/src/lib/auto-claude/pipeline-execution.test.ts +129 -33
  27. package/src/lib/auto-claude/pipeline.test.ts +2 -2
  28. package/src/lib/auto-claude/pipeline.ts +120 -36
  29. package/src/lib/auto-claude/prompt-templates/01_plan.prompt.md +68 -0
  30. package/src/lib/auto-claude/prompt-templates/{05_implement.prompt.md → 02_implement.prompt.md} +3 -2
  31. package/src/lib/auto-claude/prompt-templates/03_simplify.prompt.md +52 -0
  32. package/src/lib/auto-claude/prompt-templates/{06_review.prompt.md → 04_review.prompt.md} +29 -6
  33. package/src/lib/auto-claude/prompt-templates/index.test.ts +9 -42
  34. package/src/lib/auto-claude/prompt-templates/index.ts +13 -28
  35. package/src/lib/auto-claude/run-claude.test.ts +48 -68
  36. package/src/lib/auto-claude/shell.ts +6 -0
  37. package/src/lib/auto-claude/steps/create-pr.ts +89 -25
  38. package/src/lib/auto-claude/steps/fetch-issues.ts +4 -1
  39. package/src/lib/auto-claude/steps/implement.ts +9 -16
  40. package/src/lib/auto-claude/steps/simple-steps.ts +34 -0
  41. package/src/lib/auto-claude/steps/steps.test.ts +68 -63
  42. package/src/lib/auto-claude/templates.test.ts +91 -0
  43. package/src/lib/auto-claude/templates.ts +34 -0
  44. package/src/lib/auto-claude/test-helpers.ts +2 -1
  45. package/src/lib/auto-claude/utils-execution.test.ts +9 -57
  46. package/src/lib/auto-claude/utils.test.ts +5 -9
  47. package/src/lib/auto-claude/utils.ts +27 -253
  48. package/src/lib/graph/analyzer.test.ts +451 -0
  49. package/src/lib/graph/analyzer.ts +165 -0
  50. package/src/lib/graph/index.ts +24 -0
  51. package/src/lib/graph/labels.ts +87 -0
  52. package/src/lib/graph/parser.test.ts +150 -0
  53. package/src/lib/graph/parser.ts +65 -0
  54. package/src/lib/graph/render.ts +25 -0
  55. package/src/lib/graph/server.ts +70 -0
  56. package/src/lib/graph/sessions.ts +104 -0
  57. package/src/lib/graph/tools.ts +90 -0
  58. package/src/lib/graph/treemap.ts +211 -0
  59. package/src/lib/graph/types.ts +80 -0
  60. package/src/lib/install/claude-settings.ts +64 -0
  61. package/src/lib/journal/editor.ts +33 -0
  62. package/src/lib/journal/fs.ts +13 -0
  63. package/src/lib/journal/index.ts +11 -0
  64. package/src/lib/journal/paths.ts +106 -0
  65. package/src/lib/journal/{utils.ts → templates.ts} +3 -151
  66. package/src/utils/fs.ts +19 -0
  67. package/src/utils/git/exec.ts +18 -0
  68. package/src/utils/git/gh-cli-wrapper.test.ts +47 -8
  69. package/src/utils/git/gh-cli-wrapper.ts +31 -19
  70. package/src/utils/render.ts +3 -1
  71. package/src/commands/graph.ts +0 -970
  72. package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +0 -21
  73. package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +0 -27
  74. package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +0 -15
  75. package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +0 -35
  76. package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +0 -30
  77. package/src/lib/auto-claude/steps/plan-annotations.ts +0 -54
  78. package/src/lib/auto-claude/steps/plan-implementation.ts +0 -14
  79. package/src/lib/auto-claude/steps/plan.ts +0 -14
  80. package/src/lib/auto-claude/steps/refresh.ts +0 -114
  81. package/src/lib/auto-claude/steps/remove-label.ts +0 -22
  82. package/src/lib/auto-claude/steps/research.ts +0 -21
  83. package/src/lib/auto-claude/steps/review.ts +0 -14
@@ -0,0 +1,211 @@
1
+ import {
2
+ aggregateSessionTools,
3
+ analyzeSession,
4
+ extractProjectName,
5
+ getModelName,
6
+ getPrimaryModel,
7
+ } from "./analyzer.js";
8
+ import { extractSessionLabel } from "./labels.js";
9
+ import { parseJsonl } from "./parser.js";
10
+ import { extractToolData } from "./tools.js";
11
+ import type { JournalEntry, TreemapNode } from "./types.js";
12
+
13
+ /**
14
+ * Build turn-level nodes from session entries.
15
+ * Used by both single-session and all-sessions views.
16
+ */
17
+ export function buildTurnNodes(
18
+ sessionId: string,
19
+ entries: JournalEntry[],
20
+ filePath?: string,
21
+ ): TreemapNode[] {
22
+ const children: TreemapNode[] = [];
23
+ let turnNumber = 0;
24
+
25
+ for (const entry of entries) {
26
+ if (entry.type !== "user" && entry.type !== "assistant") continue;
27
+ if (!entry.message) continue;
28
+
29
+ const role = entry.message.role;
30
+ const usage = entry.message.usage;
31
+ const model = entry.message.model;
32
+
33
+ if (role === "user") {
34
+ turnNumber++;
35
+ }
36
+
37
+ if (!usage) continue;
38
+
39
+ const inputTokens = usage.input_tokens || 0;
40
+ const outputTokens = usage.output_tokens || 0;
41
+ const totalTokens = inputTokens + outputTokens;
42
+
43
+ if (totalTokens === 0) continue;
44
+
45
+ const ratio = outputTokens > 0 ? inputTokens / outputTokens : inputTokens > 0 ? 999 : 0;
46
+
47
+ // Extract individual tool calls from content blocks
48
+ const tools = extractToolData(entry.message.content, inputTokens, outputTokens);
49
+
50
+ // Create individual tool children nodes
51
+ const toolChildren: TreemapNode[] = tools.map((tool) => ({
52
+ name: tool.detail ? `${tool.name}: ${tool.detail}` : tool.name,
53
+ value: tool.inputTokens + tool.outputTokens,
54
+ inputTokens: tool.inputTokens,
55
+ outputTokens: tool.outputTokens,
56
+ ratio: tool.outputTokens > 0 ? tool.inputTokens / tool.outputTokens : 0,
57
+ toolName: tool.name,
58
+ }));
59
+
60
+ // Format turn name based on tools used
61
+ let turnName: string;
62
+ let primaryToolName: string | undefined;
63
+ if (role === "user") {
64
+ turnName = `Turn ${turnNumber}: User`;
65
+ } else if (tools.length === 1) {
66
+ // Single tool: show tool name and detail
67
+ const t = tools[0];
68
+ turnName = t.detail ? `${t.name}: ${t.detail}` : t.name;
69
+ primaryToolName = t.name;
70
+ } else if (tools.length > 1) {
71
+ // Multiple tools: list unique tool names, primary is most common
72
+ const uniqueNames = [...new Set(tools.map((t) => t.name))];
73
+ turnName = uniqueNames.slice(0, 3).join(", ") + (uniqueNames.length > 3 ? "..." : "");
74
+ primaryToolName = tools[0].name; // Use first tool as primary
75
+ } else {
76
+ turnName = `Turn ${turnNumber}: Response`;
77
+ primaryToolName = "Response";
78
+ }
79
+ children.push({
80
+ name: turnName,
81
+ value: toolChildren.length > 0 ? undefined : totalTokens, // Let children sum if present
82
+ children: toolChildren.length > 0 ? toolChildren : undefined,
83
+ sessionId: sessionId.slice(0, 8),
84
+ fullSessionId: sessionId,
85
+ filePath,
86
+ toolName: primaryToolName,
87
+ model: getModelName(model),
88
+ inputTokens,
89
+ outputTokens,
90
+ ratio,
91
+ tools: tools.length > 0 ? tools : undefined,
92
+ });
93
+ }
94
+
95
+ return children;
96
+ }
97
+
98
+ /**
99
+ * Build treemap for a single session.
100
+ */
101
+ export function buildSessionTreemap(sessionId: string, entries: JournalEntry[]): TreemapNode {
102
+ return {
103
+ name: `Session ${sessionId.slice(0, 8)}`,
104
+ children: buildTurnNodes(sessionId, entries),
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Build treemap for all sessions, grouped by project and date.
110
+ */
111
+ export function buildAllSessionsTreemap(
112
+ sessions: Array<{
113
+ sessionId: string;
114
+ path: string;
115
+ date: string;
116
+ tokens: number;
117
+ project: string;
118
+ }>,
119
+ ): TreemapNode {
120
+ // Group sessions by project, then by date
121
+ const byProject = new Map<string, typeof sessions>();
122
+ for (const session of sessions) {
123
+ const projectName = extractProjectName(session.project);
124
+ if (!byProject.has(projectName)) {
125
+ byProject.set(projectName, []);
126
+ }
127
+ byProject.get(projectName)!.push(session);
128
+ }
129
+
130
+ // Sort projects by total tokens
131
+ const projectTotals = [...byProject.entries()].map(([name, sess]) => ({
132
+ name,
133
+ sessions: sess,
134
+ total: sess.reduce((sum, s) => sum + s.tokens, 0),
135
+ }));
136
+ projectTotals.sort((a, b) => b.total - a.total);
137
+
138
+ const projectChildren: TreemapNode[] = [];
139
+
140
+ for (const { name: projectName, sessions: projectSessions } of projectTotals) {
141
+ // Group by date within project
142
+ const byDate = new Map<string, typeof sessions>();
143
+ for (const session of projectSessions) {
144
+ if (!byDate.has(session.date)) {
145
+ byDate.set(session.date, []);
146
+ }
147
+ byDate.get(session.date)!.push(session);
148
+ }
149
+
150
+ // Sort dates (most recent first)
151
+ const sortedDates = [...byDate.keys()].sort().reverse();
152
+
153
+ const dateChildren: TreemapNode[] = [];
154
+
155
+ for (const date of sortedDates) {
156
+ const dateSessions = byDate.get(date)!;
157
+
158
+ const sessionChildren: TreemapNode[] = [];
159
+
160
+ for (const session of dateSessions) {
161
+ const entries = parseJsonl(session.path);
162
+ const analysis = analyzeSession(entries);
163
+ const label = extractSessionLabel(entries, session.sessionId);
164
+ const tools = aggregateSessionTools(entries);
165
+ const startTime = entries[0]?.timestamp
166
+ ? new Date(entries[0].timestamp).toLocaleTimeString()
167
+ : undefined;
168
+
169
+ // Build turn-level children for drill-down
170
+ const turnChildren = buildTurnNodes(session.sessionId, entries, session.path);
171
+
172
+ sessionChildren.push({
173
+ name: label,
174
+ // If we have turn children, let them sum; otherwise use session total
175
+ value: turnChildren.length > 0 ? undefined : session.tokens,
176
+ children: turnChildren.length > 0 ? turnChildren : undefined,
177
+ sessionId: session.sessionId.slice(0, 8),
178
+ fullSessionId: session.sessionId,
179
+ filePath: session.path,
180
+ startTime,
181
+ model: getPrimaryModel(analysis),
182
+ inputTokens: analysis.inputTokens,
183
+ outputTokens: analysis.outputTokens,
184
+ ratio: analysis.outputTokens > 0 ? analysis.inputTokens / analysis.outputTokens : 0,
185
+ date: session.date,
186
+ project: projectName,
187
+ repeatedReads: analysis.repeatedReads,
188
+ modelEfficiency: analysis.modelEfficiency,
189
+ tools: tools.length > 0 ? tools : undefined,
190
+ });
191
+ }
192
+
193
+ dateChildren.push({
194
+ name: date,
195
+ children: sessionChildren,
196
+ date,
197
+ });
198
+ }
199
+
200
+ projectChildren.push({
201
+ name: projectName,
202
+ children: dateChildren,
203
+ project: projectName,
204
+ });
205
+ }
206
+
207
+ return {
208
+ name: "All Sessions",
209
+ children: projectChildren,
210
+ };
211
+ }
@@ -0,0 +1,80 @@
1
+ // Types for parsing Claude Code session JSONL files
2
+ export interface ContentBlock {
3
+ type: string;
4
+ text?: string;
5
+ id?: string;
6
+ name?: string;
7
+ input?: Record<string, unknown>;
8
+ }
9
+
10
+ export interface JournalEntry {
11
+ type: string;
12
+ sessionId: string;
13
+ timestamp: string;
14
+ message?: {
15
+ role: "user" | "assistant";
16
+ model?: string;
17
+ usage?: {
18
+ input_tokens?: number;
19
+ output_tokens?: number;
20
+ cache_read_input_tokens?: number;
21
+ cache_creation_input_tokens?: number;
22
+ };
23
+ content?: ContentBlock[] | string;
24
+ };
25
+ uuid?: string;
26
+ }
27
+
28
+ export interface ToolData {
29
+ name: string;
30
+ detail?: string;
31
+ inputTokens: number;
32
+ outputTokens: number;
33
+ }
34
+
35
+ // Bar chart types for stacked bar visualization - aggregated by project
36
+ export interface ProjectBar {
37
+ project: string;
38
+ totalTokens: number;
39
+ }
40
+
41
+ export interface BarChartDay {
42
+ date: string; // YYYY-MM-DD format
43
+ projects: ProjectBar[];
44
+ }
45
+
46
+ export interface BarChartData {
47
+ days: BarChartDay[];
48
+ }
49
+
50
+ export interface SessionResult {
51
+ sessionId: string;
52
+ path: string;
53
+ date: string;
54
+ tokens: number;
55
+ project: string;
56
+ mtime: number;
57
+ }
58
+
59
+ export interface TreemapNode {
60
+ name: string;
61
+ value?: number;
62
+ children?: TreemapNode[];
63
+ // Metadata for tooltips
64
+ sessionId?: string;
65
+ fullSessionId?: string;
66
+ filePath?: string;
67
+ startTime?: string;
68
+ model?: string;
69
+ inputTokens?: number;
70
+ outputTokens?: number;
71
+ ratio?: number;
72
+ date?: string;
73
+ project?: string;
74
+ // Waste metrics
75
+ repeatedReads?: number;
76
+ modelEfficiency?: number; // Opus tokens / total tokens
77
+ // Tool data
78
+ tools?: ToolData[];
79
+ toolName?: string; // For coloring by tool type
80
+ }
@@ -0,0 +1,64 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ export interface ClaudeSettings {
6
+ cleanupPeriodDays?: number;
7
+ alwaysThinkingEnabled?: boolean;
8
+ hooks?: Record<string, unknown[]>;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ export const CLAUDE_SETTINGS_PATH = path.join(homedir(), ".claude", "settings.json");
13
+
14
+ /**
15
+ * Load Claude settings from the given path.
16
+ * Returns an empty object if the file is missing or contains invalid JSON.
17
+ */
18
+ export function loadClaudeSettings(settingsPath: string): ClaudeSettings {
19
+ if (!fs.existsSync(settingsPath)) {
20
+ return {};
21
+ }
22
+ try {
23
+ const content = fs.readFileSync(settingsPath, "utf-8");
24
+ return JSON.parse(content) as ClaudeSettings;
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Pure function that applies recommended defaults and returns the updated
32
+ * settings plus a list of human-readable change descriptions.
33
+ */
34
+ export function applyRecommendedSettings(settings: ClaudeSettings): {
35
+ settings: ClaudeSettings;
36
+ changes: string[];
37
+ } {
38
+ const result = { ...settings };
39
+ const changes: string[] = [];
40
+
41
+ if (result.cleanupPeriodDays !== 99999) {
42
+ result.cleanupPeriodDays = 99999;
43
+ changes.push("Set cleanupPeriodDays: 99999 (prevent log deletion)");
44
+ }
45
+
46
+ if (result.alwaysThinkingEnabled !== true) {
47
+ result.alwaysThinkingEnabled = true;
48
+ changes.push("Set alwaysThinkingEnabled: true");
49
+ }
50
+
51
+ return { settings: result, changes };
52
+ }
53
+
54
+ /**
55
+ * Write Claude settings as formatted JSON to the given path,
56
+ * creating parent directories if needed.
57
+ */
58
+ export function saveClaudeSettings(settingsPath: string, settings: ClaudeSettings): void {
59
+ const dir = path.dirname(settingsPath);
60
+ if (!fs.existsSync(dir)) {
61
+ fs.mkdirSync(dir, { recursive: true });
62
+ }
63
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
64
+ }
@@ -0,0 +1,33 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import consola from "consola";
4
+
5
+ const execAsync = promisify(exec);
6
+
7
+ /**
8
+ * Open file in default editor with folder context
9
+ */
10
+ export async function openInEditor({
11
+ editor,
12
+ filePath,
13
+ folderPath,
14
+ }: {
15
+ editor: string;
16
+ filePath: string;
17
+ folderPath?: string;
18
+ }): Promise<void> {
19
+ try {
20
+ if (folderPath) {
21
+ // Open both folder and file - this works with VS Code and similar editors
22
+ // the purpose is to open the folder context for better navigation
23
+ await execAsync(`"${editor}" "${folderPath}" "${filePath}"`);
24
+ } else {
25
+ await execAsync(`"${editor}" "${filePath}"`);
26
+ }
27
+ } catch (ex) {
28
+ consola.warn(
29
+ `Could not open in editor : '${editor}'. Modify your editor in the config: examples include 'code', 'code-insiders', etc...`,
30
+ ex,
31
+ );
32
+ }
33
+ }
@@ -0,0 +1,13 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import consola from "consola";
3
+ import { colors } from "consola/utils";
4
+
5
+ /**
6
+ * Create journal directory if it doesn't exist
7
+ */
8
+ export function ensureDirectoryExists(folderPath: string): void {
9
+ if (!existsSync(folderPath)) {
10
+ consola.info(`Creating journal directory: ${colors.cyan(folderPath)}`);
11
+ mkdirSync(folderPath, { recursive: true });
12
+ }
13
+ }
@@ -0,0 +1,11 @@
1
+ export { ensureDirectoryExists } from "./fs.js";
2
+ export { openInEditor } from "./editor.js";
3
+ export {
4
+ loadTemplate,
5
+ ensureTemplatesExist,
6
+ createJournalContent,
7
+ createMeetingContent,
8
+ createNoteContent,
9
+ } from "./templates.js";
10
+ export { resolvePathTemplate, generateJournalFileInfoByType } from "./paths.js";
11
+ export type { GenerateJournalFileResult, GenerateJournalFileParams } from "./paths.js";
@@ -0,0 +1,106 @@
1
+ import path from "node:path";
2
+ import consola from "consola";
3
+ import { DateTime } from "luxon";
4
+ import { getMondayOfWeek } from "../../utils/date-utils.js";
5
+ import type { JournalSettings } from "../../config/settings.js";
6
+ import { JOURNAL_TYPES } from "../../types/journal.js";
7
+ import type { JournalType } from "../../types/journal.js";
8
+
9
+ export interface GenerateJournalFileResult {
10
+ fullPath: string;
11
+ mondayDate: Date;
12
+ currentDate: Date;
13
+ }
14
+
15
+ export interface GenerateJournalFileParams {
16
+ date: Date;
17
+ type: JournalType;
18
+ title: string;
19
+ journalSettings: JournalSettings;
20
+ }
21
+
22
+ export function resolvePathTemplate(
23
+ template: string,
24
+ title: string,
25
+ date: Date,
26
+ mondayDate: Date,
27
+ ): string {
28
+ const dateTime = DateTime.fromJSDate(date, { zone: "utc" });
29
+
30
+ // Replace Luxon format tokens wrapped in curly braces
31
+ return template.replace(/\{([^}]+)\}/g, (match, token) => {
32
+ try {
33
+ if (token === "title") {
34
+ return title.toLowerCase().replace(/\s+/g, "-");
35
+ }
36
+
37
+ if (token.startsWith("monday:")) {
38
+ const mondayToken = token.substring(7); // Remove 'monday:' prefix
39
+ const mondayDateTime = DateTime.fromJSDate(mondayDate, { zone: "utc" });
40
+ return mondayDateTime.toFormat(mondayToken);
41
+ }
42
+
43
+ const result = dateTime.toFormat(token);
44
+ // Check if the result contains suspicious patterns that indicate invalid tokens
45
+ // This is a heuristic to detect when Luxon produces garbage output for invalid tokens
46
+ const isLikelyInvalid =
47
+ token.includes("invalid") ||
48
+ result.length > 20 || // Very long results are likely garbage
49
+ (result.length > token.length * 2 && /\d{10,}/.test(result)) || // Contains very long numbers
50
+ result.includes("UTC");
51
+
52
+ if (isLikelyInvalid) {
53
+ consola.warn(`Invalid date format token: ${token}`);
54
+ return match;
55
+ }
56
+ return result;
57
+ } catch (error) {
58
+ consola.warn(`Invalid date format token: ${token}`);
59
+ return match; // Return original token if format is invalid
60
+ }
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Generate journal file info for different types using individual path templates
66
+ */
67
+ export function generateJournalFileInfoByType({
68
+ journalSettings,
69
+ date = new Date(),
70
+ type,
71
+ title,
72
+ }: GenerateJournalFileParams): GenerateJournalFileResult {
73
+ const currentDate = new Date(date);
74
+
75
+ let templatePath: string;
76
+ let mondayDate: Date;
77
+
78
+ switch (type) {
79
+ case JOURNAL_TYPES.DAILY_NOTES:
80
+ templatePath = journalSettings.dailyPathTemplate;
81
+ mondayDate = getMondayOfWeek(currentDate);
82
+ break;
83
+ case JOURNAL_TYPES.MEETING:
84
+ templatePath = journalSettings.meetingPathTemplate;
85
+ mondayDate = currentDate;
86
+ break;
87
+ case JOURNAL_TYPES.NOTE:
88
+ templatePath = journalSettings.notePathTemplate;
89
+ mondayDate = currentDate;
90
+ break;
91
+ default:
92
+ throw new Error(`Unknown JournalType: ${type}`);
93
+ }
94
+
95
+ // Resolve the path template and extract directory structure
96
+ const resolvedPath = resolvePathTemplate(templatePath, title, currentDate, mondayDate);
97
+
98
+ // Join baseFolder with the resolved path
99
+ const fullPath = path.join(journalSettings.baseFolder, resolvedPath);
100
+
101
+ return {
102
+ currentDate,
103
+ fullPath,
104
+ mondayDate,
105
+ } satisfies GenerateJournalFileResult;
106
+ }
@@ -1,14 +1,9 @@
1
- import { exec } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
2
  import path from "node:path";
4
- import { promisify } from "node:util";
5
3
  import consola from "consola";
6
4
  import { colors } from "consola/utils";
7
- import { DateTime } from "luxon";
8
- import { formatDate, getMondayOfWeek, getWeekInfo } from "../../utils/date-utils.js";
9
- import type { JournalSettings } from "../../config/settings.js";
10
- import { JOURNAL_TYPES } from "../../types/journal.js";
11
- import type { JournalType } from "../../types/journal.js";
5
+ import { formatDate, getWeekInfo } from "../../utils/date-utils.js";
6
+ import { ensureDirectoryExists } from "./fs.js";
12
7
 
13
8
  // Default template file names
14
9
  const TEMPLATE_FILES = {
@@ -17,18 +12,6 @@ const TEMPLATE_FILES = {
17
12
  note: "note.md",
18
13
  } as const;
19
14
 
20
- const execAsync = promisify(exec);
21
-
22
- /**
23
- * Create journal directory if it doesn't exist
24
- */
25
- export function ensureDirectoryExists(folderPath: string): void {
26
- if (!existsSync(folderPath)) {
27
- consola.info(`Creating journal directory: ${colors.cyan(folderPath)}`);
28
- mkdirSync(folderPath, { recursive: true });
29
- }
30
- }
31
-
32
15
  /**
33
16
  * Load template from external file or return null if not found
34
17
  */
@@ -266,134 +249,3 @@ export function createNoteContent({
266
249
 
267
250
  return content.join("\n");
268
251
  }
269
-
270
- /**
271
- * Open file in default editor with folder context
272
- */
273
- export async function openInEditor({
274
- editor,
275
- filePath,
276
- folderPath,
277
- }: {
278
- editor: string;
279
- filePath: string;
280
- folderPath?: string;
281
- }): Promise<void> {
282
- try {
283
- if (folderPath) {
284
- // Open both folder and file - this works with VS Code and similar editors
285
- // the purpose is to open the folder context for better navigation
286
- await execAsync(`"${editor}" "${folderPath}" "${filePath}"`);
287
- } else {
288
- await execAsync(`"${editor}" "${filePath}"`);
289
- }
290
- } catch (ex) {
291
- consola.warn(
292
- `Could not open in editor : '${editor}'. Modify your editor in the config: examples include 'code', 'code-insiders', etc...`,
293
- ex,
294
- );
295
- }
296
- }
297
-
298
- export function resolvePathTemplate(
299
- template: string,
300
- title: string,
301
- date: Date,
302
- mondayDate: Date,
303
- ): string {
304
- const dateTime = DateTime.fromJSDate(date, { zone: "utc" });
305
-
306
- // Replace Luxon format tokens wrapped in curly braces
307
- return template.replace(/\{([^}]+)\}/g, (match, token) => {
308
- try {
309
- if (token === "title") {
310
- return title.toLowerCase().replace(/\s+/g, "-");
311
- }
312
-
313
- if (token.startsWith("monday:")) {
314
- const mondayToken = token.substring(7); // Remove 'monday:' prefix
315
- const mondayDateTime = DateTime.fromJSDate(mondayDate, { zone: "utc" });
316
- return mondayDateTime.toFormat(mondayToken);
317
- }
318
-
319
- const result = dateTime.toFormat(token);
320
- // Check if the result contains suspicious patterns that indicate invalid tokens
321
- // This is a heuristic to detect when Luxon produces garbage output for invalid tokens
322
- const isLikelyInvalid =
323
- token.includes("invalid") ||
324
- result.length > 20 || // Very long results are likely garbage
325
- (result.length > token.length * 2 && /\d{10,}/.test(result)) || // Contains very long numbers
326
- result.includes("UTC");
327
-
328
- if (isLikelyInvalid) {
329
- consola.warn(`Invalid date format token: ${token}`);
330
- return match;
331
- }
332
- return result;
333
- } catch (error) {
334
- consola.warn(`Invalid date format token: ${token}`);
335
- return match; // Return original token if format is invalid
336
- }
337
- });
338
- }
339
-
340
- interface GenerateJournalFileResult {
341
- fullPath: string;
342
- mondayDate: Date;
343
- currentDate: Date;
344
- }
345
-
346
- interface GenerateJournalFileParams {
347
- date: Date;
348
- type: JournalType;
349
- title: string;
350
- journalSettings: JournalSettings;
351
- }
352
-
353
- /**
354
- * Generate journal file info for different types using individual path templates
355
- */
356
- export function generateJournalFileInfoByType({
357
- journalSettings,
358
- date = new Date(),
359
- type,
360
- title,
361
- }: GenerateJournalFileParams): GenerateJournalFileResult {
362
- const currentDate = new Date(date);
363
-
364
- let templatePath: string = "";
365
- let mondayDate: Date = getMondayOfWeek(currentDate);
366
-
367
- switch (type) {
368
- case JOURNAL_TYPES.DAILY_NOTES: {
369
- const monday = getMondayOfWeek(currentDate);
370
- templatePath = journalSettings.dailyPathTemplate;
371
- mondayDate = monday;
372
- break;
373
- }
374
- case JOURNAL_TYPES.MEETING: {
375
- templatePath = journalSettings.meetingPathTemplate;
376
- mondayDate = currentDate;
377
- break;
378
- }
379
- case JOURNAL_TYPES.NOTE: {
380
- templatePath = journalSettings.notePathTemplate;
381
- mondayDate = currentDate;
382
- break;
383
- }
384
- default:
385
- throw new Error(`Unknown JournalType: ${type}`);
386
- }
387
-
388
- // Resolve the path template and extract directory structure
389
- const resolvedPath = resolvePathTemplate(templatePath, title, currentDate, mondayDate);
390
-
391
- // Join baseFolder with the resolved path
392
- const fullPath = path.join(journalSettings.baseFolder, resolvedPath);
393
-
394
- return {
395
- currentDate: currentDate,
396
- fullPath: fullPath,
397
- mondayDate,
398
- } satisfies GenerateJournalFileResult;
399
- }