@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 CHANGED
@@ -1,5 +1,6 @@
1
- #!/usr/bin/env tsx
1
+ #!/usr/bin/env bun
2
2
 
3
- import { execute } from "@oclif/core";
3
+ import { runMain } from "citty";
4
+ import { main } from "../src/cli.js";
4
5
 
5
- await execute({ dir: import.meta.url });
6
+ runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towles/tool",
3
- "version": "0.0.95",
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": "pnpm tsx scripts/sync-versions.ts",
33
- "prepublishOnly": "pnpm run version:sync",
34
- "dev": "tsx bin/run.ts",
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
- "@oclif/core": "^4.3.2",
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": "pnpm format && pnpm lint:fix && pnpm typecheck && claude plugin validate ."
73
+ "pre-commit": "bun run format && bun run lint:fix && bun run typecheck && claude plugin validate ."
82
74
  },
83
- "oclif": {
84
- "bin": "tt",
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 { Args, Flags } from "@oclif/core";
5
+ import { defineCommand } from "citty";
6
6
  import consola from "consola";
7
7
 
8
- import { BaseCommand } from "../base.js";
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 class AutoClaude extends BaseCommand {
25
- static override aliases = ["ac"];
26
-
27
- static override description = "Automated issue-to-PR pipeline using Claude Code";
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
- static override flags = {
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: Flags.integer({
70
- char: "i",
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: Flags.string({
74
- char: "u",
42
+ },
43
+ until: {
44
+ type: "string" as const,
45
+ alias: "u",
75
46
  description: `Stop after this step (${STEP_NAMES.join(", ")})`,
76
- options: [...STEP_NAMES],
77
- }),
78
- reset: Flags.integer({
47
+ },
48
+ reset: {
49
+ type: "string" as const,
79
50
  description: "Delete local state for an issue (force restart)",
80
- }),
81
- model: Flags.string({
51
+ },
52
+ model: {
53
+ type: "string" as const,
82
54
  description: "Claude model to use (default: opus)",
83
55
  default: "opus",
84
- }),
85
- loop: Flags.boolean({
56
+ },
57
+ loop: {
58
+ type: "boolean" as const,
86
59
  description: "Poll for labeled issues continuously",
87
60
  default: false,
88
- }),
89
- interval: Flags.integer({
61
+ },
62
+ interval: {
63
+ type: "string" as const,
90
64
  description: "Poll interval in minutes (default: 30)",
91
- }),
92
- limit: Flags.integer({
65
+ },
66
+ limit: {
67
+ type: "string" as const,
93
68
  description: "Max issues per iteration (default: 1)",
94
- default: 1,
95
- }),
96
- label: Flags.string({
69
+ default: "1",
70
+ },
71
+ label: {
72
+ type: "string" as const,
97
73
  description: "Trigger label (default: auto-claude)",
98
- }),
99
- "main-branch": Flags.string({
74
+ },
75
+ "main-branch": {
76
+ type: "string" as const,
100
77
  description: "Override main branch detection",
101
- }),
102
- "scope-path": Flags.string({
78
+ },
79
+ "scope-path": {
80
+ type: "string" as const,
103
81
  description: "Path within repo to scope work (default: .)",
104
- }),
105
- };
106
-
107
- async run(): Promise<void> {
108
- const { args, flags } = await this.parse(AutoClaude);
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: flags.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: flags["max-turns"],
101
+ maxTurns: Number(args["max-turns"]),
122
102
  });
123
103
 
124
104
  if (result.is_error) {
125
- this.error("Claude reported an error", { exit: 1 });
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: flags.label,
133
- mainBranch: flags["main-branch"],
134
- scopePath: flags["scope-path"],
135
- model: flags.model,
113
+ triggerLabel: args.label,
114
+ mainBranch: args["main-branch"],
115
+ scopePath: args["scope-path"],
116
+ model: args.model,
136
117
  });
137
118
 
138
- if (flags.reset) {
139
- const issueDir = join(process.cwd(), `.auto-claude/issue-${flags.reset}`);
140
- log(`Resetting state for issue-${flags.reset}...`);
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 = flags.until as StepName | undefined;
147
- const loopMode = flags.loop;
148
- const intervalMs = (flags.interval ?? cfg.loopIntervalMinutes) * 60_000;
149
- const limit = flags.limit ?? 1;
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 (flags.issue) {
181
- const ctx = await fetchIssue(flags.issue);
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();