claude-task-worker 0.10.0 → 0.12.0

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 (3) hide show
  1. package/README.md +61 -2
  2. package/dist/index.js +694 -64
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -60,6 +60,10 @@ GitHub Issues/PRを定期ポーリングし、Claude Codeに処理を委譲す
60
60
 
61
61
  `epic-issue` ワーカーは、`cc-epic-issue` ラベル付きの親Issueに紐づくサブIssueがすべて Close されたタイミングで `/claude-task-worker:create-epic-pr` を起動し、エピックブランチからまとめてPRを作成する。
62
62
 
63
+ ### `--project` 指定時(ディスパッチャーモード)
64
+
65
+ `--project` オプションを指定した場合、CLIは自身でワーカーを起動する代わりに [herdr](https://herdr.dev) 経由のディスパッチャーとして動作する。指定したプロジェクトごとにherdrのタブ(ペイン)を作成し、そのプロジェクトのディレクトリで(`--project` を除いた)同じコマンドを非同期実行させ、稼働状況をステータステーブルで監視する。詳細は「[`--project <name>` オプション](#--project-name-オプション)」を参照。
66
+
63
67
  ## 必要なライブラリ
64
68
 
65
69
  ### 実行時に必要なもの
@@ -68,12 +72,13 @@ CLI 本体に npm の実行時依存パッケージはない(ビルド時に e
68
72
 
69
73
  | 名前 | バージョン | 用途 |
70
74
  |---|---|---|
71
- | [Node.js](https://nodejs.org/) | >= 20 | CLI の実行ランタイム |
75
+ | [Node.js](https://nodejs.org/) | >= 22.6.0 | CLI の実行ランタイム(`--experimental-strip-types` を使用するテスト実行に必要) |
72
76
  | [GitHub CLI (`gh`)](https://cli.github.com/) | - | Issue/PR の取得・ラベル操作など全GitHub操作(認証済みであること) |
73
77
  | [Claude Code (`claude`)](https://docs.anthropic.com/en/docs/claude-code) | - | タスク実行エンジン。各ワーカーが Claude CLI プロセスとして起動する |
74
78
  | [Git (`git`)](https://git-scm.com/) | - | worktree の作成・ブランチ操作 |
75
79
  | [jq](https://jqlang.org/) | - | プラグインスキル内でのJSON加工(`check-dependabot` / `edit-pencil-design` スキルなどで使用) |
76
80
  | [Pencil CLI (`pencil`)](https://docs.pencil.dev/for-developers/pencil-cli) | - | `.pen` デザインファイルの編集・参照・コンフリクト解消(`edit-pencil-design` / `inspect-pencil-node` / `resolve-pencil-conflict` スキルで使用) |
81
+ | [herdr](https://herdr.dev) | - | `--project` 使用時にのみ必要(通常ワーカー実行には不要)。複数プロジェクトへのディスパッチ・セッション監視・一括停止に使用 |
77
82
  | `claude-task-worker` プラグイン | - | 各ワーカーが呼び出すスキル群(本リポジトリの `plugin/`) |
78
83
 
79
84
  Slack通知(任意)を使う場合は Slack Incoming Webhook URL が必要。Claude API使用状況の取得には、macOSでは `security` コマンド(Keychain)を使用し、それ以外の環境では `~/.claude/.credentials.json` にフォールバックする。
@@ -105,10 +110,11 @@ Slack通知(任意)を使う場合は Slack Incoming Webhook URL が必要
105
110
 
106
111
  ### 前提条件
107
112
 
108
- - [Node.js](https://nodejs.org/) v20 以上がインストール済みであること
113
+ - [Node.js](https://nodejs.org/) v22.6 以上がインストール済みであること
109
114
  - [GitHub CLI (`gh`)](https://cli.github.com/) がインストール・認証済みであること
110
115
  - [Claude Code (`claude`)](https://docs.anthropic.com/en/docs/claude-code) がインストール済みであること
111
116
  - `claude-task-worker` プラグインがインストール済みであること(下記インストール手順を参照)
117
+ - `--project` オプションを使う場合のみ、[herdr](https://herdr.dev) がインストール済みであること(通常のワーカー実行には不要)
112
118
 
113
119
  詳細は上記「[必要なライブラリ](#必要なライブラリ)」を参照。
114
120
 
@@ -155,6 +161,16 @@ claude plugin install claude-task-worker@claude-task-worker
155
161
 
156
162
  インストール後、Claude Code のセッションを再起動するとプラグインが有効化される。
157
163
 
164
+ herdr(`--project` オプションを使う場合のみ必要。通常ワーカー実行には不要):
165
+
166
+ ```bash
167
+ curl -fsSL https://herdr.dev/install.sh | sh
168
+ # または
169
+ brew install herdr
170
+ ```
171
+
172
+ 詳細は [herdr インストールドキュメント](https://herdr.dev/docs/install/) を参照。
173
+
158
174
  #### 更新
159
175
 
160
176
  CLI とプラグイン(マーケットプレイス)をまとめて更新する。
@@ -232,6 +248,49 @@ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
232
248
 
233
249
  > ℹ️ `--label` で指定したラベルはユーザーのスコープ指定なので、タスク完了時にワーカー側で除去されることはない(トリガーラベルだけが除去される)。
234
250
 
251
+ ### `--project <name>` オプション
252
+
253
+ 指定したプロジェクト(またはプロジェクトグループ、`all`)に対して、[herdr](https://herdr.dev) 経由でコマンドをディスパッチする。指定すると、CLIはワーカーをその場で実行する代わりにディスパッチャーとして動作し、対象プロジェクトごとに独立したherdrタブでコマンドを実行する。
254
+
255
+ プロジェクト名・グループ名・`all` のいずれかを指定できる。
256
+
257
+ - プロジェクト名: `projects.json` の `projects` に登録された個別プロジェクト名
258
+ - グループ名: `projects.json` の `projectGroups` に登録された、複数プロジェクト名をまとめたグループ名
259
+ - `all`: `projects.json` の `projects` に登録された全プロジェクトが対象になる予約語(`projects` / `projectGroups` のキーとして使用不可)
260
+
261
+ `projects.json` は `$XDG_CONFIG_HOME/claude-task-worker/projects.json`(`XDG_CONFIG_HOME` 未設定の場合は `~/.config/claude-task-worker/projects.json`)に配置する。`projects` にプロジェクト名から絶対パスへのマッピングを、`projectGroups` にグループ名からプロジェクト名配列へのマッピングを記述する(`projectGroups` は省略可)。
262
+
263
+ ```json
264
+ {
265
+ "projects": {
266
+ "app-a": "/Users/me/repos/app-a",
267
+ "app-b": "/Users/me/repos/app-b",
268
+ "app-c": "/Users/me/repos/app-c"
269
+ },
270
+ "projectGroups": {
271
+ "frontend": ["app-a", "app-b"]
272
+ }
273
+ }
274
+ ```
275
+
276
+ `--project` は繰り返し指定可能で、複数指定した場合は解決後のプロジェクト集合の和集合が対象になる(重複は一意化される)。`--epic` / `--label` と併用でき、ディスパッチ先の各プロジェクトで実行されるコマンドにそのまま引き継がれる。
277
+
278
+ 以下のコマンドは `--project` と併用できない: `init` / `install` / `update` / `usage` / `version`
279
+
280
+ ディスパッチャーは以下の3つの機能を持つ。
281
+
282
+ - **一斉起動**: 対象プロジェクトごとにherdrタブを作成し、そのプロジェクトのディレクトリで(`--project` を除いた)同じコマンドを非同期実行する
283
+ - **稼働一覧**: 各セッションのプロジェクト名・タブID・ペインID・ステータス・稼働時間をステータステーブルとして定期的に画面へ描画し、対象プロセスが終了したセッションは自動的に一覧から除去する
284
+ - **一括停止**: SIGTERM/SIGINTを受けると、稼働中の全セッションへ ctrl-c を送信して各プロジェクトのコマンドを終了させ、終了を待ってからherdrタブを閉じる
285
+
286
+ ```bash
287
+ claude-task-worker all --project all
288
+ claude-task-worker all --project app-a
289
+ claude-task-worker all --project frontend
290
+ claude-task-worker all --project app-a --project app-c # app-a と app-c の和集合
291
+ claude-task-worker exec-issue --project app-a --epic 100 --label priority-high
292
+ ```
293
+
235
294
  ### exec-issue
236
295
 
237
296
  `cc-exec-issue` ラベルが付いた自分にアサインされたIssueを定期取得し、Claude Codeで処理を実行する。(デフォルト1分間隔)
package/dist/index.js CHANGED
@@ -1,4 +1,451 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/table.ts
13
+ var table_exports = {};
14
+ __export(table_exports, {
15
+ getDisplayWidth: () => getDisplayWidth,
16
+ padToWidth: () => padToWidth,
17
+ truncateToWidth: () => truncateToWidth
18
+ });
19
+ function getDisplayWidth(str) {
20
+ let width = 0;
21
+ for (const char of str) {
22
+ const code = char.codePointAt(0);
23
+ if (code >= 4352 && code <= 4447 || code >= 11904 && code <= 12350 || code >= 12352 && code <= 13247 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 42191 || code >= 44032 && code <= 55215 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 131072 && code <= 196605 || code >= 196608 && code <= 262141) {
24
+ width += 2;
25
+ } else {
26
+ width += 1;
27
+ }
28
+ }
29
+ return width;
30
+ }
31
+ function truncateToWidth(str, maxWidth) {
32
+ let width = 0;
33
+ let i = 0;
34
+ const chars = [...str];
35
+ while (i < chars.length) {
36
+ const charWidth = getDisplayWidth(chars[i]);
37
+ if (width + charWidth > maxWidth - 3) {
38
+ return chars.slice(0, i).join("") + "...";
39
+ }
40
+ width += charWidth;
41
+ i++;
42
+ }
43
+ return str;
44
+ }
45
+ function padToWidth(str, targetWidth) {
46
+ const currentWidth = getDisplayWidth(str);
47
+ const padding = targetWidth - currentWidth;
48
+ return padding > 0 ? str + " ".repeat(padding) : str;
49
+ }
50
+ var init_table = __esm({
51
+ "src/table.ts"() {
52
+ "use strict";
53
+ }
54
+ });
55
+
56
+ // src/herdr.ts
57
+ var herdr_exports = {};
58
+ __export(herdr_exports, {
59
+ HERDR_TIMEOUT_MS: () => HERDR_TIMEOUT_MS,
60
+ HerdrError: () => HerdrError,
61
+ HerdrUnavailableError: () => HerdrUnavailableError,
62
+ checkHerdrAvailable: () => checkHerdrAvailable,
63
+ paneProcessInfo: () => paneProcessInfo,
64
+ paneSendKeys: () => paneSendKeys,
65
+ paneSendText: () => paneSendText,
66
+ tabClose: () => tabClose,
67
+ tabCreate: () => tabCreate,
68
+ tabList: () => tabList
69
+ });
70
+ import { createRequire } from "node:module";
71
+ function runHerdr(args) {
72
+ return new Promise((resolve2) => {
73
+ childProcess.execFile(
74
+ "herdr",
75
+ args,
76
+ { timeout: HERDR_TIMEOUT_MS, killSignal: "SIGKILL" },
77
+ (error, stdout, stderr) => {
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(stdout);
81
+ } catch {
82
+ parsed = void 0;
83
+ }
84
+ resolve2({ execError: error, parsed, stdout, stderr });
85
+ }
86
+ );
87
+ });
88
+ }
89
+ async function execHerdr(args, options) {
90
+ const { execError, parsed, stdout, stderr } = await runHerdr(args);
91
+ const stderrSuffix = stderr.trim() ? `: ${stderr.trim()}` : "";
92
+ if (parsed?.error) {
93
+ throw new HerdrError(
94
+ `herdr ${args.join(" ")} failed: [${parsed.error.code}] ${parsed.error.message}`,
95
+ parsed.error.code
96
+ );
97
+ }
98
+ if (execError) {
99
+ throw new Error(`herdr ${args.join(" ")} failed: ${execError.message}${stderrSuffix}`);
100
+ }
101
+ if (!parsed) {
102
+ if (options?.allowEmptyResult && stdout.trim() === "" && stderr.trim() === "") {
103
+ return void 0;
104
+ }
105
+ throw new Error(`herdr ${args.join(" ")} failed: invalid JSON output${stderrSuffix}`);
106
+ }
107
+ return parsed.result;
108
+ }
109
+ async function tabCreate({ label, cwd }) {
110
+ const result = await execHerdr(["tab", "create", "--label", label, "--cwd", cwd, "--no-focus"]);
111
+ const paneId = result?.root_pane?.pane_id;
112
+ const tabId = result?.tab?.tab_id;
113
+ if (!paneId || !tabId) {
114
+ throw new Error("Failed to create tab: invalid response structure from herdr");
115
+ }
116
+ return { paneId, tabId };
117
+ }
118
+ async function tabClose(tabId) {
119
+ await execHerdr(["tab", "close", tabId]);
120
+ }
121
+ async function tabList() {
122
+ const result = await execHerdr(["tab", "list"]);
123
+ if (!result || !Array.isArray(result.tabs)) {
124
+ return [];
125
+ }
126
+ return result.tabs.map((tab) => ({
127
+ tabId: tab.tab_id,
128
+ label: tab.label,
129
+ workspaceId: tab.workspace_id
130
+ }));
131
+ }
132
+ async function paneSendText(paneId, text) {
133
+ await execHerdr(["pane", "send-text", paneId, text], { allowEmptyResult: true });
134
+ }
135
+ async function paneSendKeys(paneId, ...keys) {
136
+ await execHerdr(["pane", "send-keys", paneId, ...keys], { allowEmptyResult: true });
137
+ }
138
+ async function paneProcessInfo(paneId) {
139
+ const result = await execHerdr(["pane", "process-info", "--pane", paneId]);
140
+ const foregroundProcesses = result?.process_info?.foreground_processes ?? [];
141
+ return {
142
+ foregroundProcesses: foregroundProcesses.map((process2) => ({
143
+ name: process2.name,
144
+ argv: process2.argv,
145
+ cmdline: process2.cmdline,
146
+ pid: process2.pid
147
+ }))
148
+ };
149
+ }
150
+ async function checkHerdrAvailable() {
151
+ const { execError, parsed } = await runHerdr(["tab", "list"]);
152
+ if (execError?.code === "ENOENT") {
153
+ throw new HerdrUnavailableError(
154
+ "herdr\u304C\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002herdr CLI\u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
155
+ "not-installed"
156
+ );
157
+ }
158
+ if (!parsed) {
159
+ throw new HerdrUnavailableError(
160
+ "herdr\u30B5\u30FC\u30D0\u30FC\u306B\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002`herdr status` \u7B49\u3067\u8D77\u52D5\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
161
+ "server-unreachable"
162
+ );
163
+ }
164
+ }
165
+ var childProcess, HerdrUnavailableError, HerdrError, HERDR_TIMEOUT_MS;
166
+ var init_herdr = __esm({
167
+ "src/herdr.ts"() {
168
+ "use strict";
169
+ childProcess = createRequire(import.meta.url)("node:child_process");
170
+ HerdrUnavailableError = class extends Error {
171
+ reason;
172
+ constructor(message, reason) {
173
+ super(message);
174
+ this.name = "HerdrUnavailableError";
175
+ this.reason = reason;
176
+ }
177
+ };
178
+ HerdrError = class extends Error {
179
+ code;
180
+ constructor(message, code) {
181
+ super(message);
182
+ this.name = "HerdrError";
183
+ this.code = code;
184
+ }
185
+ };
186
+ HERDR_TIMEOUT_MS = 15 * 1e3;
187
+ }
188
+ });
189
+
190
+ // src/dispatcher.ts
191
+ var dispatcher_exports = {};
192
+ __export(dispatcher_exports, {
193
+ POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
194
+ SHUTDOWN_RETRY_TIMEOUT_MS: () => SHUTDOWN_RETRY_TIMEOUT_MS,
195
+ SHUTDOWN_TIMEOUT_MS: () => SHUTDOWN_TIMEOUT_MS,
196
+ formatUptime: () => formatUptime,
197
+ monitorSessions: () => monitorSessions,
198
+ pollOnce: () => pollOnce,
199
+ removeSession: () => removeSession,
200
+ renderSessionTable: () => renderSessionTable,
201
+ runDispatcher: () => runDispatcher,
202
+ shutdownDispatcher: () => shutdownDispatcher
203
+ });
204
+ async function loadHerdr() {
205
+ return await Promise.resolve().then(() => (init_herdr(), herdr_exports));
206
+ }
207
+ async function loadTable() {
208
+ return await Promise.resolve().then(() => (init_table(), table_exports));
209
+ }
210
+ async function runDispatcher(projects, forwardedCommand) {
211
+ const { checkHerdrAvailable: checkHerdrAvailable2, tabCreate: tabCreate2, tabClose: tabClose2, tabList: tabList2, paneSendText: paneSendText2, paneSendKeys: paneSendKeys2 } = await loadHerdr();
212
+ try {
213
+ await checkHerdrAvailable2();
214
+ } catch (error) {
215
+ const message = error instanceof Error ? error.message : String(error);
216
+ console.error(`[dispatcher] ${message}`);
217
+ throw error;
218
+ }
219
+ const existingTabs = await tabList2();
220
+ const existingLabels = new Set(existingTabs.map((tab) => tab.label));
221
+ const sessions = /* @__PURE__ */ new Map();
222
+ for (const project of projects) {
223
+ if (existingLabels.has(project.name)) {
224
+ console.warn(`[dispatcher] project "${project.name}" already has a running tab, skipping`);
225
+ continue;
226
+ }
227
+ let createdTabId;
228
+ try {
229
+ const { paneId, tabId } = await tabCreate2({ label: project.name, cwd: project.path });
230
+ createdTabId = tabId;
231
+ sessions.set(project.name, {
232
+ name: project.name,
233
+ tabId,
234
+ paneId,
235
+ startedAt: /* @__PURE__ */ new Date(),
236
+ status: "running"
237
+ });
238
+ await paneSendText2(paneId, forwardedCommand);
239
+ await paneSendKeys2(paneId, "enter");
240
+ } catch (error) {
241
+ console.error(`[dispatcher] failed to dispatch project "${project.name}": ${error}`);
242
+ sessions.delete(project.name);
243
+ if (createdTabId !== void 0) {
244
+ try {
245
+ await tabClose2(createdTabId);
246
+ } catch (closeError) {
247
+ console.error(`[dispatcher] failed to close dangling tab for project "${project.name}": ${closeError}`);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ return sessions;
253
+ }
254
+ async function removeSession(sessions, name, { closeTab }) {
255
+ const session = sessions.get(name);
256
+ if (!session) return;
257
+ sessions.delete(name);
258
+ if (closeTab) {
259
+ const { tabClose: tabClose2 } = await loadHerdr();
260
+ await tabClose2(session.tabId);
261
+ }
262
+ }
263
+ async function pollOnce(sessions, herdr) {
264
+ for (const [name, session] of [...sessions.entries()]) {
265
+ try {
266
+ const { foregroundProcesses } = await herdr.paneProcessInfo(session.paneId);
267
+ const isAlive = foregroundProcesses.some((process2) => process2.cmdline?.includes("claude-task-worker"));
268
+ if (!isAlive) {
269
+ await removeSession(sessions, name, { closeTab: true });
270
+ }
271
+ } catch (error) {
272
+ if (error instanceof herdr.HerdrError && error.code === "pane_not_found") {
273
+ await removeSession(sessions, name, { closeTab: false });
274
+ continue;
275
+ }
276
+ console.error(`[dispatcher] failed to poll session "${name}": ${error}`);
277
+ }
278
+ }
279
+ }
280
+ function formatUptime(start, now) {
281
+ const totalSeconds = Math.max(0, Math.floor((now.getTime() - start.getTime()) / 1e3));
282
+ const hours = Math.floor(totalSeconds / 3600);
283
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
284
+ const seconds = totalSeconds % 60;
285
+ return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
286
+ }
287
+ function renderSessionTable(sessions) {
288
+ const entries = [...sessions.values()];
289
+ if (entries.length === 0) return;
290
+ const maxProjectWidth = 20;
291
+ const rows = entries.map((session) => ({
292
+ project: getDisplayWidth2(session.name) > maxProjectWidth ? truncateToWidth2(session.name, maxProjectWidth) : session.name,
293
+ tab: session.tabId,
294
+ pane: session.paneId,
295
+ status: session.status,
296
+ uptime: formatUptime(session.startedAt, /* @__PURE__ */ new Date())
297
+ }));
298
+ const colWidths = {
299
+ project: Math.max(7, ...rows.map((r) => getDisplayWidth2(r.project))),
300
+ tab: Math.max(3, ...rows.map((r) => r.tab.length)),
301
+ pane: Math.max(4, ...rows.map((r) => r.pane.length)),
302
+ status: Math.max(6, ...rows.map((r) => r.status.length)),
303
+ uptime: Math.max(6, ...rows.map((r) => r.uptime.length))
304
+ };
305
+ const cols = [colWidths.project, colWidths.tab, colWidths.pane, colWidths.status, colWidths.uptime];
306
+ const line = (l, m, r, f) => `${l}${cols.map((w) => f.repeat(w + 2)).join(m)}${r}`;
307
+ const row = (project, tab, pane, status, uptime) => `\u2502 ${padToWidth2(project, colWidths.project)} \u2502 ${tab.padEnd(colWidths.tab)} \u2502 ${pane.padEnd(colWidths.pane)} \u2502 ${status.padEnd(colWidths.status)} \u2502 ${uptime.padEnd(colWidths.uptime)} \u2502`;
308
+ const lines = [];
309
+ lines.push(line("\u250C", "\u252C", "\u2510", "\u2500"));
310
+ lines.push(row("Project", "Tab", "Pane", "Status", "Uptime"));
311
+ lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
312
+ for (const r of rows) {
313
+ lines.push(row(r.project, r.tab, r.pane, r.status, r.uptime));
314
+ }
315
+ lines.push(line("\u2514", "\u2534", "\u2518", "\u2500"));
316
+ console.clear();
317
+ console.log(lines.join("\n"));
318
+ }
319
+ function monitorSessions(sessions, herdr, options) {
320
+ const pollIntervalMs = options?.pollIntervalMs ?? POLL_INTERVAL_MS;
321
+ const renderIntervalMs = options?.renderIntervalMs ?? 1e3;
322
+ let resolveDone;
323
+ const done = new Promise((resolve2) => {
324
+ resolveDone = resolve2;
325
+ });
326
+ let settled = false;
327
+ const finish = () => {
328
+ if (settled) return;
329
+ settled = true;
330
+ clearInterval(pollInterval);
331
+ clearInterval(renderInterval2);
332
+ resolveDone();
333
+ };
334
+ const pollInterval = setInterval(() => {
335
+ void pollOnce(sessions, herdr).then(() => {
336
+ if (sessions.size === 0) {
337
+ finish();
338
+ }
339
+ });
340
+ }, pollIntervalMs);
341
+ pollInterval.unref();
342
+ const renderInterval2 = setInterval(() => {
343
+ renderSessionTable(sessions);
344
+ }, renderIntervalMs);
345
+ renderInterval2.unref();
346
+ return {
347
+ stop: finish,
348
+ done
349
+ };
350
+ }
351
+ async function sendCtrlCToAllSessions(sessions, herdr) {
352
+ await Promise.all(
353
+ [...sessions.values()].map(async (session) => {
354
+ try {
355
+ await herdr.paneSendKeys(session.paneId, "ctrl-c");
356
+ } catch (error) {
357
+ console.error(`[dispatcher] failed to send ctrl-c to session "${session.name}": ${error}`);
358
+ }
359
+ })
360
+ );
361
+ }
362
+ async function waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, timeoutMs) {
363
+ const deadline = Date.now() + timeoutMs;
364
+ while (sessions.size > 0) {
365
+ await pollOnce(sessions, herdr);
366
+ if (sessions.size === 0) return true;
367
+ if (Date.now() >= deadline) return false;
368
+ await new Promise((resolve2) => setTimeout(resolve2, pollIntervalMs));
369
+ }
370
+ return true;
371
+ }
372
+ async function closeRemainingTabs(sessions, herdr, timeoutMs) {
373
+ const results = await Promise.all(
374
+ [...sessions.values()].map(async (session) => {
375
+ try {
376
+ await Promise.race([
377
+ herdr.tabClose(session.tabId),
378
+ new Promise((_, reject) => {
379
+ setTimeout(() => reject(new Error("tabClose timed out")), timeoutMs).unref();
380
+ })
381
+ ]);
382
+ return true;
383
+ } catch (error) {
384
+ console.error(
385
+ `[dispatcher] failed to close tab "${session.tabId}" for session "${session.name}" during shutdown: ${error}`
386
+ );
387
+ return false;
388
+ }
389
+ })
390
+ );
391
+ return results.every((closed) => closed);
392
+ }
393
+ async function forceKillAllSessions(sessions, herdr, tabCloseTimeoutMs) {
394
+ console.log(`[dispatcher] force-kill requested, sending ctrl-c and closing ${sessions.size} session(s) immediately`);
395
+ await sendCtrlCToAllSessions(sessions, herdr);
396
+ await closeRemainingTabs(sessions, herdr, tabCloseTimeoutMs);
397
+ process.exit(1);
398
+ }
399
+ async function shutdownDispatcher(sessions, monitorHandle, options) {
400
+ if (shutdownPromise) {
401
+ if (options?.forceKill) {
402
+ const herdr = options.herdr ?? await loadHerdr();
403
+ const tabCloseTimeoutMs = options.tabCloseTimeoutMs ?? 5e3;
404
+ await forceKillAllSessions(sessions, herdr, tabCloseTimeoutMs);
405
+ return;
406
+ }
407
+ console.log("[dispatcher] shutdown already in progress, ignoring duplicate request");
408
+ return shutdownPromise;
409
+ }
410
+ shutdownPromise = (async () => {
411
+ const herdr = options?.herdr ?? await loadHerdr();
412
+ const pollIntervalMs = options?.pollIntervalMs ?? POLL_INTERVAL_MS;
413
+ const shutdownTimeoutMs = options?.shutdownTimeoutMs ?? SHUTDOWN_TIMEOUT_MS;
414
+ const retryTimeoutMs = options?.retryTimeoutMs ?? SHUTDOWN_RETRY_TIMEOUT_MS;
415
+ const tabCloseTimeoutMs = options?.tabCloseTimeoutMs ?? 5e3;
416
+ if (monitorHandle) {
417
+ monitorHandle.stop();
418
+ await monitorHandle.done;
419
+ }
420
+ console.log(`[dispatcher] shutting down, sending ctrl-c to ${sessions.size} session(s)`);
421
+ await sendCtrlCToAllSessions(sessions, herdr);
422
+ let finishedInTime = await waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, shutdownTimeoutMs);
423
+ if (!finishedInTime) {
424
+ console.log(
425
+ `[dispatcher] ${sessions.size} session(s) still alive after ${shutdownTimeoutMs}ms, resending ctrl-c once`
426
+ );
427
+ await sendCtrlCToAllSessions(sessions, herdr);
428
+ finishedInTime = await waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, retryTimeoutMs);
429
+ }
430
+ const allTabsClosed = await closeRemainingTabs(sessions, herdr, tabCloseTimeoutMs);
431
+ process.exit(finishedInTime && allTabsClosed ? 0 : 1);
432
+ })();
433
+ try {
434
+ await shutdownPromise;
435
+ } finally {
436
+ shutdownPromise = void 0;
437
+ }
438
+ }
439
+ var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, SHUTDOWN_RETRY_TIMEOUT_MS, shutdownPromise;
440
+ var init_dispatcher = __esm({
441
+ async "src/dispatcher.ts"() {
442
+ "use strict";
443
+ ({ getDisplayWidth: getDisplayWidth2, truncateToWidth: truncateToWidth2, padToWidth: padToWidth2 } = await loadTable());
444
+ POLL_INTERVAL_MS = 7 * 1e3;
445
+ SHUTDOWN_TIMEOUT_MS = 10 * 60 * 1e3;
446
+ SHUTDOWN_RETRY_TIMEOUT_MS = 90 * 1e3;
447
+ }
448
+ });
2
449
 
3
450
  // src/gh.ts
4
451
  import { execFile } from "node:child_process";
@@ -460,39 +907,9 @@ async function assertRemoteTrackingExists(epicBranch) {
460
907
 
461
908
  // src/process-manager.ts
462
909
  import { spawn } from "node:child_process";
910
+ init_table();
463
911
  var childProcesses = /* @__PURE__ */ new Map();
464
912
  var TASK_TIMEOUT_MS = 90 * 60 * 1e3;
465
- function getDisplayWidth(str) {
466
- let width = 0;
467
- for (const char of str) {
468
- const code = char.codePointAt(0);
469
- if (code >= 4352 && code <= 4447 || code >= 11904 && code <= 12350 || code >= 12352 && code <= 13247 || code >= 13312 && code <= 19903 || code >= 19968 && code <= 42191 || code >= 44032 && code <= 55215 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65281 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 131072 && code <= 196605 || code >= 196608 && code <= 262141) {
470
- width += 2;
471
- } else {
472
- width += 1;
473
- }
474
- }
475
- return width;
476
- }
477
- function truncateToWidth(str, maxWidth) {
478
- let width = 0;
479
- let i = 0;
480
- const chars = [...str];
481
- while (i < chars.length) {
482
- const charWidth = getDisplayWidth(chars[i]);
483
- if (width + charWidth > maxWidth - 3) {
484
- return chars.slice(0, i).join("") + "...";
485
- }
486
- width += charWidth;
487
- i++;
488
- }
489
- return str;
490
- }
491
- function padToWidth(str, targetWidth) {
492
- const currentWidth = getDisplayWidth(str);
493
- const padding = targetWidth - currentWidth;
494
- return padding > 0 ? str + " ".repeat(padding) : str;
495
- }
496
913
  var tasks = /* @__PURE__ */ new Map();
497
914
  var shuttingDown = false;
498
915
  function setShuttingDown() {
@@ -1842,6 +2259,184 @@ function version() {
1842
2259
  }
1843
2260
  }
1844
2261
 
2262
+ // src/dispatch-args.ts
2263
+ var PROJECT_INCOMPATIBLE_COMMANDS = ["init", "install", "update", "usage", "version"];
2264
+ function collectFlagValues(argv, flag) {
2265
+ const values = [];
2266
+ for (let i = 0; i < argv.length; i++) {
2267
+ if (argv[i] !== flag) continue;
2268
+ const raw = argv[i + 1];
2269
+ if (!raw || raw.startsWith("--")) {
2270
+ console.error(`[dispatcher] ${flag} requires a value`);
2271
+ process.exit(1);
2272
+ }
2273
+ values.push(raw);
2274
+ }
2275
+ return values;
2276
+ }
2277
+ function parseProjectFilters() {
2278
+ return collectFlagValues(process.argv, "--project");
2279
+ }
2280
+ function hasProjectFilter() {
2281
+ return process.argv.includes("--project");
2282
+ }
2283
+ function assertProjectCompatibleCommand(command) {
2284
+ if (PROJECT_INCOMPATIBLE_COMMANDS.includes(command)) {
2285
+ console.error(`[dispatcher] --project cannot be used with the "${command}" command`);
2286
+ process.exit(1);
2287
+ }
2288
+ }
2289
+ function shellQuote(value) {
2290
+ if (value === "") return "''";
2291
+ return `'${value.replace(/'/g, "'\\''")}'`;
2292
+ }
2293
+ function buildForwardedCommand(argv) {
2294
+ const tokens = [];
2295
+ for (let i = 0; i < argv.length; i++) {
2296
+ if (argv[i] === "--project") {
2297
+ i++;
2298
+ continue;
2299
+ }
2300
+ tokens.push(argv[i]);
2301
+ }
2302
+ return ["claude-task-worker", ...tokens.map(shellQuote)].join(" ");
2303
+ }
2304
+
2305
+ // src/projects-config.ts
2306
+ import { readFileSync as readFileSync4, statSync } from "node:fs";
2307
+ import { homedir as homedir2 } from "node:os";
2308
+ import { isAbsolute, join as join4 } from "node:path";
2309
+ var RESERVED_ALL = "all";
2310
+ var ProjectsConfigError = class extends Error {
2311
+ constructor(message) {
2312
+ super(message);
2313
+ this.name = "ProjectsConfigError";
2314
+ }
2315
+ };
2316
+ function getProjectsConfigPath() {
2317
+ const xdg = process.env.XDG_CONFIG_HOME;
2318
+ const configHome = xdg && xdg.length > 0 ? xdg : join4(homedir2(), ".config");
2319
+ return join4(configHome, "claude-task-worker", "projects.json");
2320
+ }
2321
+ var PROJECTS_CONFIG_PATH = getProjectsConfigPath();
2322
+ function isDirectory(path) {
2323
+ try {
2324
+ return statSync(path).isDirectory();
2325
+ } catch {
2326
+ return false;
2327
+ }
2328
+ }
2329
+ function loadProjectsConfig() {
2330
+ const configPath = PROJECTS_CONFIG_PATH;
2331
+ let raw;
2332
+ try {
2333
+ raw = JSON.parse(readFileSync4(configPath, "utf-8"));
2334
+ } catch (err) {
2335
+ if (err.code === "ENOENT") {
2336
+ throw new ProjectsConfigError(`projects.json not found: ${configPath}`);
2337
+ }
2338
+ if (err instanceof SyntaxError) {
2339
+ throw new ProjectsConfigError(`projects.json contains invalid JSON: ${configPath}: ${err.message}`);
2340
+ }
2341
+ throw err;
2342
+ }
2343
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2344
+ throw new ProjectsConfigError(`projects.json must contain a JSON object: ${configPath}`);
2345
+ }
2346
+ if (!("projects" in raw) || typeof raw["projects"] !== "object" || raw["projects"] === null || Array.isArray(raw["projects"])) {
2347
+ throw new ProjectsConfigError(`projects.json must contain a "projects" section as an object: ${configPath}`);
2348
+ }
2349
+ if ("projectGroups" in raw && (typeof raw["projectGroups"] !== "object" || raw["projectGroups"] === null || Array.isArray(raw["projectGroups"]))) {
2350
+ throw new ProjectsConfigError(`projects.json "projectGroups" must be an object: ${configPath}`);
2351
+ }
2352
+ const rawProjects = raw["projects"];
2353
+ const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
2354
+ const projectKeys = Object.keys(rawProjects);
2355
+ const groupKeys = Object.keys(rawProjectGroups);
2356
+ if (projectKeys.includes(RESERVED_ALL) || groupKeys.includes(RESERVED_ALL)) {
2357
+ throw new ProjectsConfigError(
2358
+ `"${RESERVED_ALL}" is a reserved word and cannot be used as a key in "projects" or "projectGroups"`
2359
+ );
2360
+ }
2361
+ const groupKeySet = new Set(groupKeys);
2362
+ const duplicateKeys = projectKeys.filter((key) => groupKeySet.has(key));
2363
+ if (duplicateKeys.length > 0) {
2364
+ throw new ProjectsConfigError(
2365
+ `"projects" and "projectGroups" share the same key namespace; duplicate keys are not allowed: ${duplicateKeys.join(", ")}`
2366
+ );
2367
+ }
2368
+ const projects = {};
2369
+ for (const [name, value] of Object.entries(rawProjects)) {
2370
+ if (name === "__proto__") {
2371
+ throw new ProjectsConfigError(`"__proto__" cannot be used as a key in "projects": ${configPath}`);
2372
+ }
2373
+ if (typeof value !== "string" || !isAbsolute(value)) {
2374
+ console.warn(`[projects-config] invalid projects.${name}: expected an absolute path, skipping`);
2375
+ continue;
2376
+ }
2377
+ if (!isDirectory(value)) {
2378
+ console.warn(`[projects-config] projects.${name} does not exist as a directory: ${value}, skipping`);
2379
+ continue;
2380
+ }
2381
+ projects[name] = value;
2382
+ }
2383
+ const projectGroups = {};
2384
+ for (const [groupName, value] of Object.entries(rawProjectGroups)) {
2385
+ if (groupName === "__proto__") {
2386
+ throw new ProjectsConfigError(`"__proto__" cannot be used as a key in "projectGroups": ${configPath}`);
2387
+ }
2388
+ if (!Array.isArray(value)) {
2389
+ console.warn(`[projects-config] invalid projectGroups.${groupName}: expected an array, skipping`);
2390
+ continue;
2391
+ }
2392
+ const members = [];
2393
+ for (const member of value) {
2394
+ if (typeof member !== "string" || !Object.prototype.hasOwnProperty.call(projects, member)) {
2395
+ console.warn(
2396
+ `[projects-config] projectGroups.${groupName} references unknown project "${String(member)}", skipping`
2397
+ );
2398
+ continue;
2399
+ }
2400
+ members.push(member);
2401
+ }
2402
+ projectGroups[groupName] = members;
2403
+ }
2404
+ return { projects, projectGroups };
2405
+ }
2406
+ function resolveTargetProjects(requested, config) {
2407
+ const resolved = /* @__PURE__ */ new Map();
2408
+ for (const name of requested) {
2409
+ if (name === RESERVED_ALL) {
2410
+ for (const [projectName, projectPath] of Object.entries(config.projects)) {
2411
+ resolved.set(projectName, { name: projectName, path: projectPath });
2412
+ }
2413
+ continue;
2414
+ }
2415
+ if (Object.prototype.hasOwnProperty.call(config.projects, name)) {
2416
+ resolved.set(name, { name, path: config.projects[name] });
2417
+ continue;
2418
+ }
2419
+ if (Object.prototype.hasOwnProperty.call(config.projectGroups, name)) {
2420
+ for (const projectName of config.projectGroups[name]) {
2421
+ const projectPath = config.projects[projectName];
2422
+ if (projectPath === void 0) continue;
2423
+ resolved.set(projectName, { name: projectName, path: projectPath });
2424
+ }
2425
+ continue;
2426
+ }
2427
+ const availableProjects = Object.keys(config.projects).join(", ") || "(none)";
2428
+ const availableGroups = Object.keys(config.projectGroups).join(", ") || "(none)";
2429
+ throw new ProjectsConfigError(
2430
+ `Unknown project or group: "${name}". Available projects: ${availableProjects}. Available groups: ${availableGroups}.`
2431
+ );
2432
+ }
2433
+ const resolvedProjects = Array.from(resolved.values());
2434
+ if (resolvedProjects.length === 0) {
2435
+ throw new ProjectsConfigError(`No projects resolved from requested targets: ${requested.join(", ")}`);
2436
+ }
2437
+ return resolvedProjects;
2438
+ }
2439
+
1845
2440
  // src/index.ts
1846
2441
  var WORKERS = {
1847
2442
  "exec-issue": execIssueWorker,
@@ -1856,7 +2451,7 @@ var WORKERS = {
1856
2451
  "epic-issue": epicIssueWorker
1857
2452
  };
1858
2453
  function printUsage() {
1859
- console.log(`Usage: claude-task-worker <command> [--epic <issue-number>] [--label <label-name>]
2454
+ console.log(`Usage: claude-task-worker <command> [--project <name>] [--epic <issue-number>] [--label <label-name>]
1860
2455
 
1861
2456
  Commands:
1862
2457
  init [--force] Create required GitHub labels and config file (use --force to overwrite existing files)
@@ -1880,6 +2475,7 @@ Workers:
1880
2475
  yolo Poll all workers including triage-created-issue, triage-pr, check-dependabot
1881
2476
 
1882
2477
  Options:
2478
+ --project <name> Dispatch to project(s) via herdr instead of running the worker locally. Accepts a project name, a project group name, or "all". Repeatable.
1883
2479
  --epic <number> Limit issue-based workers to sub-issues of the specified epic issue. Repeatable: any matching parent (OR).
1884
2480
  --label <name> Limit issue-based workers to issues that also carry the specified label. Repeatable: all must be present (AND).
1885
2481
 
@@ -1890,7 +2486,10 @@ Example:
1890
2486
  claude-task-worker all --epic 100 --epic 200
1891
2487
  claude-task-worker all --label priority-high
1892
2488
  claude-task-worker all --label priority-high --label needs-design
1893
- claude-task-worker yolo --epic 100 --epic 200 --label priority-high`);
2489
+ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
2490
+ claude-task-worker all --project all
2491
+ claude-task-worker all --project igsa
2492
+ claude-task-worker exec-issue --project my-app --epic 100`);
1894
2493
  }
1895
2494
  var workerType = process.argv[2];
1896
2495
  if (workerType === "version" || workerType === "--version" || workerType === "-v") {
@@ -1906,7 +2505,10 @@ if (workerType !== "all" && workerType !== "yolo" && workerType !== "init" && wo
1906
2505
  printUsage();
1907
2506
  process.exit(1);
1908
2507
  }
1909
- function collectFlagValues(flag) {
2508
+ if (hasProjectFilter()) {
2509
+ assertProjectCompatibleCommand(workerType);
2510
+ }
2511
+ function collectFlagValues2(flag) {
1910
2512
  const values = [];
1911
2513
  for (let i = 0; i < process.argv.length; i++) {
1912
2514
  if (process.argv[i] !== flag) continue;
@@ -1920,7 +2522,7 @@ function collectFlagValues(flag) {
1920
2522
  return values;
1921
2523
  }
1922
2524
  function parseEpicFilters() {
1923
- const raws = collectFlagValues("--epic");
2525
+ const raws = collectFlagValues2("--epic");
1924
2526
  return raws.map((raw) => {
1925
2527
  const num = Number(raw);
1926
2528
  if (!Number.isFinite(num) || !Number.isInteger(num) || num <= 0) {
@@ -1931,40 +2533,68 @@ function parseEpicFilters() {
1931
2533
  });
1932
2534
  }
1933
2535
  function parseLabelFilters() {
1934
- return collectFlagValues("--label");
2536
+ return collectFlagValues2("--label");
1935
2537
  }
1936
2538
  process.on("unhandledRejection", (err) => {
1937
2539
  console.error("[worker] unhandled rejection:", err);
1938
2540
  process.exit(1);
1939
2541
  });
1940
- process.on("SIGTERM", async () => {
1941
- if (isShuttingDown()) return;
1942
- setShuttingDown();
1943
- console.log(
1944
- "\n[worker] Stopping new tasks. Waiting for in-flight tasks to finish... (Send SIGTERM again to force kill)"
1945
- );
1946
- await waitForAllProcesses();
1947
- process.exit(0);
1948
- });
1949
- var forceKilling = false;
1950
- process.on("SIGINT", async () => {
1951
- if (isShuttingDown()) {
1952
- if (forceKilling) return;
1953
- forceKilling = true;
1954
- console.log("\n[worker] Force killing running tasks... (cleaning up labels and worktrees)");
1955
- shutdown("SIGKILL");
1956
- const cleanupTimeout = new Promise((resolve2) => setTimeout(resolve2, 6e4).unref());
1957
- await Promise.race([waitForAllProcesses(), cleanupTimeout]);
1958
- process.exit(1);
1959
- }
1960
- setShuttingDown();
1961
- console.log(
1962
- "\n[worker] Stopping new tasks. Waiting for in-flight tasks to finish... (Press Ctrl-C again to force kill)"
1963
- );
1964
- await waitForAllProcesses();
1965
- process.exit(0);
1966
- });
1967
- if (workerType === "init") {
2542
+ if (!hasProjectFilter()) {
2543
+ process.on("SIGTERM", async () => {
2544
+ if (isShuttingDown()) return;
2545
+ setShuttingDown();
2546
+ console.log(
2547
+ "\n[worker] Stopping new tasks. Waiting for in-flight tasks to finish... (Send SIGTERM again to force kill)"
2548
+ );
2549
+ await waitForAllProcesses();
2550
+ process.exit(0);
2551
+ });
2552
+ let forceKilling = false;
2553
+ process.on("SIGINT", async () => {
2554
+ if (isShuttingDown()) {
2555
+ if (forceKilling) return;
2556
+ forceKilling = true;
2557
+ console.log("\n[worker] Force killing running tasks... (cleaning up labels and worktrees)");
2558
+ shutdown("SIGKILL");
2559
+ const cleanupTimeout = new Promise((resolve2) => setTimeout(resolve2, 6e4).unref());
2560
+ await Promise.race([waitForAllProcesses(), cleanupTimeout]);
2561
+ process.exit(1);
2562
+ }
2563
+ setShuttingDown();
2564
+ console.log(
2565
+ "\n[worker] Stopping new tasks. Waiting for in-flight tasks to finish... (Press Ctrl-C again to force kill)"
2566
+ );
2567
+ await waitForAllProcesses();
2568
+ process.exit(0);
2569
+ });
2570
+ }
2571
+ if (hasProjectFilter()) {
2572
+ (async () => {
2573
+ let sessions = /* @__PURE__ */ new Map();
2574
+ let monitorHandle;
2575
+ const handleShutdown = async () => {
2576
+ const { shutdownDispatcher: shutdownDispatcher2 } = await init_dispatcher().then(() => dispatcher_exports);
2577
+ await shutdownDispatcher2(sessions, monitorHandle);
2578
+ };
2579
+ process.on("SIGTERM", handleShutdown);
2580
+ process.on("SIGINT", handleShutdown);
2581
+ const herdr = await Promise.resolve().then(() => (init_herdr(), herdr_exports));
2582
+ try {
2583
+ const config = loadProjectsConfig();
2584
+ const projects = resolveTargetProjects(parseProjectFilters(), config);
2585
+ const forwardedCommand = buildForwardedCommand(process.argv.slice(2));
2586
+ const { runDispatcher: runDispatcher2, monitorSessions: monitorSessions2 } = await init_dispatcher().then(() => dispatcher_exports);
2587
+ sessions = await runDispatcher2(projects, forwardedCommand);
2588
+ monitorHandle = monitorSessions2(sessions, herdr);
2589
+ } catch (err) {
2590
+ if (err instanceof ProjectsConfigError || err instanceof herdr.HerdrUnavailableError) {
2591
+ console.error(`[dispatcher] ${err.message}`);
2592
+ process.exit(1);
2593
+ }
2594
+ throw err;
2595
+ }
2596
+ })();
2597
+ } else if (workerType === "init") {
1968
2598
  const force = process.argv.slice(3).includes("--force");
1969
2599
  init({ force });
1970
2600
  } else if (workerType === "install") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "CLI tool that polls GitHub Issues/PRs and delegates work to Claude CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,6 +12,7 @@
12
12
  "dev": "tsc --noEmit --watch",
13
13
  "lint": "eslint .",
14
14
  "lint:fix": "eslint . --fix",
15
+ "test": "node --experimental-strip-types --test src/**/*.test.ts",
15
16
  "format": "prettier --write .",
16
17
  "format:check": "prettier --check .",
17
18
  "prepublishOnly": "npm run build"
@@ -39,7 +40,7 @@
39
40
  "author": "getty104",
40
41
  "license": "MIT",
41
42
  "engines": {
42
- "node": ">=20"
43
+ "node": ">=22.6.0"
43
44
  },
44
45
  "devDependencies": {
45
46
  "@eslint/js": "^10.0.1",