@towles/tool 0.0.11-beta.2 → 0.0.11
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/README.md +28 -1
- package/dist/index.mjs +200 -347
- package/package.json +6 -7
package/README.md
CHANGED
|
@@ -8,8 +8,35 @@
|
|
|
8
8
|
|
|
9
9
|
One off quality of life scripts that I use on a daily basis.
|
|
10
10
|
|
|
11
|
+
## Journal Type System
|
|
12
|
+
|
|
13
|
+
The journal system supports three types of files with different templates and organization:
|
|
14
|
+
|
|
15
|
+
### Daily Notes (`journal daily-notes`)
|
|
16
|
+
- **Purpose**: Weekly files with daily sections for ongoing work and notes
|
|
17
|
+
- **File structure**: `YYYY/daily-notes/YYYY-MM-DD-week-log.md` (Monday's date)
|
|
18
|
+
- **Template**: Includes sections for Monday through Friday with date headers
|
|
19
|
+
- **Use case**: Regular daily journaling, work logs, scratch pad for notes
|
|
20
|
+
|
|
21
|
+
### Meeting Files (`journal meeting [title]`)
|
|
22
|
+
- **Purpose**: Structured meeting notes with agenda and action items
|
|
23
|
+
- **File structure**: `YYYY/meetings/YYYY-MM-DD-HHMM-meeting-[title].md`
|
|
24
|
+
- **Template**: Includes Date, Time, Attendees, Agenda, Notes, Action Items, and Follow-up sections
|
|
25
|
+
- **Use case**: Meeting preparation, note-taking, and action item tracking
|
|
26
|
+
|
|
27
|
+
### Note Files (`journal note [title]`)
|
|
28
|
+
- **Purpose**: General-purpose notes with structured sections
|
|
29
|
+
- **File structure**: `YYYY/notes/YYYY-MM-DD-HHMM-note-[title].md`
|
|
30
|
+
- **Template**: Includes Summary, Details, and References sections
|
|
31
|
+
- **Use case**: Research notes, documentation, general information capture
|
|
32
|
+
|
|
33
|
+
### Commands
|
|
34
|
+
- `journal` or `journal daily-notes` - Create/open current week's daily notes
|
|
35
|
+
- `journal meeting [title]` - Create a new meeting file with optional title
|
|
36
|
+
- `journal note [title]` - Create a new note file with optional title
|
|
37
|
+
|
|
11
38
|
## Tools to add
|
|
12
|
-
- [x]
|
|
39
|
+
- [x] Journal system - creates and opens markdown files with templates for daily-notes, meetings, and notes
|
|
13
40
|
- [ ] use claude code to generate git commits with multiple options for the commit message.
|
|
14
41
|
|
|
15
42
|
## Install from repository
|
package/dist/index.mjs
CHANGED
|
@@ -3,360 +3,111 @@ import process from 'node:process';
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import consola from 'consola';
|
|
5
5
|
import { colors } from 'consola/utils';
|
|
6
|
-
import { spinner } from '@clack/prompts';
|
|
7
|
-
import { Fzf } from 'fzf';
|
|
8
6
|
import prompts from 'prompts';
|
|
9
|
-
import z$1, { z } from 'zod/v4';
|
|
10
|
-
import { err, ok } from 'neverthrow';
|
|
11
|
-
import { query } from '@anthropic-ai/claude-code';
|
|
12
7
|
import { execSync, exec } from 'node:child_process';
|
|
13
|
-
import util, { promisify } from 'node:util';
|
|
14
8
|
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
15
9
|
import * as path from 'node:path';
|
|
16
10
|
import path__default from 'node:path';
|
|
11
|
+
import util, { promisify } from 'node:util';
|
|
17
12
|
import { homedir } from 'node:os';
|
|
18
13
|
import { setupDotenv, loadConfig } from 'c12';
|
|
19
14
|
import { updateConfig } from 'c12/update';
|
|
20
15
|
|
|
21
|
-
const version = "0.0.11
|
|
16
|
+
const version = "0.0.11";
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
cause;
|
|
26
|
-
constructor(message, cause) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.cause = isError(cause) ? cause : void 0;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
class ClaudeError extends AnyError {
|
|
32
|
-
name = "ClaudeError";
|
|
33
|
-
}
|
|
34
|
-
function isError(error) {
|
|
35
|
-
return error instanceof Error || error instanceof AnyError;
|
|
18
|
+
function execCommand(cmd, cwd) {
|
|
19
|
+
return execSync(cmd, { encoding: "utf8", cwd }).trim();
|
|
36
20
|
}
|
|
37
21
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
22
|
+
async function gitCommitCommand(config, messageArgs) {
|
|
23
|
+
let statusOutput;
|
|
24
|
+
try {
|
|
25
|
+
statusOutput = execCommand("git status --porcelain", config.cwd);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
consola.error("Failed to get git status");
|
|
28
|
+
process.exit(1);
|
|
44
29
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
consola.
|
|
51
|
-
return
|
|
30
|
+
const lines = statusOutput.trim().split("\n").filter((line) => line.length > 0);
|
|
31
|
+
const stagedFiles = lines.filter((line) => line[0] !== " " && line[0] !== "?");
|
|
32
|
+
const unstagedFiles = lines.filter((line) => line[1] !== " " && line[1] !== "?");
|
|
33
|
+
const untrackedFiles = lines.filter((line) => line.startsWith("??"));
|
|
34
|
+
if (lines.length === 0) {
|
|
35
|
+
consola.info("Working tree clean - nothing to commit");
|
|
36
|
+
return;
|
|
52
37
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
z.string(),
|
|
58
|
-
z.number(),
|
|
59
|
-
z.boolean(),
|
|
60
|
-
z.null()
|
|
61
|
-
]);
|
|
62
|
-
const jsonValueSchema = z.lazy(
|
|
63
|
-
() => z.union([
|
|
64
|
-
jsonPrimitiveSchema,
|
|
65
|
-
z.array(jsonValueSchema),
|
|
66
|
-
z.record(z.string(), jsonValueSchema)
|
|
67
|
-
])
|
|
68
|
-
);
|
|
69
|
-
z.record(
|
|
70
|
-
z.string(),
|
|
71
|
-
jsonValueSchema
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
class ClaudeService {
|
|
75
|
-
pathToClaudeCodeExecutable;
|
|
76
|
-
constructor(pathToClaudeCodeExecutable) {
|
|
77
|
-
this.pathToClaudeCodeExecutable = pathToClaudeCodeExecutable;
|
|
38
|
+
consola.info("Git status:");
|
|
39
|
+
if (stagedFiles.length > 0) {
|
|
40
|
+
consola.info(colors.green("Staged files:"));
|
|
41
|
+
stagedFiles.forEach((file) => consola.info(` ${colors.green(file)}`));
|
|
78
42
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
pathToClaudeCodeExecutable: this.pathToClaudeCodeExecutable,
|
|
83
|
-
permissionMode: "bypassPermissions"
|
|
84
|
-
};
|
|
85
|
-
if (input.sessionId) {
|
|
86
|
-
options.resume = input.sessionId;
|
|
87
|
-
}
|
|
88
|
-
if (input.cwd) {
|
|
89
|
-
options.cwd = input.cwd;
|
|
90
|
-
}
|
|
91
|
-
if (input.allowedTools) {
|
|
92
|
-
options.allowedTools = input.allowedTools;
|
|
93
|
-
}
|
|
94
|
-
if (input.bypassPermissions) {
|
|
95
|
-
options.permissionMode = "bypassPermissions";
|
|
96
|
-
}
|
|
97
|
-
const messages = [];
|
|
98
|
-
for await (const message of query({
|
|
99
|
-
prompt: input.message,
|
|
100
|
-
options
|
|
101
|
-
})) {
|
|
102
|
-
const customMessage = message;
|
|
103
|
-
messages.push(customMessage);
|
|
104
|
-
onChunk(customMessage);
|
|
105
|
-
}
|
|
106
|
-
if (messages.length === 0) {
|
|
107
|
-
throw new Error("No response received from Claude Code SDK");
|
|
108
|
-
}
|
|
109
|
-
return ok(messages);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
return err(
|
|
112
|
-
new ClaudeError("Failed to stream message from Claude", error)
|
|
113
|
-
);
|
|
114
|
-
}
|
|
43
|
+
if (unstagedFiles.length > 0) {
|
|
44
|
+
consola.info(colors.yellow("Modified files (not staged):"));
|
|
45
|
+
unstagedFiles.forEach((file) => consola.info(` ${colors.yellow(file)}`));
|
|
115
46
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return err(
|
|
120
|
-
new ClaudeError("Invalid assistant content format", parsed.error)
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
if (!Array.isArray(parsed.value)) {
|
|
124
|
-
return err(
|
|
125
|
-
new ClaudeError("Assistant content must be an array of content blocks")
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
for (const item of parsed.value) {
|
|
129
|
-
if (typeof item !== "object" || item === null || !("type" in item)) {
|
|
130
|
-
return err(
|
|
131
|
-
new ClaudeError("Assistant content contains invalid content block")
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return ok(parsed.value);
|
|
47
|
+
if (untrackedFiles.length > 0) {
|
|
48
|
+
consola.info(colors.red("Untracked files:"));
|
|
49
|
+
untrackedFiles.forEach((file) => consola.info(` ${colors.red(file)}`));
|
|
136
50
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
51
|
+
if (stagedFiles.length === 0) {
|
|
52
|
+
if (unstagedFiles.length > 0 || untrackedFiles.length > 0) {
|
|
53
|
+
const { shouldStage } = await prompts({
|
|
54
|
+
type: "confirm",
|
|
55
|
+
name: "shouldStage",
|
|
56
|
+
message: "No files are staged. Would you like to add files first?",
|
|
57
|
+
initial: true
|
|
58
|
+
});
|
|
59
|
+
if (shouldStage) {
|
|
60
|
+
const { addAll } = await prompts({
|
|
61
|
+
type: "confirm",
|
|
62
|
+
name: "addAll",
|
|
63
|
+
message: "Add all modified and untracked files?",
|
|
64
|
+
initial: true
|
|
65
|
+
});
|
|
66
|
+
if (addAll) {
|
|
67
|
+
try {
|
|
68
|
+
execCommand("git add .", config.cwd);
|
|
69
|
+
consola.success("All files staged successfully");
|
|
70
|
+
} catch (error) {
|
|
71
|
+
consola.error("Failed to stage files");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
consola.info(`Use ${colors.cyan("git add <file>...")} to stage specific files, then run the commit command again`);
|
|
76
|
+
return;
|
|
153
77
|
}
|
|
78
|
+
} else {
|
|
79
|
+
consola.error(`No staged changes found to commit. Use ${colors.cyan("git add <file>...")} to stage changes before committing.`);
|
|
80
|
+
process.exit(1);
|
|
154
81
|
}
|
|
155
|
-
|
|
82
|
+
} else {
|
|
83
|
+
consola.error("No changes to commit");
|
|
84
|
+
return;
|
|
156
85
|
}
|
|
157
|
-
return err(
|
|
158
|
-
new ClaudeError(
|
|
159
|
-
"User content must be a string or array of content blocks"
|
|
160
|
-
)
|
|
161
|
-
);
|
|
162
86
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function printJson(obj) {
|
|
181
|
-
consola.log(util.inspect(obj, {
|
|
182
|
-
depth: 2,
|
|
183
|
-
colors: true,
|
|
184
|
-
showHidden: false,
|
|
185
|
-
compact: false
|
|
186
|
-
}));
|
|
187
|
-
}
|
|
188
|
-
function printDebug(message, ...args) {
|
|
189
|
-
if (process.env.DEBUG) {
|
|
190
|
-
consola.debug(`DEBUG: ${message}`, ...args);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const commitTypes = ["feat", "fix", "docs", "test", "wip"];
|
|
195
|
-
const maxSubjectLength = 72;
|
|
196
|
-
const llmMaxSubjectLength = maxSubjectLength - 12;
|
|
197
|
-
const gitCommitSuggestionSchema = z$1.object({
|
|
198
|
-
subject: z$1.string().min(1).max(maxSubjectLength),
|
|
199
|
-
// larger than we told the LLM to generate
|
|
200
|
-
body: z$1.string().optional()
|
|
201
|
-
});
|
|
202
|
-
function finalPrompt(diff, generate_number) {
|
|
203
|
-
const generate_number_plural = "s" ;
|
|
204
|
-
const first_part = [
|
|
205
|
-
`You are a helpful assistant specializing in writing clear and informative Git commit messages using the conventional style.`,
|
|
206
|
-
`Based on the given code changes or context, generate exactly ${generate_number} conventional Git commit message${generate_number_plural} based on the following guidelines.`,
|
|
207
|
-
`1. Format: follow the conventional commits format following of one of the following. :`,
|
|
208
|
-
"<type>: <commit message>",
|
|
209
|
-
"<type>(<optional scope>): <commit message>",
|
|
210
|
-
"",
|
|
211
|
-
`2. Types: use one of the following types:`,
|
|
212
|
-
` ${commitTypes.join("-, ")}`,
|
|
213
|
-
"3. Guidelines for writing commit messages:",
|
|
214
|
-
" - Be specific about what changes were made",
|
|
215
|
-
' - Use imperative mood ("add feature" not "added feature")',
|
|
216
|
-
` - Keep subject line under ${llmMaxSubjectLength} characters`,
|
|
217
|
-
" - make each entry more generic and not too specific to the code changes",
|
|
218
|
-
" - Do not end the subject line with a period",
|
|
219
|
-
" - Use the body to explain what and why vs. how",
|
|
220
|
-
"4. Focus on:",
|
|
221
|
-
" - What problem this commit solves",
|
|
222
|
-
" - Why this change was necessary",
|
|
223
|
-
" - Any important technical details",
|
|
224
|
-
"5. Exclude anything unnecessary such as translation or implementation details."
|
|
225
|
-
];
|
|
226
|
-
const diff_part = [
|
|
227
|
-
`Here is the context for the commit message${generate_number_plural}:`,
|
|
228
|
-
"",
|
|
229
|
-
"```diff",
|
|
230
|
-
`${diff}`,
|
|
231
|
-
"```",
|
|
232
|
-
"",
|
|
233
|
-
`If no code changes are provided, use the context to generate a commit message based on the task or issue description.`
|
|
234
|
-
];
|
|
235
|
-
const last_part = [
|
|
236
|
-
"",
|
|
237
|
-
`Lastly, Provide your response as a JSON array containing exactly ${generate_number} object${generate_number_plural}, each with the following keys:`,
|
|
238
|
-
`- "subject": The main commit message using the conventional commit style. It should be a concise summary of the changes.`,
|
|
239
|
-
`- "body": An optional detailed explanation of the changes. If not needed, use an empty string.`,
|
|
240
|
-
`The array must always contain ${generate_number} element${generate_number_plural}, no more and no less.`,
|
|
241
|
-
`The LLM result will be valid json only if it is well-formed and follows the schema below.`,
|
|
242
|
-
"",
|
|
243
|
-
`<result>`,
|
|
244
|
-
`[`,
|
|
245
|
-
` {`,
|
|
246
|
-
` "subject": "chore: update <file> to handle auth",`,
|
|
247
|
-
` "body": "- Update login function to handle edge cases\\n- Add additional error logging for debugging",`,
|
|
248
|
-
` }`,
|
|
249
|
-
` {`,
|
|
250
|
-
` "subject": "fix(auth): fix bug in user authentication process",`,
|
|
251
|
-
` "body": "- Update login function to handle edge cases\\n- Add additional error logging for debugging",`,
|
|
252
|
-
` }`,
|
|
253
|
-
`]`,
|
|
254
|
-
`</result>`,
|
|
255
|
-
"",
|
|
256
|
-
"",
|
|
257
|
-
`Ensure you generate exactly ${generate_number} commit message${generate_number_plural}, even if it requires creating slightly varied versions for similar changes.`,
|
|
258
|
-
`The response should be valid JSON that can be parsed without errors.`
|
|
259
|
-
];
|
|
260
|
-
return [
|
|
261
|
-
...first_part,
|
|
262
|
-
...diff_part,
|
|
263
|
-
...last_part
|
|
264
|
-
].join("\n");
|
|
265
|
-
}
|
|
266
|
-
async function gitCommitCommand(config) {
|
|
267
|
-
const s = spinner();
|
|
268
|
-
s.start("Generating commit message based on diff...");
|
|
269
|
-
s.message("getting git diff...");
|
|
270
|
-
const diff = await getGitDiff(config.cwd);
|
|
271
|
-
if (diff.trim().length === 0) {
|
|
272
|
-
consola.error(`No staged changes found to commit. use '${colors.blue("git add <file>...")}' to stage changes before committing.`);
|
|
273
|
-
process.exit(1);
|
|
274
|
-
}
|
|
275
|
-
s.message("generating prompt...");
|
|
276
|
-
const prompt = finalPrompt(
|
|
277
|
-
diff,
|
|
278
|
-
/* config.generate_number || */
|
|
279
|
-
6
|
|
280
|
-
);
|
|
281
|
-
const service = new ClaudeService();
|
|
282
|
-
const input = {
|
|
283
|
-
message: prompt
|
|
284
|
-
};
|
|
285
|
-
s.message("sending prompt to claude and waiting answer...");
|
|
286
|
-
const result = await service.sendMessageStream(input, () => {
|
|
287
|
-
});
|
|
288
|
-
if (result.isErr()) {
|
|
289
|
-
consola.error(`Error sending message: ${result.error.message}`);
|
|
290
|
-
process.exit(1);
|
|
291
|
-
}
|
|
292
|
-
s.stop(`Received commit messages from Claude!${colors.green("\u2713")}`);
|
|
293
|
-
const filteredResults = result.value.filter((msg) => msg.type === "result");
|
|
294
|
-
if (filteredResults.length === 0) {
|
|
295
|
-
consola.error("No valid commit messages received from Claude");
|
|
296
|
-
process.exit(1);
|
|
297
|
-
}
|
|
298
|
-
if (filteredResults.length > 1) {
|
|
299
|
-
consola.warn(`Received ${filteredResults.length} commit messages, using the first one`);
|
|
300
|
-
}
|
|
301
|
-
const startIndex = filteredResults[0].result.indexOf("[");
|
|
302
|
-
const endIndex = filteredResults[0].result.lastIndexOf("]");
|
|
303
|
-
if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
|
|
304
|
-
consola.error("Could not find valid JSON array in Claude response");
|
|
305
|
-
process.exit(1);
|
|
306
|
-
}
|
|
307
|
-
const cleanedResultRaw = filteredResults[0].result.substring(startIndex, endIndex + 1);
|
|
308
|
-
const cleanedResult = JSON.parse(cleanedResultRaw);
|
|
309
|
-
const suggestions = validate(z$1.array(gitCommitSuggestionSchema), cleanedResult);
|
|
310
|
-
consola.success("Claude suggestions validated for format!");
|
|
311
|
-
if (!suggestions.isOk()) {
|
|
312
|
-
consola.error("Invalid suggestions format:", suggestions.error);
|
|
313
|
-
process.exit(1);
|
|
87
|
+
let commitMessage;
|
|
88
|
+
if (messageArgs && messageArgs.length > 0) {
|
|
89
|
+
commitMessage = messageArgs.join(" ");
|
|
90
|
+
} else {
|
|
91
|
+
const { message } = await prompts({
|
|
92
|
+
type: "text",
|
|
93
|
+
name: "message",
|
|
94
|
+
message: "Enter commit message:",
|
|
95
|
+
validate: (value) => value.trim().length > 0 || "Commit message cannot be empty"
|
|
96
|
+
});
|
|
97
|
+
if (!message) {
|
|
98
|
+
consola.info(colors.dim("Commit cancelled"));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
commitMessage = message.trim();
|
|
314
102
|
}
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
consola.info(`Running command: ${colors.bgCyan(commandWithArgs)}`);
|
|
103
|
+
const commandWithArgs = `git commit -m "${commitMessage.replace(/"/g, '\\"')}"`;
|
|
104
|
+
consola.info(`Running: ${colors.cyan(commandWithArgs)}`);
|
|
318
105
|
try {
|
|
319
106
|
execCommand(commandWithArgs, config.cwd);
|
|
107
|
+
consola.success("Commit created successfully!");
|
|
320
108
|
} catch (error) {
|
|
321
|
-
consola.error(
|
|
322
|
-
|
|
323
|
-
process.exit(1);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
async function promptUserForCommitMessage(suggestions) {
|
|
327
|
-
let profileChoices = suggestions.map((x) => ({
|
|
328
|
-
title: x.subject,
|
|
329
|
-
value: x.subject
|
|
330
|
-
// description: limitText(`${x.getFriendlyAccountName()}`, 15),
|
|
331
|
-
}));
|
|
332
|
-
profileChoices = profileChoices.sort((a, b) => a.title.localeCompare(b.title));
|
|
333
|
-
const fzf = new Fzf(profileChoices, {
|
|
334
|
-
selector: (item) => `${item.title}`,
|
|
335
|
-
casing: "case-insensitive"
|
|
336
|
-
});
|
|
337
|
-
try {
|
|
338
|
-
const { fn } = await prompts({
|
|
339
|
-
name: "fn",
|
|
340
|
-
message: "Choose your git commit:",
|
|
341
|
-
type: "autocomplete",
|
|
342
|
-
choices: profileChoices,
|
|
343
|
-
async suggest(input, choices) {
|
|
344
|
-
const results = fzf.find(input);
|
|
345
|
-
return results.map((r) => choices.find((x) => x.value === r.item.title));
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
if (!fn) {
|
|
349
|
-
consola.log(colors.dim("Cancelled!"));
|
|
350
|
-
process.exit(1);
|
|
351
|
-
}
|
|
352
|
-
const entry = suggestions.find((x) => x.subject === fn);
|
|
353
|
-
if (!entry) {
|
|
354
|
-
consola.error("No matching commit message found");
|
|
355
|
-
process.exit(1);
|
|
356
|
-
}
|
|
357
|
-
return entry;
|
|
358
|
-
} catch (ex) {
|
|
359
|
-
consola.error("Failed to run : exit", ex);
|
|
109
|
+
consola.error("Failed to commit changes:");
|
|
110
|
+
consola.error(error);
|
|
360
111
|
process.exit(1);
|
|
361
112
|
}
|
|
362
113
|
}
|
|
@@ -391,6 +142,11 @@ function formatDate(date) {
|
|
|
391
142
|
}
|
|
392
143
|
|
|
393
144
|
const execAsync = promisify(exec);
|
|
145
|
+
const JOURNAL_TYPES = {
|
|
146
|
+
DAILY_NOTES: "daily-notes",
|
|
147
|
+
MEETING: "meeting",
|
|
148
|
+
NOTE: "note"
|
|
149
|
+
};
|
|
394
150
|
function ensureDirectoryExists(folderPath) {
|
|
395
151
|
if (!existsSync(folderPath)) {
|
|
396
152
|
consola.info(`Creating journal directory: ${colors.cyan(folderPath)}`);
|
|
@@ -413,6 +169,44 @@ function createJournalContent({ mondayDate }) {
|
|
|
413
169
|
content.push(``);
|
|
414
170
|
return content.join("\n");
|
|
415
171
|
}
|
|
172
|
+
function createMeetingContent({ title, date }) {
|
|
173
|
+
const dateStr = formatDate(date);
|
|
174
|
+
const timeStr = date.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" });
|
|
175
|
+
const content = [`# Meeting: ${title || "Meeting"}`];
|
|
176
|
+
content.push(``);
|
|
177
|
+
content.push(`**Date:** ${dateStr}`);
|
|
178
|
+
content.push(`**Time:** ${timeStr}`);
|
|
179
|
+
content.push(`**Attendees:** `);
|
|
180
|
+
content.push(``);
|
|
181
|
+
content.push(`## Agenda`);
|
|
182
|
+
content.push(``);
|
|
183
|
+
content.push(`- `);
|
|
184
|
+
content.push(``);
|
|
185
|
+
content.push(`## Notes`);
|
|
186
|
+
content.push(``);
|
|
187
|
+
content.push(`## Action Items`);
|
|
188
|
+
content.push(``);
|
|
189
|
+
content.push(`- [ ] `);
|
|
190
|
+
content.push(``);
|
|
191
|
+
content.push(`## Follow-up`);
|
|
192
|
+
content.push(``);
|
|
193
|
+
return content.join("\n");
|
|
194
|
+
}
|
|
195
|
+
function createNoteContent({ title, date }) {
|
|
196
|
+
const dateStr = formatDate(date);
|
|
197
|
+
const timeStr = date.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" });
|
|
198
|
+
const content = [`# ${title || "Note"}`];
|
|
199
|
+
content.push(``);
|
|
200
|
+
content.push(`**Created:** ${dateStr} ${timeStr}`);
|
|
201
|
+
content.push(``);
|
|
202
|
+
content.push(`## Summary`);
|
|
203
|
+
content.push(``);
|
|
204
|
+
content.push(`## Details`);
|
|
205
|
+
content.push(``);
|
|
206
|
+
content.push(`## References`);
|
|
207
|
+
content.push(``);
|
|
208
|
+
return content.join("\n");
|
|
209
|
+
}
|
|
416
210
|
async function openInEditor({ editor, filePath }) {
|
|
417
211
|
try {
|
|
418
212
|
await execAsync(`"${editor}" "${filePath}"`);
|
|
@@ -420,35 +214,87 @@ async function openInEditor({ editor, filePath }) {
|
|
|
420
214
|
consola.warn(`Could not open in editor : '${editor}'. Modify your editor in the config: examples include 'code', 'code-insiders', etc...`, ex);
|
|
421
215
|
}
|
|
422
216
|
}
|
|
423
|
-
|
|
217
|
+
function generateJournalFileInfoByType({ date = /* @__PURE__ */ new Date(), type = JOURNAL_TYPES.DAILY_NOTES, title }) {
|
|
218
|
+
const currentDate = new Date(date);
|
|
219
|
+
const year = currentDate.getFullYear().toString();
|
|
220
|
+
switch (type) {
|
|
221
|
+
case JOURNAL_TYPES.DAILY_NOTES: {
|
|
222
|
+
const monday = getMondayOfWeek(currentDate);
|
|
223
|
+
const fileName = `${formatDate(monday)}-week-log.md`;
|
|
224
|
+
const pathPrefix = [year, "daily-notes"];
|
|
225
|
+
return { pathPrefix, fileName, mondayDate: monday };
|
|
226
|
+
}
|
|
227
|
+
case JOURNAL_TYPES.MEETING: {
|
|
228
|
+
const dateStr = formatDate(currentDate);
|
|
229
|
+
const timeStr = currentDate.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }).replace(":", "");
|
|
230
|
+
const titleSlug = title ? `-${title.toLowerCase().replace(/\s+/g, "-")}` : "";
|
|
231
|
+
const fileName = `${dateStr}-${timeStr}-meeting${titleSlug}.md`;
|
|
232
|
+
const pathPrefix = [year, "meetings"];
|
|
233
|
+
return { pathPrefix, fileName, mondayDate: currentDate };
|
|
234
|
+
}
|
|
235
|
+
case JOURNAL_TYPES.NOTE: {
|
|
236
|
+
const dateStr = formatDate(currentDate);
|
|
237
|
+
const timeStr = currentDate.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }).replace(":", "");
|
|
238
|
+
const titleSlug = title ? `-${title.toLowerCase().replace(/\s+/g, "-")}` : "";
|
|
239
|
+
const fileName = `${dateStr}-${timeStr}-note${titleSlug}.md`;
|
|
240
|
+
const pathPrefix = [year, "notes"];
|
|
241
|
+
return { pathPrefix, fileName, mondayDate: currentDate };
|
|
242
|
+
}
|
|
243
|
+
default:
|
|
244
|
+
throw new Error(`Unknown journal type: ${type}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function createJournalFile({ userConfig, type, title }) {
|
|
424
248
|
try {
|
|
425
|
-
const
|
|
249
|
+
const currentDate = /* @__PURE__ */ new Date();
|
|
250
|
+
const fileInfo = generateJournalFileInfoByType({ date: currentDate, type, title });
|
|
426
251
|
const filePath = path__default.join(userConfig.journalDir, ...fileInfo.pathPrefix, fileInfo.fileName);
|
|
427
252
|
ensureDirectoryExists(path__default.join(userConfig.journalDir, ...fileInfo.pathPrefix));
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
writeFileSync(filePath, content, "utf8");
|
|
431
|
-
consola.info(`Created new journal file: ${colors.cyan(filePath)}`);
|
|
253
|
+
if (existsSync(filePath)) {
|
|
254
|
+
consola.info(`Opening existing ${type} file: ${colors.cyan(filePath)}`);
|
|
432
255
|
} else {
|
|
433
|
-
|
|
256
|
+
let content;
|
|
257
|
+
switch (type) {
|
|
258
|
+
case JOURNAL_TYPES.DAILY_NOTES:
|
|
259
|
+
content = createJournalContent({ mondayDate: fileInfo.mondayDate });
|
|
260
|
+
break;
|
|
261
|
+
case JOURNAL_TYPES.MEETING:
|
|
262
|
+
content = createMeetingContent({ title, date: currentDate });
|
|
263
|
+
break;
|
|
264
|
+
case JOURNAL_TYPES.NOTE:
|
|
265
|
+
content = createNoteContent({ title, date: currentDate });
|
|
266
|
+
break;
|
|
267
|
+
default:
|
|
268
|
+
throw new Error(`Unknown journal type: ${type}`);
|
|
269
|
+
}
|
|
270
|
+
writeFileSync(filePath, content, "utf8");
|
|
271
|
+
consola.info(`Created new ${type} file: ${colors.cyan(filePath)}`);
|
|
434
272
|
}
|
|
435
273
|
await openInEditor({ editor: userConfig.editor, filePath });
|
|
436
274
|
} catch (error) {
|
|
437
|
-
consola.warn(
|
|
275
|
+
consola.warn(`Error creating ${type} file:`, error);
|
|
438
276
|
process.exit(1);
|
|
439
277
|
}
|
|
440
278
|
}
|
|
441
|
-
function generateJournalFileInfo(date = /* @__PURE__ */ new Date()) {
|
|
442
|
-
const monday = getMondayOfWeek(new Date(date));
|
|
443
|
-
const fileName = `${formatDate(monday)}-week.md`;
|
|
444
|
-
const pathPrefix = [`${date.getFullYear()}`, "journal"];
|
|
445
|
-
return { pathPrefix, fileName, mondayDate: monday };
|
|
446
|
-
}
|
|
447
279
|
|
|
448
280
|
const constants = {
|
|
449
281
|
toolName: "towles-tool"
|
|
450
282
|
};
|
|
451
283
|
|
|
284
|
+
function printJson(obj) {
|
|
285
|
+
consola.log(util.inspect(obj, {
|
|
286
|
+
depth: 2,
|
|
287
|
+
colors: true,
|
|
288
|
+
showHidden: false,
|
|
289
|
+
compact: false
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
function printDebug(message, ...args) {
|
|
293
|
+
if (process.env.DEBUG) {
|
|
294
|
+
consola.debug(`DEBUG: ${message}`, ...args);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
452
298
|
function getDefaultUserConfig() {
|
|
453
299
|
return {
|
|
454
300
|
journalDir: path.join(homedir(), "journal"),
|
|
@@ -519,11 +365,18 @@ async function main() {
|
|
|
519
365
|
const config = await loadTowlesToolConfig({ cwd: process.cwd() });
|
|
520
366
|
const program = new Command();
|
|
521
367
|
program.name(constants.toolName).description("One off quality of life scripts that I use on a daily basis").version(version);
|
|
522
|
-
program.command("
|
|
523
|
-
|
|
368
|
+
const journalCmd = program.command("journal").description("quickly create md files from templates files like daily-notes, meeting, notes, etc.");
|
|
369
|
+
journalCmd.command("daily-notes").description("Weekly files with daily sections for ongoing work and notes").action(async () => {
|
|
370
|
+
await createJournalFile({ userConfig: config.userConfig, type: JOURNAL_TYPES.DAILY_NOTES });
|
|
371
|
+
});
|
|
372
|
+
journalCmd.command("meeting [title]").description("Structured meeting notes with agenda and action items").action(async (title) => {
|
|
373
|
+
await createJournalFile({ userConfig: config.userConfig, type: JOURNAL_TYPES.MEETING, title });
|
|
374
|
+
});
|
|
375
|
+
journalCmd.command("note [title]").description("General-purpose notes with structured sections").action(async (title) => {
|
|
376
|
+
await createJournalFile({ userConfig: config.userConfig, type: JOURNAL_TYPES.NOTE, title });
|
|
524
377
|
});
|
|
525
|
-
program.command("git-commit").alias("gc").description("Git commit command").action(async () => {
|
|
526
|
-
await gitCommitCommand(config);
|
|
378
|
+
program.command("git-commit [message...]").alias("gc").description("Git commit command with optional message").action(async (message) => {
|
|
379
|
+
await gitCommitCommand(config, message);
|
|
527
380
|
});
|
|
528
381
|
program.command("config").description("set or show configuration file.").action(async () => {
|
|
529
382
|
consola$1.log(colors.green("Showing configuration..."));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towles/tool",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.11
|
|
4
|
+
"version": "0.0.11",
|
|
5
5
|
"description": "One off quality of life scripts that I use on a daily basis.",
|
|
6
6
|
"author": "Chris Towles <Chris.Towles.Dev@gmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -47,14 +47,13 @@
|
|
|
47
47
|
"zod": "^4.0.5"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@antfu/eslint-config": "^4.16.2",
|
|
51
50
|
"@antfu/ni": "^25.0.0",
|
|
52
51
|
"@antfu/utils": "^9.2.0",
|
|
53
52
|
"@types/node": "^22.16.3",
|
|
54
53
|
"@types/prompts": "^2.4.9",
|
|
55
54
|
"bumpp": "^10.2.0",
|
|
56
|
-
"eslint": "^9.31.0",
|
|
57
55
|
"lint-staged": "^15.5.2",
|
|
56
|
+
"oxlint": "^1.7.0",
|
|
58
57
|
"simple-git-hooks": "^2.13.0",
|
|
59
58
|
"tinyexec": "^0.3.2",
|
|
60
59
|
"tsx": "^4.20.3",
|
|
@@ -69,7 +68,7 @@
|
|
|
69
68
|
"pre-commit": "pnpm i --frozen-lockfile --ignore-scripts --offline && npx lint-staged"
|
|
70
69
|
},
|
|
71
70
|
"lint-staged": {
|
|
72
|
-
"*": "
|
|
71
|
+
"*": "oxlint --fix"
|
|
73
72
|
},
|
|
74
73
|
"directories": {
|
|
75
74
|
"test": "test"
|
|
@@ -77,9 +76,9 @@
|
|
|
77
76
|
"scripts": {
|
|
78
77
|
"build": "unbuild",
|
|
79
78
|
"dev": "unbuild --stub",
|
|
80
|
-
"lint": "
|
|
81
|
-
"lint:fix": "
|
|
82
|
-
"lint:fix_all": "
|
|
79
|
+
"lint": "oxlint",
|
|
80
|
+
"lint:fix": "oxlint --fix",
|
|
81
|
+
"lint:fix_all": "oxlint --fix .",
|
|
83
82
|
"release:local": "bumpp && pnpm publish --no-git-checks -r --access public",
|
|
84
83
|
"release": "bumpp && echo \"github action will run and publish to npm\"",
|
|
85
84
|
"start": "tsx src/index.ts",
|