@towles/tool 0.0.95 → 0.0.103
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/bin/run.ts +4 -3
- package/package.json +8 -37
- package/src/cli.ts +19 -0
- package/src/commands/agentboard.ts +327 -0
- package/src/commands/auto-claude/index.ts +74 -91
- package/src/commands/auto-claude/list.ts +33 -43
- package/src/commands/auto-claude/retry.test.ts +10 -6
- package/src/commands/auto-claude/retry.ts +26 -39
- package/src/commands/auto-claude/status.ts +10 -17
- package/src/commands/config.test.ts +4 -10
- package/src/commands/config.ts +14 -28
- package/src/commands/doctor.ts +156 -178
- package/src/commands/gh/branch-clean.ts +28 -43
- package/src/commands/gh/branch.ts +22 -37
- package/src/commands/gh/index.ts +10 -0
- package/src/commands/gh/pr.ts +82 -100
- package/src/commands/graph/index.ts +59 -70
- package/src/commands/install.ts +91 -115
- package/src/commands/journal/daily-notes.ts +16 -24
- package/src/commands/journal/index.ts +10 -0
- package/src/commands/journal/meeting.ts +16 -34
- package/src/commands/journal/note.ts +16 -34
- package/src/commands/shared.ts +21 -0
- package/src/lib/auto-claude/templates.test.ts +16 -11
- package/src/lib/graph/parser.test.ts +11 -10
- package/src/utils/git/gh-cli-wrapper.test.ts +6 -5
- package/src/commands/agentboard2.ts +0 -280
- package/src/commands/base.ts +0 -32
package/bin/run.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towles/tool",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.103",
|
|
4
4
|
"description": "One off quality of life scripts that I use on a daily basis.",
|
|
5
5
|
"homepage": "https://github.com/ChrisTowles/towles-tool#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -29,19 +29,14 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"version:sync": "
|
|
33
|
-
"prepublishOnly": "
|
|
34
|
-
"dev": "
|
|
32
|
+
"version:sync": "bun run scripts/sync-versions.ts",
|
|
33
|
+
"prepublishOnly": "bun run version:sync",
|
|
34
|
+
"dev": "bun run bin/run.ts",
|
|
35
35
|
"format": "oxfmt --write .",
|
|
36
36
|
"format:check": "oxfmt --check .",
|
|
37
37
|
"lint": "oxlint",
|
|
38
38
|
"lint:fix": "oxlint --fix",
|
|
39
39
|
"test": "vitest run",
|
|
40
|
-
"test:prompts": "promptfoo eval && promptfoo eval -c plugins/tt-core/promptfooconfig.yaml && promptfoo eval -c plugins/tt-auto-claude/promptfooconfig.yaml",
|
|
41
|
-
"test:prompts:root": "promptfoo eval",
|
|
42
|
-
"test:prompts:tt-core": "promptfoo eval -c plugins/tt-core/promptfooconfig.yaml",
|
|
43
|
-
"test:prompts:tt-core:llm": "promptfoo eval -c plugins/tt-core/promptfooconfig.llm.yaml",
|
|
44
|
-
"test:prompts:tt-auto-claude": "promptfoo eval -c plugins/tt-auto-claude/promptfooconfig.yaml",
|
|
45
40
|
"test:watch": "CI=DisableCallingClaude vitest watch",
|
|
46
41
|
"typecheck": "tsgo --noEmit --incremental",
|
|
47
42
|
"prepare": "simple-git-hooks"
|
|
@@ -49,7 +44,7 @@
|
|
|
49
44
|
"dependencies": {
|
|
50
45
|
"@anthropic-ai/claude-code": "^2.1.4",
|
|
51
46
|
"@anthropic-ai/sdk": "^0.56.0",
|
|
52
|
-
"
|
|
47
|
+
"citty": "^0.1.6",
|
|
53
48
|
"consola": "^3.4.2",
|
|
54
49
|
"d3-hierarchy": "^3.1.2",
|
|
55
50
|
"fzf": "^0.5.2",
|
|
@@ -62,7 +57,6 @@
|
|
|
62
57
|
"zod": "^4.0.5"
|
|
63
58
|
},
|
|
64
59
|
"devDependencies": {
|
|
65
|
-
"@oclif/test": "^4.1.10",
|
|
66
60
|
"@types/d3-hierarchy": "^3.1.7",
|
|
67
61
|
"@types/luxon": "^3.6.2",
|
|
68
62
|
"@types/node": "^22.16.3",
|
|
@@ -71,38 +65,15 @@
|
|
|
71
65
|
"bumpp": "^10.4.0",
|
|
72
66
|
"oxfmt": "^0.24.0",
|
|
73
67
|
"oxlint": "^1.7.0",
|
|
74
|
-
"promptfoo": "^0.121.2",
|
|
75
68
|
"simple-git-hooks": "^2.13.0",
|
|
76
|
-
"tsx": "^4.19.4",
|
|
77
69
|
"typescript": "^5.8.3",
|
|
78
70
|
"vitest": "^4.0.17"
|
|
79
71
|
},
|
|
80
72
|
"simple-git-hooks": {
|
|
81
|
-
"pre-commit": "
|
|
73
|
+
"pre-commit": "bun run format && bun run lint:fix && bun run typecheck && claude plugin validate ."
|
|
82
74
|
},
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"commands": "./src/commands",
|
|
86
|
-
"dirname": "towles-tool",
|
|
87
|
-
"topicSeparator": " "
|
|
88
|
-
},
|
|
89
|
-
"packageManager": "pnpm@10.27.0",
|
|
90
|
-
"pnpm": {
|
|
91
|
-
"patchedDependencies": {
|
|
92
|
-
"prompts@2.4.2": "patches/prompts.patch"
|
|
93
|
-
},
|
|
94
|
-
"onlyBuiltDependencies": [
|
|
95
|
-
"@anthropic-ai/claude-code",
|
|
96
|
-
"@parcel/watcher",
|
|
97
|
-
"@playwright/browser-chromium",
|
|
98
|
-
"@swc/core",
|
|
99
|
-
"better-sqlite3",
|
|
100
|
-
"esbuild",
|
|
101
|
-
"onnxruntime-node",
|
|
102
|
-
"protobufjs",
|
|
103
|
-
"sharp",
|
|
104
|
-
"simple-git-hooks"
|
|
105
|
-
]
|
|
75
|
+
"patchedDependencies": {
|
|
76
|
+
"prompts@2.4.2": "patches/prompts.patch"
|
|
106
77
|
},
|
|
107
78
|
"trustedDependencies": [
|
|
108
79
|
"@anthropic-ai/claude-code"
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
|
|
3
|
+
export const main = defineCommand({
|
|
4
|
+
meta: { name: "tt", description: "towles-tool — personal CLI utilities" },
|
|
5
|
+
subCommands: {
|
|
6
|
+
config: () => import("./commands/config.js").then((m) => m.default),
|
|
7
|
+
doctor: () => import("./commands/doctor.js").then((m) => m.default),
|
|
8
|
+
install: () => import("./commands/install.js").then((m) => m.default),
|
|
9
|
+
agentboard: () => import("./commands/agentboard.js").then((m) => m.default),
|
|
10
|
+
ag: () => import("./commands/agentboard.js").then((m) => m.default),
|
|
11
|
+
gh: () => import("./commands/gh/index.js").then((m) => m.default),
|
|
12
|
+
pr: () => import("./commands/gh/pr.js").then((m) => m.default),
|
|
13
|
+
journal: () => import("./commands/journal/index.js").then((m) => m.default),
|
|
14
|
+
today: () => import("./commands/journal/daily-notes.js").then((m) => m.default),
|
|
15
|
+
"auto-claude": () => import("./commands/auto-claude/index.js").then((m) => m.default),
|
|
16
|
+
ac: () => import("./commands/auto-claude/index.js").then((m) => m.default),
|
|
17
|
+
graph: () => import("./commands/graph/index.js").then((m) => m.default),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import consola from "consola";
|
|
6
|
+
import { colors } from "consola/utils";
|
|
7
|
+
import { debugArg } from "./shared.js";
|
|
8
|
+
|
|
9
|
+
const SERVER_HOST = "127.0.0.1";
|
|
10
|
+
const SERVER_PORT = 4201;
|
|
11
|
+
|
|
12
|
+
const PLUGIN_DIR = resolve(import.meta.dirname, "../../plugins/tt-agentboard");
|
|
13
|
+
|
|
14
|
+
// Keybinding defaults
|
|
15
|
+
const DEFAULT_KEY = "a";
|
|
16
|
+
const TMUX_BINDINGS = { toggle: "t", focus: "s" } as const;
|
|
17
|
+
const RUN_SHELL_LINE = `run-shell '${PLUGIN_DIR}/agentboard.tmux'`;
|
|
18
|
+
const MARKER = "# agentboard";
|
|
19
|
+
|
|
20
|
+
function findTmuxConf(): string | null {
|
|
21
|
+
const candidates = [
|
|
22
|
+
resolve(process.env.HOME ?? "~", ".tmux.conf"),
|
|
23
|
+
resolve(process.env.HOME ?? "~", ".config/tmux/tmux.conf"),
|
|
24
|
+
];
|
|
25
|
+
for (const path of candidates) {
|
|
26
|
+
try {
|
|
27
|
+
const real = existsSync(path) ? path : null;
|
|
28
|
+
if (real) return real;
|
|
29
|
+
} catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureDeps(): void {
|
|
37
|
+
try {
|
|
38
|
+
execSync("bun --version", { stdio: "pipe" });
|
|
39
|
+
} catch {
|
|
40
|
+
consola.error("bun is required but not found. Install: https://bun.sh");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const runtimeNodeModules = resolve(PLUGIN_DIR, "packages/runtime/node_modules");
|
|
45
|
+
if (!existsSync(runtimeNodeModules)) {
|
|
46
|
+
consola.info("Installing agentboard dependencies...");
|
|
47
|
+
execSync("bun install", { cwd: PLUGIN_DIR, stdio: "inherit" });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function reloadTmux(): void {
|
|
52
|
+
try {
|
|
53
|
+
execSync(
|
|
54
|
+
"tmux source-file ~/.config/tmux/tmux.conf 2>/dev/null || tmux source-file ~/.tmux.conf 2>/dev/null",
|
|
55
|
+
{
|
|
56
|
+
stdio: "pipe",
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
consola.success("tmux config reloaded");
|
|
60
|
+
} catch {
|
|
61
|
+
consola.info("Reload tmux manually: tmux source-file ~/.config/tmux/tmux.conf");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function showKeys(): void {
|
|
66
|
+
let prefix = "C-a";
|
|
67
|
+
let key = DEFAULT_KEY;
|
|
68
|
+
try {
|
|
69
|
+
prefix = execSync("tmux show-option -gv prefix", {
|
|
70
|
+
encoding: "utf8",
|
|
71
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
72
|
+
}).trim();
|
|
73
|
+
const abKey = execSync(
|
|
74
|
+
`tmux show-option -gv @agentboard-key 2>/dev/null || echo ${DEFAULT_KEY}`,
|
|
75
|
+
{
|
|
76
|
+
encoding: "utf8",
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
78
|
+
},
|
|
79
|
+
).trim();
|
|
80
|
+
if (abKey) key = abKey;
|
|
81
|
+
} catch {
|
|
82
|
+
// use defaults
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { toggle, focus } = TMUX_BINDINGS;
|
|
86
|
+
consola.box(
|
|
87
|
+
[
|
|
88
|
+
`${colors.bold("AgentBoard Keybindings")}\n`,
|
|
89
|
+
`${colors.cyan(`tmux (prefix = ${prefix}, C = Ctrl):`)}`,
|
|
90
|
+
` ${prefix} ${key} ${toggle} toggle sidebar`,
|
|
91
|
+
` ${prefix} ${key} ${focus} focus sidebar`,
|
|
92
|
+
` ${prefix} ${key} 1-9 jump to session\n`,
|
|
93
|
+
`${colors.cyan("In sidebar:")}`,
|
|
94
|
+
` Tab cycle sessions`,
|
|
95
|
+
` j / ↓ move down`,
|
|
96
|
+
` k / ↑ move up`,
|
|
97
|
+
` Enter / l switch to selected session`,
|
|
98
|
+
` 1-9 jump to session`,
|
|
99
|
+
` d hide session`,
|
|
100
|
+
` x kill session`,
|
|
101
|
+
` r refresh`,
|
|
102
|
+
` ? help`,
|
|
103
|
+
` q quit`,
|
|
104
|
+
].join("\n"),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function setup(): void {
|
|
109
|
+
ensureDeps();
|
|
110
|
+
|
|
111
|
+
const confPath = findTmuxConf();
|
|
112
|
+
if (!confPath) {
|
|
113
|
+
consola.warn("No tmux.conf found. Add this line manually:");
|
|
114
|
+
consola.info(colors.cyan(` ${RUN_SHELL_LINE}`));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let editPath = confPath;
|
|
119
|
+
try {
|
|
120
|
+
editPath = realpathSync(confPath);
|
|
121
|
+
} catch {
|
|
122
|
+
// keep confPath
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const content = readFileSync(editPath, "utf8");
|
|
126
|
+
if (content.includes("agentboard.tmux")) {
|
|
127
|
+
consola.success("Already installed in tmux.conf");
|
|
128
|
+
reloadTmux();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const tpmLine = "run '~/.config/tmux/plugins/tpm/tpm'";
|
|
133
|
+
const altTpmLine = "run-shell '~/.tmux/plugins/tpm/tpm'";
|
|
134
|
+
const insertLines = `\n${MARKER}\n${RUN_SHELL_LINE}\n`;
|
|
135
|
+
|
|
136
|
+
let newContent: string;
|
|
137
|
+
if (content.includes(tpmLine)) {
|
|
138
|
+
newContent = content.replace(tpmLine, `${insertLines}\n${tpmLine}`);
|
|
139
|
+
} else if (content.includes(altTpmLine)) {
|
|
140
|
+
newContent = content.replace(altTpmLine, `${insertLines}\n${altTpmLine}`);
|
|
141
|
+
} else {
|
|
142
|
+
newContent = content + insertLines;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
writeFileSync(editPath, newContent);
|
|
146
|
+
consola.success(`Added agentboard to ${editPath}`);
|
|
147
|
+
|
|
148
|
+
reloadTmux();
|
|
149
|
+
showKeys();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function uninstall(): void {
|
|
153
|
+
const confPath = findTmuxConf();
|
|
154
|
+
if (!confPath) {
|
|
155
|
+
consola.info("No tmux.conf found.");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let editPath = confPath;
|
|
160
|
+
try {
|
|
161
|
+
editPath = realpathSync(confPath);
|
|
162
|
+
} catch {
|
|
163
|
+
// keep confPath
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const content = readFileSync(editPath, "utf8");
|
|
167
|
+
if (!content.includes("agentboard")) {
|
|
168
|
+
consola.info("agentboard not found in tmux.conf");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const newContent = content
|
|
173
|
+
.split("\n")
|
|
174
|
+
.filter((line) => !line.includes("agentboard"))
|
|
175
|
+
.join("\n")
|
|
176
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
177
|
+
|
|
178
|
+
writeFileSync(editPath, newContent);
|
|
179
|
+
consola.success("Removed agentboard from tmux.conf");
|
|
180
|
+
reloadTmux();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function startServer(): void {
|
|
184
|
+
ensureDeps();
|
|
185
|
+
|
|
186
|
+
const serverEntry = resolve(PLUGIN_DIR, "apps/server/src/main.ts");
|
|
187
|
+
consola.info("Starting agentboard server (foreground, Ctrl+C to stop)...");
|
|
188
|
+
|
|
189
|
+
execSync(`bun run ${serverEntry}`, {
|
|
190
|
+
stdio: "inherit",
|
|
191
|
+
cwd: PLUGIN_DIR,
|
|
192
|
+
env: {
|
|
193
|
+
...process.env,
|
|
194
|
+
AGENTBOARD_DIR: PLUGIN_DIR,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function serverAlive(): Promise<boolean> {
|
|
200
|
+
try {
|
|
201
|
+
const res = await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/`, {
|
|
202
|
+
signal: AbortSignal.timeout(500),
|
|
203
|
+
});
|
|
204
|
+
return res.ok;
|
|
205
|
+
} catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function ensureServerUp(): Promise<boolean> {
|
|
211
|
+
if (await serverAlive()) return true;
|
|
212
|
+
|
|
213
|
+
const serverEntry = resolve(PLUGIN_DIR, "apps/server/src/main.ts");
|
|
214
|
+
consola.info("Starting agentboard server...");
|
|
215
|
+
const child = spawn("bun", ["run", serverEntry], {
|
|
216
|
+
stdio: "ignore",
|
|
217
|
+
cwd: PLUGIN_DIR,
|
|
218
|
+
detached: true,
|
|
219
|
+
env: { ...process.env, AGENTBOARD_DIR: PLUGIN_DIR },
|
|
220
|
+
});
|
|
221
|
+
child.unref();
|
|
222
|
+
|
|
223
|
+
for (let i = 0; i < 30; i++) {
|
|
224
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
225
|
+
if (await serverAlive()) return true;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function restart(): Promise<void> {
|
|
231
|
+
ensureDeps();
|
|
232
|
+
|
|
233
|
+
// 1. Kill stash sessions left over from hidden sidebars
|
|
234
|
+
try {
|
|
235
|
+
const result = spawnSync("tmux", ["list-sessions", "-F", "#{session_name}"], {
|
|
236
|
+
encoding: "utf8",
|
|
237
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
238
|
+
});
|
|
239
|
+
const sessions = (result.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
240
|
+
for (const name of sessions) {
|
|
241
|
+
if (name.startsWith("_ab_stash")) {
|
|
242
|
+
spawnSync("tmux", ["kill-session", "-t", name], { stdio: "pipe" });
|
|
243
|
+
consola.info(`Killed stash session: ${name}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
// no tmux or no sessions
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 2. Ensure server is running
|
|
251
|
+
if (!(await ensureServerUp())) {
|
|
252
|
+
consola.error("Failed to start agentboard server");
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
consola.success("Server is running");
|
|
256
|
+
|
|
257
|
+
// 3. Toggle sidebar on (the server spawns sidebars in all active windows)
|
|
258
|
+
try {
|
|
259
|
+
const res = await fetch(`http://${SERVER_HOST}:${SERVER_PORT}/toggle`, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
body: "",
|
|
262
|
+
signal: AbortSignal.timeout(2000),
|
|
263
|
+
});
|
|
264
|
+
if (res.ok) {
|
|
265
|
+
consola.success("Sidebar toggled on for all sessions");
|
|
266
|
+
} else {
|
|
267
|
+
consola.warn(`Toggle returned: ${res.status}`);
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
consola.error("Failed to toggle sidebar:", err);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function startTui(): void {
|
|
275
|
+
ensureDeps();
|
|
276
|
+
|
|
277
|
+
const tuiEntry = resolve(PLUGIN_DIR, "apps/tui/src/index.tsx");
|
|
278
|
+
|
|
279
|
+
execSync(`bun run ${tuiEntry}`, {
|
|
280
|
+
stdio: "inherit",
|
|
281
|
+
cwd: resolve(PLUGIN_DIR, "apps/tui"),
|
|
282
|
+
env: {
|
|
283
|
+
...process.env,
|
|
284
|
+
AGENTBOARD_DIR: PLUGIN_DIR,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export default defineCommand({
|
|
290
|
+
meta: { name: "agentboard", description: "AgentBoard — tmux TUI sidebar" },
|
|
291
|
+
args: {
|
|
292
|
+
debug: debugArg,
|
|
293
|
+
subcommand: {
|
|
294
|
+
type: "positional",
|
|
295
|
+
required: false,
|
|
296
|
+
description: "Subcommand: setup, uninstall, server, tui, start, restart, keys",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
async run({ args }) {
|
|
300
|
+
switch (args.subcommand) {
|
|
301
|
+
case "setup":
|
|
302
|
+
setup();
|
|
303
|
+
break;
|
|
304
|
+
case "uninstall":
|
|
305
|
+
uninstall();
|
|
306
|
+
break;
|
|
307
|
+
case "server":
|
|
308
|
+
startServer();
|
|
309
|
+
break;
|
|
310
|
+
case "tui":
|
|
311
|
+
startTui();
|
|
312
|
+
break;
|
|
313
|
+
case "start":
|
|
314
|
+
startTui();
|
|
315
|
+
break;
|
|
316
|
+
case "restart":
|
|
317
|
+
await restart();
|
|
318
|
+
break;
|
|
319
|
+
case "keys":
|
|
320
|
+
showKeys();
|
|
321
|
+
break;
|
|
322
|
+
default:
|
|
323
|
+
showKeys();
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
});
|
|
@@ -2,10 +2,10 @@ import { rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
6
|
import consola from "consola";
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { debugArg } from "../shared.js";
|
|
9
9
|
import {
|
|
10
10
|
STEP_NAMES,
|
|
11
11
|
fetchIssue,
|
|
@@ -21,95 +21,75 @@ import {
|
|
|
21
21
|
} from "../../lib/auto-claude/index.js";
|
|
22
22
|
import type { IssueContext, StepName } from "../../lib/auto-claude/index.js";
|
|
23
23
|
|
|
24
|
-
export default
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
static override args = {
|
|
30
|
-
prompt: Args.string({
|
|
31
|
-
description: "Run a single prompt (skips issue pipeline)",
|
|
24
|
+
export default defineCommand({
|
|
25
|
+
meta: { name: "auto-claude", description: "Automated issue-to-PR pipeline using Claude Code" },
|
|
26
|
+
args: {
|
|
27
|
+
prompt: {
|
|
28
|
+
type: "positional" as const,
|
|
32
29
|
required: false,
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
static override examples = [
|
|
37
|
-
{
|
|
38
|
-
description: "Run a single prompt",
|
|
39
|
-
command: '<%= config.bin %> auto-claude "Fix the login bug in auth.ts"',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
description: "Process a specific issue",
|
|
43
|
-
command: "<%= config.bin %> auto-claude --issue 42",
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
description: "Run until plan step",
|
|
47
|
-
command: "<%= config.bin %> auto-claude --issue 42 --until plan",
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
description: "Reset local state for an issue",
|
|
51
|
-
command: "<%= config.bin %> auto-claude --reset 42",
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
description: "Loop mode: poll for labeled issues",
|
|
55
|
-
command: "<%= config.bin %> auto-claude --loop",
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
description: "Loop with custom interval",
|
|
59
|
-
command: "<%= config.bin %> auto-claude --loop --interval 45",
|
|
30
|
+
description: "Run a single prompt (skips issue pipeline)",
|
|
60
31
|
},
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
...BaseCommand.baseFlags,
|
|
65
|
-
"max-turns": Flags.integer({
|
|
32
|
+
debug: debugArg,
|
|
33
|
+
"max-turns": {
|
|
34
|
+
type: "string" as const,
|
|
66
35
|
description: "Maximum conversation turns for prompt mode (default: 10)",
|
|
67
|
-
default: 10,
|
|
68
|
-
}
|
|
69
|
-
issue:
|
|
70
|
-
|
|
36
|
+
default: "10",
|
|
37
|
+
},
|
|
38
|
+
issue: {
|
|
39
|
+
type: "string" as const,
|
|
40
|
+
alias: "i",
|
|
71
41
|
description: "Process a specific issue number",
|
|
72
|
-
}
|
|
73
|
-
until:
|
|
74
|
-
|
|
42
|
+
},
|
|
43
|
+
until: {
|
|
44
|
+
type: "string" as const,
|
|
45
|
+
alias: "u",
|
|
75
46
|
description: `Stop after this step (${STEP_NAMES.join(", ")})`,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
47
|
+
},
|
|
48
|
+
reset: {
|
|
49
|
+
type: "string" as const,
|
|
79
50
|
description: "Delete local state for an issue (force restart)",
|
|
80
|
-
}
|
|
81
|
-
model:
|
|
51
|
+
},
|
|
52
|
+
model: {
|
|
53
|
+
type: "string" as const,
|
|
82
54
|
description: "Claude model to use (default: opus)",
|
|
83
55
|
default: "opus",
|
|
84
|
-
}
|
|
85
|
-
loop:
|
|
56
|
+
},
|
|
57
|
+
loop: {
|
|
58
|
+
type: "boolean" as const,
|
|
86
59
|
description: "Poll for labeled issues continuously",
|
|
87
60
|
default: false,
|
|
88
|
-
}
|
|
89
|
-
interval:
|
|
61
|
+
},
|
|
62
|
+
interval: {
|
|
63
|
+
type: "string" as const,
|
|
90
64
|
description: "Poll interval in minutes (default: 30)",
|
|
91
|
-
}
|
|
92
|
-
limit:
|
|
65
|
+
},
|
|
66
|
+
limit: {
|
|
67
|
+
type: "string" as const,
|
|
93
68
|
description: "Max issues per iteration (default: 1)",
|
|
94
|
-
default: 1,
|
|
95
|
-
}
|
|
96
|
-
label:
|
|
69
|
+
default: "1",
|
|
70
|
+
},
|
|
71
|
+
label: {
|
|
72
|
+
type: "string" as const,
|
|
97
73
|
description: "Trigger label (default: auto-claude)",
|
|
98
|
-
}
|
|
99
|
-
"main-branch":
|
|
74
|
+
},
|
|
75
|
+
"main-branch": {
|
|
76
|
+
type: "string" as const,
|
|
100
77
|
description: "Override main branch detection",
|
|
101
|
-
}
|
|
102
|
-
"scope-path":
|
|
78
|
+
},
|
|
79
|
+
"scope-path": {
|
|
80
|
+
type: "string" as const,
|
|
103
81
|
description: "Path within repo to scope work (default: .)",
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
subCommands: {
|
|
85
|
+
list: () => import("./list.js").then((m) => m.default),
|
|
86
|
+
status: () => import("./status.js").then((m) => m.default),
|
|
87
|
+
retry: () => import("./retry.js").then((m) => m.default),
|
|
88
|
+
},
|
|
89
|
+
async run({ args }) {
|
|
110
90
|
// Prompt mode: run a single prompt with structured output, skip issue pipeline
|
|
111
91
|
if (args.prompt) {
|
|
112
|
-
await initConfig({ model:
|
|
92
|
+
await initConfig({ model: args.model });
|
|
113
93
|
|
|
114
94
|
const promptDir = join(tmpdir(), "tt-auto-claude");
|
|
115
95
|
mkdirSync(promptDir, { recursive: true });
|
|
@@ -118,41 +98,44 @@ export default class AutoClaude extends BaseCommand {
|
|
|
118
98
|
|
|
119
99
|
const result = await runClaude({
|
|
120
100
|
promptFile,
|
|
121
|
-
maxTurns:
|
|
101
|
+
maxTurns: Number(args["max-turns"]),
|
|
122
102
|
});
|
|
123
103
|
|
|
124
104
|
if (result.is_error) {
|
|
125
|
-
|
|
105
|
+
consola.error("Claude reported an error");
|
|
106
|
+
process.exit(1);
|
|
126
107
|
}
|
|
127
108
|
return;
|
|
128
109
|
}
|
|
129
110
|
|
|
130
111
|
// Issue pipeline mode
|
|
131
112
|
const cfg = await initConfig({
|
|
132
|
-
triggerLabel:
|
|
133
|
-
mainBranch:
|
|
134
|
-
scopePath:
|
|
135
|
-
model:
|
|
113
|
+
triggerLabel: args.label,
|
|
114
|
+
mainBranch: args["main-branch"],
|
|
115
|
+
scopePath: args["scope-path"],
|
|
116
|
+
model: args.model,
|
|
136
117
|
});
|
|
137
118
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
119
|
+
const resetIssue = args.reset ? Number(args.reset) : undefined;
|
|
120
|
+
if (resetIssue) {
|
|
121
|
+
const issueDir = join(process.cwd(), `.auto-claude/issue-${resetIssue}`);
|
|
122
|
+
log(`Resetting state for issue-${resetIssue}...`);
|
|
141
123
|
rmSync(issueDir, { recursive: true, force: true });
|
|
142
124
|
log(`Cleaned ${issueDir}`);
|
|
143
125
|
return;
|
|
144
126
|
}
|
|
145
127
|
|
|
146
|
-
const untilStep =
|
|
147
|
-
const loopMode =
|
|
148
|
-
const intervalMs = (
|
|
149
|
-
const limit =
|
|
128
|
+
const untilStep = args.until as StepName | undefined;
|
|
129
|
+
const loopMode = args.loop as boolean;
|
|
130
|
+
const intervalMs = (args.interval ? Number(args.interval) : cfg.loopIntervalMinutes) * 60_000;
|
|
131
|
+
const limit = Number(args.limit);
|
|
150
132
|
|
|
151
133
|
if (loopMode) {
|
|
152
134
|
registerShutdownHandlers();
|
|
153
135
|
log(`Loop mode — interval: ${intervalMs / 60_000}min, limit: ${limit}`);
|
|
154
136
|
}
|
|
155
137
|
|
|
138
|
+
const issueNumber = args.issue ? Number(args.issue) : undefined;
|
|
156
139
|
let iteration = 0;
|
|
157
140
|
|
|
158
141
|
do {
|
|
@@ -177,8 +160,8 @@ export default class AutoClaude extends BaseCommand {
|
|
|
177
160
|
|
|
178
161
|
log("Fetching labeled issues…");
|
|
179
162
|
let contexts: IssueContext[];
|
|
180
|
-
if (
|
|
181
|
-
const ctx = await fetchIssue(
|
|
163
|
+
if (issueNumber) {
|
|
164
|
+
const ctx = await fetchIssue(issueNumber);
|
|
182
165
|
contexts = ctx ? [ctx] : [];
|
|
183
166
|
} else {
|
|
184
167
|
contexts = await fetchIssues(limit);
|
|
@@ -212,8 +195,8 @@ export default class AutoClaude extends BaseCommand {
|
|
|
212
195
|
} while (loopMode);
|
|
213
196
|
|
|
214
197
|
log("Done.");
|
|
215
|
-
}
|
|
216
|
-
}
|
|
198
|
+
},
|
|
199
|
+
});
|
|
217
200
|
|
|
218
201
|
async function syncWithRemote(): Promise<void> {
|
|
219
202
|
const cfg = getConfig();
|