@towles/tool 0.0.20 → 0.0.41

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.md +9 -10
  3. package/README.md +121 -78
  4. package/bin/run.ts +5 -0
  5. package/package.json +63 -53
  6. package/patches/prompts.patch +34 -0
  7. package/src/commands/base.ts +42 -0
  8. package/src/commands/config.test.ts +15 -0
  9. package/src/commands/config.ts +43 -0
  10. package/src/commands/doctor.ts +133 -0
  11. package/src/commands/gh/branch-clean.ts +110 -0
  12. package/src/commands/gh/branch.test.ts +124 -0
  13. package/src/commands/gh/branch.ts +132 -0
  14. package/src/commands/gh/pr.ts +168 -0
  15. package/src/commands/index.ts +55 -0
  16. package/src/commands/install.ts +148 -0
  17. package/src/commands/journal/daily-notes.ts +66 -0
  18. package/src/commands/journal/meeting.ts +83 -0
  19. package/src/commands/journal/note.ts +83 -0
  20. package/src/commands/journal/utils.ts +399 -0
  21. package/src/commands/observe/graph.test.ts +89 -0
  22. package/src/commands/observe/graph.ts +1640 -0
  23. package/src/commands/observe/report.ts +166 -0
  24. package/src/commands/observe/session.ts +385 -0
  25. package/src/commands/observe/setup.ts +180 -0
  26. package/src/commands/observe/status.ts +146 -0
  27. package/src/commands/ralph/lib/execution.ts +302 -0
  28. package/src/commands/ralph/lib/formatter.ts +298 -0
  29. package/src/commands/ralph/lib/index.ts +4 -0
  30. package/src/commands/ralph/lib/marker.ts +108 -0
  31. package/src/commands/ralph/lib/state.ts +191 -0
  32. package/src/commands/ralph/marker/create.ts +23 -0
  33. package/src/commands/ralph/plan.ts +73 -0
  34. package/src/commands/ralph/progress.ts +44 -0
  35. package/src/commands/ralph/ralph.test.ts +673 -0
  36. package/src/commands/ralph/run.ts +408 -0
  37. package/src/commands/ralph/task/add.ts +105 -0
  38. package/src/commands/ralph/task/done.ts +73 -0
  39. package/src/commands/ralph/task/list.test.ts +48 -0
  40. package/src/commands/ralph/task/list.ts +110 -0
  41. package/src/commands/ralph/task/remove.ts +62 -0
  42. package/src/config/context.ts +7 -0
  43. package/src/config/settings.ts +155 -0
  44. package/src/constants.ts +3 -0
  45. package/src/types/journal.ts +16 -0
  46. package/src/utils/anthropic/types.ts +158 -0
  47. package/src/utils/date-utils.test.ts +96 -0
  48. package/src/utils/date-utils.ts +54 -0
  49. package/src/utils/exec.ts +8 -0
  50. package/src/utils/git/gh-cli-wrapper.test.ts +14 -0
  51. package/src/utils/git/gh-cli-wrapper.ts +54 -0
  52. package/src/utils/git/git-wrapper.test.ts +26 -0
  53. package/src/utils/git/git-wrapper.ts +15 -0
  54. package/src/utils/git/git.ts +25 -0
  55. package/src/utils/render.test.ts +71 -0
  56. package/src/utils/render.ts +34 -0
  57. package/dist/index.d.mts +0 -1
  58. package/dist/index.mjs +0 -805
@@ -0,0 +1,55 @@
1
+ // Explicit command exports for Bun compiled binaries
2
+ // oclif's pattern-based discovery doesn't work with bundled executables
3
+
4
+ import GhBranchClean from "./gh/branch-clean.js";
5
+ import Config from "./config.js";
6
+ import Doctor from "./doctor.js";
7
+ import GhBranch from "./gh/branch.js";
8
+ import GhPr from "./gh/pr.js";
9
+ import Install from "./install.js";
10
+ import RalphRun from "./ralph/run.js";
11
+ import RalphPlan from "./ralph/plan.js";
12
+ import RalphProgress from "./ralph/progress.js";
13
+ import RalphMarkerCreate from "./ralph/marker/create.js";
14
+ import RalphTaskAdd from "./ralph/task/add.js";
15
+ import RalphTaskDone from "./ralph/task/done.js";
16
+ import RalphTaskList from "./ralph/task/list.js";
17
+ import RalphTaskRemove from "./ralph/task/remove.js";
18
+ import JournalDailyNotes from "./journal/daily-notes.js";
19
+ import JournalMeeting from "./journal/meeting.js";
20
+ import JournalNote from "./journal/note.js";
21
+ import ObserveSetup from "./observe/setup.js";
22
+ import ObserveStatus from "./observe/status.js";
23
+ import ObserveReport from "./observe/report.js";
24
+ import ObserveGraph from "./observe/graph.js";
25
+ import ObserveSession from "./observe/session.js";
26
+
27
+ export default {
28
+ config: Config,
29
+ doctor: Doctor,
30
+ "gh:branch": GhBranch,
31
+ "gh:branch-clean": GhBranchClean,
32
+ "gh:pr": GhPr,
33
+ install: Install,
34
+ "ralph:run": RalphRun,
35
+ "ralph:plan": RalphPlan,
36
+ "ralph:progress": RalphProgress,
37
+ "ralph:marker:create": RalphMarkerCreate,
38
+ "ralph:task:add": RalphTaskAdd,
39
+ "ralph:task:done": RalphTaskDone,
40
+ "ralph:task:list": RalphTaskList,
41
+ "ralph:task:remove": RalphTaskRemove,
42
+ "journal:daily-notes": JournalDailyNotes,
43
+ "journal:meeting": JournalMeeting,
44
+ "journal:note": JournalNote,
45
+ "observe:setup": ObserveSetup,
46
+ "observe:status": ObserveStatus,
47
+ "observe:report": ObserveReport,
48
+ "observe:graph": ObserveGraph,
49
+ "observe:session": ObserveSession,
50
+ // Aliases
51
+ graph: ObserveGraph,
52
+ today: JournalDailyNotes,
53
+ pr: GhPr,
54
+ run: RalphRun,
55
+ };
@@ -0,0 +1,148 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { Flags } from "@oclif/core";
5
+ import pc from "picocolors";
6
+ import consola from "consola";
7
+ import { BaseCommand } from "./base.js";
8
+
9
+ const CLAUDE_SETTINGS_PATH = path.join(homedir(), ".claude", "settings.json");
10
+
11
+ interface ClaudeSettings {
12
+ cleanupPeriodDays?: number;
13
+ alwaysThinkingEnabled?: boolean;
14
+ hooks?: Record<string, unknown[]>;
15
+ [key: string]: unknown;
16
+ }
17
+
18
+ /**
19
+ * Install and configure towles-tool with Claude Code
20
+ */
21
+ export default class Install extends BaseCommand {
22
+ static override description =
23
+ "Configure Claude Code settings and optionally enable observability";
24
+
25
+ static override examples = [
26
+ "<%= config.bin %> install",
27
+ "<%= config.bin %> install --observability",
28
+ ];
29
+
30
+ static override flags = {
31
+ ...BaseCommand.baseFlags,
32
+ observability: Flags.boolean({
33
+ char: "o",
34
+ description: "Show OTEL setup instructions and configure SubagentStop hook",
35
+ default: false,
36
+ }),
37
+ };
38
+
39
+ async run(): Promise<void> {
40
+ const { flags } = await this.parse(Install);
41
+
42
+ this.log(pc.bold("\n🔧 towles-tool install\n"));
43
+
44
+ // Load or create Claude settings
45
+ let claudeSettings: ClaudeSettings = {};
46
+ if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
47
+ try {
48
+ const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
49
+ claudeSettings = JSON.parse(content);
50
+ this.log(pc.dim(`Found existing Claude settings at ${CLAUDE_SETTINGS_PATH}`));
51
+ } catch {
52
+ this.log(
53
+ pc.yellow(`Warning: Could not parse ${CLAUDE_SETTINGS_PATH}, will create fresh settings`),
54
+ );
55
+ }
56
+ } else {
57
+ this.log(pc.dim(`No Claude settings file found, will create one`));
58
+ }
59
+
60
+ // Configure recommended settings
61
+ let modified = false;
62
+
63
+ // Prevent log deletion (set to ~274 years)
64
+ if (claudeSettings.cleanupPeriodDays !== 99999) {
65
+ claudeSettings.cleanupPeriodDays = 99999;
66
+ modified = true;
67
+ this.log(pc.green("✓ Set cleanupPeriodDays: 99999 (prevent log deletion)"));
68
+ } else {
69
+ this.log(pc.dim("✓ cleanupPeriodDays already set to 99999"));
70
+ }
71
+
72
+ // Enable thinking by default
73
+ if (claudeSettings.alwaysThinkingEnabled !== true) {
74
+ claudeSettings.alwaysThinkingEnabled = true;
75
+ modified = true;
76
+ this.log(pc.green("✓ Set alwaysThinkingEnabled: true"));
77
+ } else {
78
+ this.log(pc.dim("✓ alwaysThinkingEnabled already set to true"));
79
+ }
80
+
81
+ // Save settings if modified
82
+ if (modified) {
83
+ this.saveClaudeSettings(claudeSettings);
84
+ this.log(pc.green(`\n✓ Saved Claude settings to ${CLAUDE_SETTINGS_PATH}`));
85
+ }
86
+
87
+ // Show observability setup if requested
88
+ if (flags.observability) {
89
+ this.log(pc.bold("\n📊 Observability Setup\n"));
90
+ this.showOtelInstructions();
91
+ }
92
+
93
+ this.log(pc.bold(pc.green("\n✅ Installation complete!\n")));
94
+
95
+ // Offer to install plugins from marketplace
96
+ this.log(pc.cyan("To install plugins from the Claude Code marketplace:"));
97
+ this.log(
98
+ pc.dim(" claude /plugins marketplace add https://github.com/ChrisTowles/towles-tool"),
99
+ );
100
+ this.log("");
101
+
102
+ const answer = await consola.prompt("Install tt-core plugin from marketplace now?", {
103
+ type: "confirm",
104
+ initial: true,
105
+ });
106
+
107
+ if (answer) {
108
+ const { x } = await import("tinyexec");
109
+ this.log("");
110
+
111
+ const result = await x("claude", ["plugin", "install", "tt@towles-tool", "--scope", "user"]);
112
+ if (result.stdout) this.log(result.stdout);
113
+ if (result.stderr) this.log(pc.dim(result.stderr));
114
+ if (result.exitCode === 0) {
115
+ this.log(pc.green("✓ tt-core plugin installed"));
116
+ } else {
117
+ this.log(pc.yellow(`Command exited with code ${result.exitCode}`));
118
+ }
119
+ }
120
+
121
+ this.log("");
122
+ }
123
+
124
+ private saveClaudeSettings(settings: ClaudeSettings): void {
125
+ const dir = path.dirname(CLAUDE_SETTINGS_PATH);
126
+ if (!fs.existsSync(dir)) {
127
+ fs.mkdirSync(dir, { recursive: true });
128
+ }
129
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
130
+ }
131
+
132
+ private showOtelInstructions(): void {
133
+ this.log(pc.cyan("Add these environment variables to your shell profile:\n"));
134
+
135
+ consola.box(`export CLAUDE_CODE_ENABLE_TELEMETRY=1
136
+ export OTEL_METRICS_EXPORTER=otlp
137
+ export OTEL_LOGS_EXPORTER=otlp
138
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317`);
139
+
140
+ this.log("");
141
+ this.log(
142
+ pc.dim("For more info, see: https://github.com/anthropics/claude-code-monitoring-guide"),
143
+ );
144
+ this.log("");
145
+ this.log(pc.cyan("Quick cost analysis (no setup required):"));
146
+ this.log(pc.dim(" npx ccusage@latest --breakdown"));
147
+ }
148
+ }
@@ -0,0 +1,66 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import consola from "consola";
5
+ import { colors } from "consola/utils";
6
+ import { BaseCommand } from "../base.js";
7
+ import { JOURNAL_TYPES } from "../../types/journal.js";
8
+ import {
9
+ createJournalContent,
10
+ ensureDirectoryExists,
11
+ ensureTemplatesExist,
12
+ generateJournalFileInfoByType,
13
+ openInEditor,
14
+ } from "./utils.js";
15
+
16
+ /**
17
+ * Create or open daily notes journal file
18
+ */
19
+ export default class DailyNotes extends BaseCommand {
20
+ static override description = "Weekly files with daily sections for ongoing work and notes";
21
+
22
+ static override examples = [
23
+ "<%= config.bin %> journal daily-notes",
24
+ "<%= config.bin %> journal today",
25
+ ];
26
+
27
+ async run(): Promise<void> {
28
+ await this.parse(DailyNotes);
29
+
30
+ try {
31
+ const journalSettings = this.settings.settingsFile.settings.journalSettings;
32
+ const templateDir = journalSettings.templateDir;
33
+
34
+ // Ensure templates exist on first run
35
+ ensureTemplatesExist(templateDir);
36
+
37
+ const currentDate = new Date();
38
+ const fileInfo = generateJournalFileInfoByType({
39
+ journalSettings,
40
+ date: currentDate,
41
+ type: JOURNAL_TYPES.DAILY_NOTES,
42
+ title: "",
43
+ });
44
+
45
+ // Ensure journal directory exists
46
+ ensureDirectoryExists(path.dirname(fileInfo.fullPath));
47
+
48
+ if (existsSync(fileInfo.fullPath)) {
49
+ consola.info(`Opening existing daily-notes file: ${colors.cyan(fileInfo.fullPath)}`);
50
+ } else {
51
+ const content = createJournalContent({ mondayDate: fileInfo.mondayDate, templateDir });
52
+ consola.info(`Creating new daily-notes file: ${colors.cyan(fileInfo.fullPath)}`);
53
+ writeFileSync(fileInfo.fullPath, content, "utf8");
54
+ }
55
+
56
+ await openInEditor({
57
+ editor: this.settings.settingsFile.settings.preferredEditor,
58
+ filePath: fileInfo.fullPath,
59
+ folderPath: journalSettings.baseFolder,
60
+ });
61
+ } catch (error) {
62
+ consola.warn(`Error creating daily-notes file:`, error);
63
+ process.exit(1);
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,83 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { Args } from "@oclif/core";
5
+ import consola from "consola";
6
+ import { colors } from "consola/utils";
7
+ import { BaseCommand } from "../base.js";
8
+ import { JOURNAL_TYPES } from "../../types/journal.js";
9
+ import {
10
+ createMeetingContent,
11
+ ensureDirectoryExists,
12
+ ensureTemplatesExist,
13
+ generateJournalFileInfoByType,
14
+ openInEditor,
15
+ } from "./utils.js";
16
+
17
+ /**
18
+ * Create or open meeting notes file
19
+ */
20
+ export default class Meeting extends BaseCommand {
21
+ static override description = "Structured meeting notes with agenda and action items";
22
+
23
+ static override args = {
24
+ title: Args.string({
25
+ description: "Meeting title",
26
+ required: false,
27
+ }),
28
+ };
29
+
30
+ static override examples = [
31
+ "<%= config.bin %> journal meeting",
32
+ '<%= config.bin %> journal meeting "Sprint Planning"',
33
+ '<%= config.bin %> journal m "Standup"',
34
+ ];
35
+
36
+ async run(): Promise<void> {
37
+ const { args } = await this.parse(Meeting);
38
+
39
+ try {
40
+ const journalSettings = this.settings.settingsFile.settings.journalSettings;
41
+ const templateDir = journalSettings.templateDir;
42
+
43
+ // Ensure templates exist on first run
44
+ ensureTemplatesExist(templateDir);
45
+
46
+ // Prompt for title if not provided
47
+ let title = args.title || "";
48
+ if (title.trim().length === 0) {
49
+ title = await consola.prompt(`Enter meeting title:`, {
50
+ type: "text",
51
+ });
52
+ }
53
+
54
+ const currentDate = new Date();
55
+ const fileInfo = generateJournalFileInfoByType({
56
+ journalSettings,
57
+ date: currentDate,
58
+ type: JOURNAL_TYPES.MEETING,
59
+ title,
60
+ });
61
+
62
+ // Ensure journal directory exists
63
+ ensureDirectoryExists(path.dirname(fileInfo.fullPath));
64
+
65
+ if (existsSync(fileInfo.fullPath)) {
66
+ consola.info(`Opening existing meeting file: ${colors.cyan(fileInfo.fullPath)}`);
67
+ } else {
68
+ const content = createMeetingContent({ title, date: currentDate, templateDir });
69
+ consola.info(`Creating new meeting file: ${colors.cyan(fileInfo.fullPath)}`);
70
+ writeFileSync(fileInfo.fullPath, content, "utf8");
71
+ }
72
+
73
+ await openInEditor({
74
+ editor: this.settings.settingsFile.settings.preferredEditor,
75
+ filePath: fileInfo.fullPath,
76
+ folderPath: journalSettings.baseFolder,
77
+ });
78
+ } catch (error) {
79
+ consola.warn(`Error creating meeting file:`, error);
80
+ process.exit(1);
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,83 @@
1
+ import { existsSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { Args } from "@oclif/core";
5
+ import consola from "consola";
6
+ import { colors } from "consola/utils";
7
+ import { BaseCommand } from "../base.js";
8
+ import { JOURNAL_TYPES } from "../../types/journal.js";
9
+ import {
10
+ createNoteContent,
11
+ ensureDirectoryExists,
12
+ ensureTemplatesExist,
13
+ generateJournalFileInfoByType,
14
+ openInEditor,
15
+ } from "./utils.js";
16
+
17
+ /**
18
+ * Create or open general-purpose note file
19
+ */
20
+ export default class Note extends BaseCommand {
21
+ static override description = "General-purpose notes with structured sections";
22
+
23
+ static override args = {
24
+ title: Args.string({
25
+ description: "Note title",
26
+ required: false,
27
+ }),
28
+ };
29
+
30
+ static override examples = [
31
+ "<%= config.bin %> journal note",
32
+ '<%= config.bin %> journal note "Research Notes"',
33
+ '<%= config.bin %> journal n "Ideas"',
34
+ ];
35
+
36
+ async run(): Promise<void> {
37
+ const { args } = await this.parse(Note);
38
+
39
+ try {
40
+ const journalSettings = this.settings.settingsFile.settings.journalSettings;
41
+ const templateDir = journalSettings.templateDir;
42
+
43
+ // Ensure templates exist on first run
44
+ ensureTemplatesExist(templateDir);
45
+
46
+ // Prompt for title if not provided
47
+ let title = args.title || "";
48
+ if (title.trim().length === 0) {
49
+ title = await consola.prompt(`Enter note title:`, {
50
+ type: "text",
51
+ });
52
+ }
53
+
54
+ const currentDate = new Date();
55
+ const fileInfo = generateJournalFileInfoByType({
56
+ journalSettings,
57
+ date: currentDate,
58
+ type: JOURNAL_TYPES.NOTE,
59
+ title,
60
+ });
61
+
62
+ // Ensure journal directory exists
63
+ ensureDirectoryExists(path.dirname(fileInfo.fullPath));
64
+
65
+ if (existsSync(fileInfo.fullPath)) {
66
+ consola.info(`Opening existing note file: ${colors.cyan(fileInfo.fullPath)}`);
67
+ } else {
68
+ const content = createNoteContent({ title, date: currentDate, templateDir });
69
+ consola.info(`Creating new note file: ${colors.cyan(fileInfo.fullPath)}`);
70
+ writeFileSync(fileInfo.fullPath, content, "utf8");
71
+ }
72
+
73
+ await openInEditor({
74
+ editor: this.settings.settingsFile.settings.preferredEditor,
75
+ filePath: fileInfo.fullPath,
76
+ folderPath: journalSettings.baseFolder,
77
+ });
78
+ } catch (error) {
79
+ consola.warn(`Error creating note file:`, error);
80
+ process.exit(1);
81
+ }
82
+ }
83
+ }