@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.
- package/package.json +50 -57
- package/src/commands/agentboard.ts +176 -0
- package/src/commands/{auto-claude.ts → auto-claude/index.ts} +18 -28
- package/src/commands/auto-claude/list.ts +114 -0
- package/src/commands/auto-claude/retry.test.ts +138 -0
- package/src/commands/auto-claude/retry.ts +139 -0
- package/src/commands/auto-claude/status.test.ts +147 -0
- package/src/commands/auto-claude/status.ts +123 -0
- package/src/commands/base.ts +7 -2
- package/src/commands/config.ts +5 -7
- package/src/commands/doctor.ts +111 -12
- package/src/commands/gh/branch.ts +4 -4
- package/src/commands/gh/pr.ts +1 -0
- package/src/commands/graph/index.ts +169 -0
- package/src/commands/graph.test.ts +1 -1
- package/src/commands/install.ts +40 -68
- package/src/commands/journal/daily-notes.ts +3 -3
- package/src/commands/journal/meeting.ts +3 -3
- package/src/commands/journal/note.ts +3 -3
- package/src/lib/auto-claude/claude-cli.ts +183 -0
- package/src/lib/auto-claude/config.test.ts +6 -8
- package/src/lib/auto-claude/config.ts +3 -4
- package/src/lib/auto-claude/index.ts +2 -3
- package/src/lib/auto-claude/labels.test.ts +85 -0
- package/src/lib/auto-claude/labels.ts +42 -0
- package/src/lib/auto-claude/pipeline-execution.test.ts +129 -33
- package/src/lib/auto-claude/pipeline.test.ts +2 -2
- package/src/lib/auto-claude/pipeline.ts +120 -36
- package/src/lib/auto-claude/prompt-templates/01_plan.prompt.md +68 -0
- package/src/lib/auto-claude/prompt-templates/{05_implement.prompt.md → 02_implement.prompt.md} +3 -2
- package/src/lib/auto-claude/prompt-templates/03_simplify.prompt.md +52 -0
- package/src/lib/auto-claude/prompt-templates/{06_review.prompt.md → 04_review.prompt.md} +29 -6
- package/src/lib/auto-claude/prompt-templates/index.test.ts +9 -42
- package/src/lib/auto-claude/prompt-templates/index.ts +13 -28
- package/src/lib/auto-claude/run-claude.test.ts +48 -68
- package/src/lib/auto-claude/shell.ts +6 -0
- package/src/lib/auto-claude/steps/create-pr.ts +89 -25
- package/src/lib/auto-claude/steps/fetch-issues.ts +4 -1
- package/src/lib/auto-claude/steps/implement.ts +9 -16
- package/src/lib/auto-claude/steps/simple-steps.ts +34 -0
- package/src/lib/auto-claude/steps/steps.test.ts +68 -63
- package/src/lib/auto-claude/templates.test.ts +91 -0
- package/src/lib/auto-claude/templates.ts +34 -0
- package/src/lib/auto-claude/test-helpers.ts +2 -1
- package/src/lib/auto-claude/utils-execution.test.ts +9 -57
- package/src/lib/auto-claude/utils.test.ts +5 -9
- package/src/lib/auto-claude/utils.ts +27 -253
- package/src/lib/graph/analyzer.test.ts +451 -0
- package/src/lib/graph/analyzer.ts +165 -0
- package/src/lib/graph/index.ts +24 -0
- package/src/lib/graph/labels.ts +87 -0
- package/src/lib/graph/parser.test.ts +150 -0
- package/src/lib/graph/parser.ts +65 -0
- package/src/lib/graph/render.ts +25 -0
- package/src/lib/graph/server.ts +70 -0
- package/src/lib/graph/sessions.ts +104 -0
- package/src/lib/graph/tools.ts +90 -0
- package/src/lib/graph/treemap.ts +211 -0
- package/src/lib/graph/types.ts +80 -0
- package/src/lib/install/claude-settings.ts +64 -0
- package/src/lib/journal/editor.ts +33 -0
- package/src/lib/journal/fs.ts +13 -0
- package/src/lib/journal/index.ts +11 -0
- package/src/lib/journal/paths.ts +106 -0
- package/src/lib/journal/{utils.ts → templates.ts} +3 -151
- package/src/utils/fs.ts +19 -0
- package/src/utils/git/exec.ts +18 -0
- package/src/utils/git/gh-cli-wrapper.test.ts +47 -8
- package/src/utils/git/gh-cli-wrapper.ts +31 -19
- package/src/utils/render.ts +3 -1
- package/src/commands/graph.ts +0 -970
- package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +0 -21
- package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +0 -27
- package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +0 -15
- package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +0 -35
- package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +0 -30
- package/src/lib/auto-claude/steps/plan-annotations.ts +0 -54
- package/src/lib/auto-claude/steps/plan-implementation.ts +0 -14
- package/src/lib/auto-claude/steps/plan.ts +0 -14
- package/src/lib/auto-claude/steps/refresh.ts +0 -114
- package/src/lib/auto-claude/steps/remove-label.ts +0 -22
- package/src/lib/auto-claude/steps/research.ts +0 -21
- 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 {
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
-
}
|