claude-task-worker 0.24.0 → 0.26.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 (2) hide show
  1. package/dist/index.js +233 -24
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -61,6 +61,7 @@ __export(herdr_exports, {
61
61
  HerdrUnavailableError: () => HerdrUnavailableError,
62
62
  checkHerdrAvailable: () => checkHerdrAvailable,
63
63
  paneProcessInfo: () => paneProcessInfo,
64
+ paneRead: () => paneRead,
64
65
  paneSendKeys: () => paneSendKeys,
65
66
  paneSendText: () => paneSendText,
66
67
  tabClose: () => tabClose,
@@ -129,6 +130,50 @@ async function tabList() {
129
130
  workspaceId: tab.workspace_id
130
131
  }));
131
132
  }
133
+ function isPaneReadErrorPayload(value) {
134
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
135
+ return false;
136
+ }
137
+ const keys = Object.keys(value);
138
+ if (keys.some((key) => !PANE_READ_ERROR_ALLOWED_KEYS.has(key))) {
139
+ return false;
140
+ }
141
+ const payload = value;
142
+ if (typeof payload.code !== "string") {
143
+ return false;
144
+ }
145
+ if ("message" in payload && typeof payload.message !== "string") {
146
+ return false;
147
+ }
148
+ return true;
149
+ }
150
+ async function paneRead(paneId, options) {
151
+ const args = ["pane", "read", paneId, "--source", options?.source ?? "visible"];
152
+ if (options?.lines !== void 0) {
153
+ args.push("--lines", String(options.lines));
154
+ }
155
+ const { execError, stdout, stderr } = await runHerdr(args);
156
+ const trimmed = stdout.trim();
157
+ if (trimmed.startsWith("{")) {
158
+ let parsed;
159
+ try {
160
+ parsed = JSON.parse(trimmed);
161
+ } catch {
162
+ parsed = void 0;
163
+ }
164
+ if (isPaneReadErrorPayload(parsed)) {
165
+ throw new HerdrError(
166
+ `herdr ${args.join(" ")} failed: [${parsed.code}] ${parsed.message ?? ""}`.trim(),
167
+ parsed.code
168
+ );
169
+ }
170
+ }
171
+ if (execError) {
172
+ const stderrSuffix = stderr.trim() ? `: ${stderr.trim()}` : "";
173
+ throw new Error(`herdr ${args.join(" ")} failed: ${execError.message}${stderrSuffix}`);
174
+ }
175
+ return stdout;
176
+ }
132
177
  async function paneSendText(paneId, text) {
133
178
  await execHerdr(["pane", "send-text", paneId, text], { allowEmptyResult: true });
134
179
  }
@@ -162,7 +207,7 @@ async function checkHerdrAvailable() {
162
207
  );
163
208
  }
164
209
  }
165
- var childProcess2, HerdrUnavailableError, HerdrError, HERDR_TIMEOUT_MS;
210
+ var childProcess2, HerdrUnavailableError, HerdrError, HERDR_TIMEOUT_MS, PANE_READ_ERROR_ALLOWED_KEYS;
166
211
  var init_herdr = __esm({
167
212
  "src/herdr.ts"() {
168
213
  "use strict";
@@ -184,25 +229,36 @@ var init_herdr = __esm({
184
229
  }
185
230
  };
186
231
  HERDR_TIMEOUT_MS = 15 * 1e3;
232
+ PANE_READ_ERROR_ALLOWED_KEYS = /* @__PURE__ */ new Set(["code", "message"]);
187
233
  }
188
234
  });
189
235
 
190
236
  // src/dispatcher.ts
191
237
  var dispatcher_exports = {};
192
238
  __export(dispatcher_exports, {
239
+ PANE_READY_POLL_INTERVAL_MS: () => PANE_READY_POLL_INTERVAL_MS,
240
+ PANE_READY_TIMEOUT_MS: () => PANE_READY_TIMEOUT_MS,
193
241
  POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
242
+ SEND_MAX_ATTEMPTS: () => SEND_MAX_ATTEMPTS,
194
243
  SHUTDOWN_RETRY_TIMEOUT_MS: () => SHUTDOWN_RETRY_TIMEOUT_MS,
195
244
  SHUTDOWN_TIMEOUT_MS: () => SHUTDOWN_TIMEOUT_MS,
196
245
  TAB_LABEL_PREFIX: () => TAB_LABEL_PREFIX,
246
+ WORKER_STARTUP_POLL_INTERVAL_MS: () => WORKER_STARTUP_POLL_INTERVAL_MS,
247
+ WORKER_STARTUP_TIMEOUT_MS: () => WORKER_STARTUP_TIMEOUT_MS,
197
248
  createDispatcherShutdownHandler: () => createDispatcherShutdownHandler,
198
249
  formatUptime: () => formatUptime,
250
+ isShellProcess: () => isShellProcess,
251
+ isWorkerProcess: () => isWorkerProcess,
199
252
  monitorSessions: () => monitorSessions,
200
253
  pollOnce: () => pollOnce,
201
254
  removeSession: () => removeSession,
202
255
  renderSessionTable: () => renderSessionTable,
203
256
  runDispatcher: () => runDispatcher,
204
257
  shutdownDispatcher: () => shutdownDispatcher,
205
- tabLabelFor: () => tabLabelFor
258
+ startWorkerInPane: () => startWorkerInPane,
259
+ tabLabelFor: () => tabLabelFor,
260
+ waitForPaneReady: () => waitForPaneReady,
261
+ waitForWorkerStartup: () => waitForWorkerStartup
206
262
  });
207
263
  async function loadHerdr() {
208
264
  return await Promise.resolve().then(() => (init_herdr(), herdr_exports));
@@ -213,8 +269,72 @@ async function loadTable() {
213
269
  function tabLabelFor(projectName) {
214
270
  return `${TAB_LABEL_PREFIX}${projectName}`;
215
271
  }
216
- async function runDispatcher(projects, forwardedCommand) {
217
- const { checkHerdrAvailable: checkHerdrAvailable2, tabCreate: tabCreate2, tabClose: tabClose2, tabList: tabList2, paneSendText: paneSendText2, paneSendKeys: paneSendKeys2 } = await loadHerdr();
272
+ function sleep(ms) {
273
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
274
+ }
275
+ function isWorkerProcess(process2) {
276
+ return process2.cmdline?.includes("claude-task-worker") ?? false;
277
+ }
278
+ function isShellProcess(process2) {
279
+ return SHELL_NAME_PATTERN.test(process2.name ?? "") || SHELL_NAME_PATTERN.test(process2.cmdline ?? "");
280
+ }
281
+ async function waitForPaneReady(paneId, herdr, options) {
282
+ const timeoutMs = options?.timeoutMs ?? PANE_READY_TIMEOUT_MS;
283
+ const pollIntervalMs = options?.pollIntervalMs ?? PANE_READY_POLL_INTERVAL_MS;
284
+ const deadline = Date.now() + timeoutMs;
285
+ for (; ; ) {
286
+ const content = await herdr.paneRead(paneId, { source: "visible" });
287
+ if (content.trim() !== "") return true;
288
+ if (Date.now() >= deadline) return false;
289
+ await sleep(pollIntervalMs);
290
+ }
291
+ }
292
+ async function waitForWorkerStartup(paneId, herdr, options) {
293
+ const timeoutMs = options?.timeoutMs ?? WORKER_STARTUP_TIMEOUT_MS;
294
+ const pollIntervalMs = options?.pollIntervalMs ?? WORKER_STARTUP_POLL_INTERVAL_MS;
295
+ const deadline = Date.now() + timeoutMs;
296
+ for (; ; ) {
297
+ const { foregroundProcesses } = await herdr.paneProcessInfo(paneId);
298
+ if (foregroundProcesses.some(isWorkerProcess)) return "started";
299
+ if (foregroundProcesses.length > 0 && !foregroundProcesses.every(isShellProcess)) return "other";
300
+ if (Date.now() >= deadline) return "shell";
301
+ await sleep(pollIntervalMs);
302
+ }
303
+ }
304
+ async function startWorkerInPane(paneId, forwardedCommand, herdr, options) {
305
+ const maxAttempts = options?.sendMaxAttempts ?? SEND_MAX_ATTEMPTS;
306
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
307
+ const ready = await waitForPaneReady(paneId, herdr, {
308
+ timeoutMs: options?.paneReadyTimeoutMs,
309
+ pollIntervalMs: options?.paneReadyPollIntervalMs
310
+ });
311
+ if (!ready) {
312
+ console.warn(`[dispatcher] pane ${paneId} produced no prompt before the timeout, sending the command anyway`);
313
+ }
314
+ await herdr.paneSendText(paneId, forwardedCommand);
315
+ await herdr.paneSendKeys(paneId, "enter");
316
+ const result = await waitForWorkerStartup(paneId, herdr, {
317
+ timeoutMs: options?.workerStartupTimeoutMs,
318
+ pollIntervalMs: options?.workerStartupPollIntervalMs
319
+ });
320
+ if (result === "started") return true;
321
+ if (result === "other") {
322
+ console.warn(
323
+ `[dispatcher] pane ${paneId} foreground is neither the shell nor the worker, giving up without resending`
324
+ );
325
+ return false;
326
+ }
327
+ if (attempt < maxAttempts) {
328
+ console.warn(
329
+ `[dispatcher] worker did not start in pane ${paneId} (attempt ${attempt}/${maxAttempts}), resending the command`
330
+ );
331
+ }
332
+ }
333
+ return false;
334
+ }
335
+ async function runDispatcher(projects, forwardedCommand, timing) {
336
+ const herdr = await loadHerdr();
337
+ const { checkHerdrAvailable: checkHerdrAvailable2, tabCreate: tabCreate2, tabClose: tabClose2, tabList: tabList2 } = herdr;
218
338
  try {
219
339
  await checkHerdrAvailable2();
220
340
  } catch (error) {
@@ -241,8 +361,12 @@ async function runDispatcher(projects, forwardedCommand) {
241
361
  startedAt: /* @__PURE__ */ new Date(),
242
362
  status: "running"
243
363
  });
244
- await paneSendText2(paneId, forwardedCommand);
245
- await paneSendKeys2(paneId, "enter");
364
+ const started = await startWorkerInPane(paneId, forwardedCommand, herdr, timing);
365
+ if (!started) {
366
+ throw new Error(
367
+ `worker did not start in pane ${paneId} after ${timing?.sendMaxAttempts ?? SEND_MAX_ATTEMPTS} attempt(s)`
368
+ );
369
+ }
246
370
  } catch (error) {
247
371
  console.error(`[dispatcher] failed to dispatch project "${project.name}": ${error}`);
248
372
  sessions.delete(project.name);
@@ -270,8 +394,7 @@ async function pollOnce(sessions, herdr) {
270
394
  for (const [name, session] of [...sessions.entries()]) {
271
395
  try {
272
396
  const { foregroundProcesses } = await herdr.paneProcessInfo(session.paneId);
273
- const isAlive = foregroundProcesses.some((process2) => process2.cmdline?.includes("claude-task-worker"));
274
- if (!isAlive) {
397
+ if (!foregroundProcesses.some(isWorkerProcess)) {
275
398
  await removeSession(sessions, name, { closeTab: true });
276
399
  }
277
400
  } catch (error) {
@@ -467,14 +590,20 @@ function createDispatcherShutdownHandler(shutdown2) {
467
590
  };
468
591
  return { handle, isShuttingDown: () => shuttingDown2 };
469
592
  }
470
- var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, TAB_LABEL_PREFIX, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
593
+ var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, PANE_READY_TIMEOUT_MS, PANE_READY_POLL_INTERVAL_MS, WORKER_STARTUP_TIMEOUT_MS, WORKER_STARTUP_POLL_INTERVAL_MS, SEND_MAX_ATTEMPTS, TAB_LABEL_PREFIX, SHELL_NAME_PATTERN, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
471
594
  var init_dispatcher = __esm({
472
595
  async "src/dispatcher.ts"() {
473
596
  "use strict";
474
597
  ({ getDisplayWidth: getDisplayWidth2, truncateToWidth: truncateToWidth2, padToWidth: padToWidth2 } = await loadTable());
475
598
  POLL_INTERVAL_MS = 7 * 1e3;
476
599
  SHUTDOWN_TIMEOUT_MS = 10 * 60 * 1e3;
600
+ PANE_READY_TIMEOUT_MS = 30 * 1e3;
601
+ PANE_READY_POLL_INTERVAL_MS = 200;
602
+ WORKER_STARTUP_TIMEOUT_MS = 30 * 1e3;
603
+ WORKER_STARTUP_POLL_INTERVAL_MS = 500;
604
+ SEND_MAX_ATTEMPTS = 3;
477
605
  TAB_LABEL_PREFIX = "ctw:";
606
+ SHELL_NAME_PATTERN = /^-?(zsh|bash|sh)$/;
478
607
  SHUTDOWN_RETRY_TIMEOUT_MS = 90 * 1e3;
479
608
  CTRL_C_KEY = "ctrl+c";
480
609
  }
@@ -1510,10 +1639,86 @@ function isGeneratedWorktreeName(name) {
1510
1639
 
1511
1640
  // src/slack.ts
1512
1641
  import { exec } from "node:child_process";
1513
- import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
1514
- import { homedir } from "node:os";
1515
- import { join as join2 } from "node:path";
1642
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1643
+ import { homedir as homedir2 } from "node:os";
1644
+ import { join as join3 } from "node:path";
1516
1645
  import { promisify as promisify2 } from "node:util";
1646
+
1647
+ // src/runcat.ts
1648
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
1649
+ import { homedir } from "node:os";
1650
+ import { dirname, join as join2 } from "node:path";
1651
+ var RUNCAT_OUT_FILE = process.env.RUNCAT_OUT_FILE ?? join2(process.env.HOME ?? homedir(), ".claude", "runcat-usage.json");
1652
+ var JST = "Asia/Tokyo";
1653
+ function formatG(value) {
1654
+ return String(Number(value.toPrecision(6)));
1655
+ }
1656
+ function jstFields(date) {
1657
+ const parts = new Intl.DateTimeFormat("en-US", {
1658
+ timeZone: JST,
1659
+ month: "2-digit",
1660
+ day: "2-digit",
1661
+ hour: "2-digit",
1662
+ minute: "2-digit",
1663
+ hourCycle: "h23"
1664
+ }).formatToParts(date);
1665
+ const pick2 = (type) => parts.find((p) => p.type === type)?.value ?? "";
1666
+ return { month: pick2("month"), day: pick2("day"), hour: pick2("hour"), minute: pick2("minute") };
1667
+ }
1668
+ function parseResetsAt(isoString) {
1669
+ const date = new Date(isoString);
1670
+ return Number.isNaN(date.getTime()) ? null : date;
1671
+ }
1672
+ function resetStamp(isoString, now = /* @__PURE__ */ new Date()) {
1673
+ const date = parseResetsAt(isoString);
1674
+ if (!date) return "";
1675
+ const reset = jstFields(date);
1676
+ const today = jstFields(now);
1677
+ const time = `${reset.hour}:${reset.minute}`;
1678
+ if (reset.month === today.month && reset.day === today.day) return time;
1679
+ return `${reset.month}/${reset.day} ${time}`;
1680
+ }
1681
+ function resetHour(isoString) {
1682
+ const date = parseResetsAt(isoString);
1683
+ return date ? jstFields(date).hour : "";
1684
+ }
1685
+ function metric(title, pct, resetsAt, now) {
1686
+ const stamp = resetStamp(resetsAt, now);
1687
+ return {
1688
+ title,
1689
+ formattedValue: `${formatG(pct)}%` + (stamp ? ` \u21BB${stamp}` : ""),
1690
+ normalizedValue: Math.round(pct / 100 * 1e4) / 1e4
1691
+ };
1692
+ }
1693
+ function buildRuncatSnapshot(usage, now = /* @__PURE__ */ new Date()) {
1694
+ const bar = `${formatG(usage.fiveHourUtilization)}/${formatG(usage.sevenDayUtilization)}`;
1695
+ const stamp = resetHour(usage.fiveHourResetsAt);
1696
+ return {
1697
+ title: "Claude Code",
1698
+ symbol: "staroflife",
1699
+ metrics: [
1700
+ metric("5h", usage.fiveHourUtilization, usage.fiveHourResetsAt, now),
1701
+ metric("7d", usage.sevenDayUtilization, usage.sevenDayResetsAt, now)
1702
+ ],
1703
+ metricsBarValue: stamp ? `${bar} \u21BB${stamp}` : bar,
1704
+ lastUpdatedDate: now.toISOString().replace(/\.\d{3}Z$/, "Z")
1705
+ };
1706
+ }
1707
+ function writeRuncatUsage(usage, outFile = RUNCAT_OUT_FILE) {
1708
+ const snapshot = buildRuncatSnapshot(usage);
1709
+ const dir = dirname(outFile);
1710
+ const tmp = join2(dir, `.runcat-${process.pid}-${Date.now()}.json`);
1711
+ try {
1712
+ mkdirSync(dir, { recursive: true });
1713
+ writeFileSync(tmp, JSON.stringify(snapshot), "utf-8");
1714
+ renameSync(tmp, outFile);
1715
+ } catch (err) {
1716
+ rmSync(tmp, { force: true });
1717
+ console.error(`[runcat] Failed to write ${outFile}: ${err}`);
1718
+ }
1719
+ }
1720
+
1721
+ // src/slack.ts
1517
1722
  var execAsync = promisify2(exec);
1518
1723
  var WEBHOOK_URL = process.env.CLAUDE_TASK_WORKER_SLACK_WEBHOOK_URL;
1519
1724
  var USAGE_CACHE_PATH = "/tmp/claude-usage-cache.json";
@@ -1539,7 +1744,7 @@ async function getOAuthToken() {
1539
1744
  const { stdout } = await execAsync('security find-generic-password -s "Claude Code-credentials" -w');
1540
1745
  return extractToken(JSON.parse(stdout.trim()));
1541
1746
  } catch {
1542
- const raw = readFileSync2(join2(homedir(), ".claude", ".credentials.json"), "utf-8");
1747
+ const raw = readFileSync2(join3(homedir2(), ".claude", ".credentials.json"), "utf-8");
1543
1748
  return extractToken(JSON.parse(raw));
1544
1749
  }
1545
1750
  }
@@ -1556,7 +1761,7 @@ function readUsageCache() {
1556
1761
  }
1557
1762
  function writeUsageCache(data) {
1558
1763
  try {
1559
- writeFileSync(USAGE_CACHE_PATH, JSON.stringify({ timestamp: Date.now(), data }));
1764
+ writeFileSync2(USAGE_CACHE_PATH, JSON.stringify({ timestamp: Date.now(), data }));
1560
1765
  } catch {
1561
1766
  }
1562
1767
  }
@@ -1604,9 +1809,7 @@ function formatResetTimeJST(isoString) {
1604
1809
  minute: "2-digit"
1605
1810
  });
1606
1811
  }
1607
- async function buildTokenLimitText() {
1608
- const usage = await fetchUsageInfo();
1609
- if (!usage) return "";
1812
+ function formatTokenLimitText(usage) {
1610
1813
  const fiveH = usage.fiveHourUtilization.toFixed(1);
1611
1814
  const sevenD = usage.sevenDayUtilization.toFixed(1);
1612
1815
  const emoji = utilizationEmoji(Math.max(usage.fiveHourUtilization, usage.sevenDayUtilization));
@@ -1614,6 +1817,12 @@ async function buildTokenLimitText() {
1614
1817
  const sevenDReset = formatResetTimeJST(usage.sevenDayResetsAt);
1615
1818
  return ` | ${emoji} 5h: ${fiveH}% (reset: ${fiveHReset}) / 7d: ${sevenD}% (reset: ${sevenDReset})`;
1616
1819
  }
1820
+ async function buildTokenLimitText() {
1821
+ const usage = await fetchUsageInfo();
1822
+ if (!usage) return "";
1823
+ writeRuncatUsage(usage);
1824
+ return formatTokenLimitText(usage);
1825
+ }
1617
1826
  async function notifyTaskCompleted(workerName, repoName, id, title, url, output) {
1618
1827
  const tokenText = await buildTokenLimitText();
1619
1828
  const truncatedOutput = output && output.length > 1e3 ? `\u2026${output.slice(-1e3)}` : output;
@@ -2473,11 +2682,11 @@ async function update() {
2473
2682
 
2474
2683
  // src/commands/version.ts
2475
2684
  import { readFileSync as readFileSync3 } from "node:fs";
2476
- import { dirname, join as join3 } from "node:path";
2685
+ import { dirname as dirname2, join as join4 } from "node:path";
2477
2686
  import { fileURLToPath } from "node:url";
2478
2687
  function version() {
2479
- const here = dirname(fileURLToPath(import.meta.url));
2480
- const pkgPath = join3(here, "..", "package.json");
2688
+ const here = dirname2(fileURLToPath(import.meta.url));
2689
+ const pkgPath = join4(here, "..", "package.json");
2481
2690
  try {
2482
2691
  const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
2483
2692
  console.log(pkg.version ?? "unknown");
@@ -2532,8 +2741,8 @@ function buildForwardedCommand(argv) {
2532
2741
 
2533
2742
  // src/projects-config.ts
2534
2743
  import { readFileSync as readFileSync4, statSync } from "node:fs";
2535
- import { homedir as homedir2 } from "node:os";
2536
- import { isAbsolute, join as join4 } from "node:path";
2744
+ import { homedir as homedir3 } from "node:os";
2745
+ import { isAbsolute, join as join5 } from "node:path";
2537
2746
  var RESERVED_ALL = "all";
2538
2747
  var ProjectsConfigError = class extends Error {
2539
2748
  constructor(message) {
@@ -2543,8 +2752,8 @@ var ProjectsConfigError = class extends Error {
2543
2752
  };
2544
2753
  function getProjectsConfigPath() {
2545
2754
  const xdg = process.env.XDG_CONFIG_HOME;
2546
- const configHome = xdg && xdg.length > 0 ? xdg : join4(homedir2(), ".config");
2547
- return join4(configHome, "claude-task-worker", "projects.json");
2755
+ const configHome = xdg && xdg.length > 0 ? xdg : join5(homedir3(), ".config");
2756
+ return join5(configHome, "claude-task-worker", "projects.json");
2548
2757
  }
2549
2758
  var PROJECTS_CONFIG_PATH = getProjectsConfigPath();
2550
2759
  function isDirectory(path) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.24.0",
3
+ "version": "0.26.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",