claude-task-worker 0.142.1 → 0.142.3

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/README.md CHANGED
@@ -430,7 +430,7 @@ export CLAUDE_TASK_WORKER_SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx
430
430
  claude-task-worker all
431
431
  ```
432
432
 
433
- 通知には Claude API の使用状況も含まれる。あわせて [RunCat Neo](https://kyome.io/runcat/) 用のスナップショットを `~/.claude/runcat-usage.json`(`RUNCAT_OUT_FILE` で変更可)へ書き出す。クラウド実行のタスクは通知の先頭行にセッションURLが入る。
433
+ Claude API の使用状況はタスク通知には含めない(`usage` コマンドを実行したときだけ取得する)。[RunCat Neo](https://kyome.io/runcat/) 用のスナップショット `~/.claude/runcat-usage.json`(`RUNCAT_OUT_FILE` で変更可)も `usage` が書き出す。クラウド実行のタスクは通知の先頭行にセッションURLが入る。
434
434
 
435
435
  ## プロセス管理
436
436
 
@@ -2141,7 +2141,7 @@ function generateWorktreeName() {
2141
2141
 
2142
2142
  // src/slack.ts
2143
2143
  import { exec } from "node:child_process";
2144
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "node:fs";
2144
+ import { readFileSync as readFileSync2, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
2145
2145
  import { homedir as homedir3 } from "node:os";
2146
2146
  import { join as join3 } from "node:path";
2147
2147
  import { promisify as promisify2 } from "node:util";
@@ -2234,9 +2234,11 @@ function writeRuncatUsage(usage, outFile = RUNCAT_OUT_FILE) {
2234
2234
  // src/slack.ts
2235
2235
  var execAsync = promisify2(exec);
2236
2236
  var WEBHOOK_URL = process.env.CLAUDE_TASK_WORKER_SLACK_WEBHOOK_URL;
2237
- var USAGE_CACHE_PATH = "/tmp/claude-usage-cache.json";
2238
2237
  var USAGE_POLL_INTERVAL_SECONDS = 600;
2239
- var USAGE_CACHE_TTL_SECONDS = 360;
2238
+ var USAGE_BACKOFF_PATH = "/tmp/claude-usage-backoff.json";
2239
+ var USAGE_RATE_LIMIT_DEFAULT_BACKOFF_SECONDS = 600;
2240
+ var USAGE_CACHE_PATH = "/tmp/claude-usage-cache.json";
2241
+ var USAGE_CACHE_TTL_SECONDS = 600;
2240
2242
  async function send(payload) {
2241
2243
  if (!WEBHOOK_URL) return;
2242
2244
  try {
@@ -2257,59 +2259,112 @@ function parseUsageResponse(body) {
2257
2259
  sevenDayResetsAt: body.seven_day?.resets_at ?? null
2258
2260
  };
2259
2261
  }
2260
- function extractToken(credentials) {
2261
- if (typeof credentials === "string") return credentials;
2262
- return credentials.claudeAiOauth?.accessToken ?? credentials.oauth_token ?? credentials.access_token ?? "";
2262
+ function extractCredential(credentials) {
2263
+ if (typeof credentials === "string") return { token: credentials };
2264
+ return {
2265
+ token: credentials.claudeAiOauth?.accessToken ?? credentials.oauth_token ?? credentials.access_token ?? "",
2266
+ expiresAt: credentials.claudeAiOauth?.expiresAt
2267
+ };
2263
2268
  }
2264
- async function getOAuthToken() {
2269
+ async function getStoredCredential() {
2265
2270
  try {
2266
2271
  const { stdout } = await execAsync('security find-generic-password -s "Claude Code-credentials" -w');
2267
- return extractToken(JSON.parse(stdout.trim()));
2272
+ return extractCredential(JSON.parse(stdout.trim()));
2268
2273
  } catch {
2269
2274
  const raw = readFileSync2(join3(homedir3(), ".claude", ".credentials.json"), "utf-8");
2270
- return extractToken(JSON.parse(raw));
2275
+ return extractCredential(JSON.parse(raw));
2271
2276
  }
2272
2277
  }
2273
- function readUsageCache() {
2278
+ function rateLimitBackoffMs(retryAfter, now = Date.now()) {
2279
+ if (retryAfter) {
2280
+ const seconds = Number(retryAfter);
2281
+ if (Number.isFinite(seconds) && seconds > 0) return seconds * 1e3;
2282
+ const date = Date.parse(retryAfter);
2283
+ if (!Number.isNaN(date) && date > now) return date - now;
2284
+ }
2285
+ return USAGE_RATE_LIMIT_DEFAULT_BACKOFF_SECONDS * 1e3;
2286
+ }
2287
+ function readBackoffUntil() {
2274
2288
  try {
2275
- const raw = readFileSync2(USAGE_CACHE_PATH, "utf-8");
2276
- const cached = JSON.parse(raw);
2277
- if (Date.now() - cached.timestamp < USAGE_CACHE_TTL_SECONDS * 1e3) {
2278
- return cached.data;
2279
- }
2289
+ return Number(JSON.parse(readFileSync2(USAGE_BACKOFF_PATH, "utf-8")).until) || 0;
2290
+ } catch {
2291
+ return 0;
2292
+ }
2293
+ }
2294
+ function writeBackoffUntil(until) {
2295
+ try {
2296
+ const tmp = `${USAGE_BACKOFF_PATH}.${process.pid}.tmp`;
2297
+ writeFileSync4(tmp, JSON.stringify({ until: Math.max(until, readBackoffUntil()) }));
2298
+ renameSync3(tmp, USAGE_BACKOFF_PATH);
2299
+ } catch {
2300
+ }
2301
+ }
2302
+ function readUsageCache(now = Date.now()) {
2303
+ try {
2304
+ const { fetchedAt, usage } = JSON.parse(readFileSync2(USAGE_CACHE_PATH, "utf-8"));
2305
+ return now - Number(fetchedAt) < USAGE_CACHE_TTL_SECONDS * 1e3 ? usage : null;
2280
2306
  } catch {
2307
+ return null;
2281
2308
  }
2282
- return null;
2283
2309
  }
2284
- function writeUsageCache(data) {
2310
+ function writeUsageCache(usage) {
2285
2311
  try {
2286
- writeFileSync4(USAGE_CACHE_PATH, JSON.stringify({ timestamp: Date.now(), data }));
2312
+ const tmp = `${USAGE_CACHE_PATH}.${process.pid}.tmp`;
2313
+ writeFileSync4(tmp, JSON.stringify({ fetchedAt: Date.now(), usage }));
2314
+ renameSync3(tmp, USAGE_CACHE_PATH);
2287
2315
  } catch {
2288
2316
  }
2289
2317
  }
2290
- async function fetchUsageInfo(fresh) {
2291
- const cached = fresh ? null : readUsageCache();
2318
+ async function fetchUsageInfo() {
2319
+ const backoffUntil = readBackoffUntil();
2320
+ if (Date.now() < backoffUntil) {
2321
+ console.error(`[slack] Usage API rate limited; skipping until ${new Date(backoffUntil).toISOString()}`);
2322
+ return null;
2323
+ }
2324
+ const cached = readUsageCache();
2292
2325
  if (cached) return cached;
2293
2326
  try {
2294
- const token = await getOAuthToken();
2295
- const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
2296
- headers: {
2297
- Authorization: `Bearer ${token}`,
2298
- "anthropic-beta": "oauth-2025-04-20"
2299
- }
2300
- });
2301
- if (!res.ok) {
2302
- console.error(`[slack] Usage API returned ${res.status}`);
2327
+ const { token, expiresAt } = await getStoredCredential();
2328
+ if (!token) {
2329
+ console.error("[slack] No OAuth token found (sign in to Claude Code)");
2303
2330
  return null;
2304
2331
  }
2305
- const data = parseUsageResponse(await res.json());
2306
- writeUsageCache(data);
2307
- return data;
2332
+ if (expiresAt !== void 0 && expiresAt <= Date.now()) {
2333
+ console.error(
2334
+ `[slack] OAuth token expired at ${new Date(expiresAt).toISOString()} (it is refreshed when claude runs)`
2335
+ );
2336
+ return null;
2337
+ }
2338
+ const usage = await handleUsageResponse(await requestUsage(token));
2339
+ if (usage) writeUsageCache(usage);
2340
+ return usage;
2308
2341
  } catch (err) {
2309
2342
  console.error(`[slack] Failed to fetch usage info: ${err}`);
2310
2343
  return null;
2311
2344
  }
2312
2345
  }
2346
+ function requestUsage(token) {
2347
+ return fetch("https://api.anthropic.com/api/oauth/usage", {
2348
+ headers: {
2349
+ Authorization: `Bearer ${token}`,
2350
+ "anthropic-beta": "oauth-2025-04-20"
2351
+ }
2352
+ });
2353
+ }
2354
+ async function handleUsageResponse(res) {
2355
+ if (!res.ok) {
2356
+ const body = (await res.text().catch(() => "")).slice(0, 500);
2357
+ if (res.status === 429) {
2358
+ const until = Date.now() + rateLimitBackoffMs(res.headers.get("retry-after"));
2359
+ writeBackoffUntil(until);
2360
+ console.error(`[slack] Usage API returned 429; backing off until ${new Date(until).toISOString()}: ${body}`);
2361
+ } else {
2362
+ console.error(`[slack] Usage API returned ${res.status}: ${body}`);
2363
+ }
2364
+ return null;
2365
+ }
2366
+ return parseUsageResponse(await res.json());
2367
+ }
2313
2368
  function utilizationEmoji(value) {
2314
2369
  if (value < 50) return "\u{1F7E2}";
2315
2370
  if (value < 80) return "\u{1F7E1}";
@@ -2339,8 +2394,8 @@ function formatTokenLimitText(usage) {
2339
2394
  const values = [usage.fiveHourUtilization, usage.sevenDayUtilization].filter((v) => v !== null);
2340
2395
  return ` | ${utilizationEmoji(Math.max(...values))} ${parts.join(" / ")}`;
2341
2396
  }
2342
- async function buildTokenLimitText(fresh = false) {
2343
- const usage = await fetchUsageInfo(fresh);
2397
+ async function buildTokenLimitText() {
2398
+ const usage = await fetchUsageInfo();
2344
2399
  if (!usage) return null;
2345
2400
  writeRuncatUsage(usage);
2346
2401
  return formatTokenLimitText(usage);
@@ -2354,13 +2409,13 @@ function truncateOutputForNotification(output) {
2354
2409
  return `\u2026${output.slice(-1e3)}`;
2355
2410
  }
2356
2411
  function buildTaskNotificationText(params) {
2357
- const { status, workerName, repoName, id, title, url, tokenText, output, cloud } = params;
2412
+ const { status, workerName, repoName, id, title, url, output, cloud } = params;
2358
2413
  const emoji = status === "completed" ? "\u2705" : "\u274C";
2359
2414
  const label = status === "completed" ? "completed" : "failed";
2360
2415
  const truncatedOutput = output ? truncateOutputForNotification(output) : output;
2361
2416
  const outputBlock = truncatedOutput ? `
2362
2417
  \`\`\`${truncatedOutput}\`\`\`` : "";
2363
- const body = `${emoji} [${workerName}] ${repoName} | Task ${label}: <${url}|#${id} ${title}>${tokenText}${outputBlock}`;
2418
+ const body = `${emoji} [${workerName}] ${repoName} | Task ${label}: <${url}|#${id} ${title}>${outputBlock}`;
2364
2419
  let sessionUrlPrefix = "";
2365
2420
  if (cloud) {
2366
2421
  sessionUrlPrefix = cloud.sessionId?.trim() ? `https://claude.ai/code/${cloud.sessionId.trim()}
@@ -2369,7 +2424,6 @@ function buildTaskNotificationText(params) {
2369
2424
  return `${sessionUrlPrefix}${body}`;
2370
2425
  }
2371
2426
  async function notifyTaskCompleted(workerName, repoName, id, title, url, output, cloud) {
2372
- const tokenText = await buildTokenLimitText() ?? "";
2373
2427
  await send({
2374
2428
  text: buildTaskNotificationText({
2375
2429
  status: "completed",
@@ -2378,14 +2432,12 @@ async function notifyTaskCompleted(workerName, repoName, id, title, url, output,
2378
2432
  id,
2379
2433
  title,
2380
2434
  url,
2381
- tokenText,
2382
2435
  output,
2383
2436
  cloud
2384
2437
  })
2385
2438
  });
2386
2439
  }
2387
2440
  async function notifyTaskFailed(workerName, repoName, id, title, url, output, cloud) {
2388
- const tokenText = await buildTokenLimitText() ?? "";
2389
2441
  await send({
2390
2442
  text: buildTaskNotificationText({
2391
2443
  status: "failed",
@@ -2394,7 +2446,6 @@ async function notifyTaskFailed(workerName, repoName, id, title, url, output, cl
2394
2446
  id,
2395
2447
  title,
2396
2448
  url,
2397
- tokenText,
2398
2449
  output,
2399
2450
  cloud
2400
2451
  })
package/dist/index.js CHANGED
@@ -65,7 +65,7 @@ import {
65
65
  shutdown,
66
66
  waitForAllProcesses,
67
67
  writeLastRun
68
- } from "./chunk-IJLDHTJ4.js";
68
+ } from "./chunk-QCQA2UJO.js";
69
69
  import {
70
70
  UserConfigError,
71
71
  assertCloudCompatibleCommand,
@@ -1893,7 +1893,7 @@ if (hasProjectFilter()) {
1893
1893
  })();
1894
1894
  } else if (workerType === "usage") {
1895
1895
  const pollUsage = async (notify) => {
1896
- const text = await buildTokenLimitText(true);
1896
+ const text = await buildTokenLimitText();
1897
1897
  if (text === null) {
1898
1898
  console.error(`[usage] ${(/* @__PURE__ */ new Date()).toISOString()} Failed to fetch usage info`);
1899
1899
  return;
package/dist/lib.js CHANGED
@@ -55,7 +55,7 @@ import {
55
55
  removeWorktreeByBranch,
56
56
  run,
57
57
  syncDefaultBranch
58
- } from "./chunk-IJLDHTJ4.js";
58
+ } from "./chunk-QCQA2UJO.js";
59
59
  import {
60
60
  getPermissionMode,
61
61
  getRunMode,
package/dist/slack.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  /** `usage` コマンドが利用状況をポーリングする間隔 */
2
2
  export declare const USAGE_POLL_INTERVAL_SECONDS = 600;
3
+ /** 取得結果を使い回す期間。この間は usage API を叩かない */
4
+ export declare const USAGE_CACHE_TTL_SECONDS = 600;
3
5
  export declare function send(payload: Record<string, unknown>): Promise<void>;
4
6
  export interface UsageInfo {
5
7
  fiveHourUtilization: number | null;
@@ -16,15 +18,32 @@ export declare function parseUsageResponse(body: {
16
18
  five_hour?: UsageWindow | null;
17
19
  seven_day?: UsageWindow | null;
18
20
  }): UsageInfo;
21
+ interface CredentialPayload {
22
+ claudeAiOauth?: {
23
+ accessToken?: string;
24
+ expiresAt?: number;
25
+ };
26
+ oauth_token?: string;
27
+ access_token?: string;
28
+ }
29
+ export interface OAuthCredential {
30
+ token: string;
31
+ expiresAt?: number;
32
+ }
33
+ export declare function extractCredential(credentials: CredentialPayload | string): OAuthCredential;
34
+ /**
35
+ * 429 後に API を叩かない期間(ミリ秒)。Retry-After(秒 or HTTP-date)を優先し、
36
+ * 無い・読めないときは既定値。失敗のたびに叩き直すと制限が明けないため
37
+ */
38
+ export declare function rateLimitBackoffMs(retryAfter: string | null, now?: number): number;
19
39
  /** 返ってきた窓だけを並べる。どちらも無ければ空文字 */
20
40
  export declare function formatTokenLimitText(usage: UsageInfo): string;
21
41
  /**
22
42
  * 現在の利用状況を取得し、RunCat Neo 用スナップショットを更新したうえで通知に添えるテキストを返す。
23
- * ワーカーの完了/失敗通知はすべてここを通るため、タスクが終わるたびに RunCat 側も最新化される
24
- * (Slack webhook 未設定でも通知が no-op になるだけで、スナップショットの更新は行われる)。
43
+ * 呼ぶのは `usage` コマンドだけ(ワーカーの通知からは呼ばない。usage API のレート制限をワーカーと分け合わないため)。
25
44
  * 取得に失敗したときだけ null を返す(どの枠も返らない正常応答は空文字)。
26
45
  */
27
- export declare function buildTokenLimitText(fresh?: boolean): Promise<string | null>;
46
+ export declare function buildTokenLimitText(): Promise<string | null>;
28
47
  /**
29
48
  * 通知本文へ載せる output を末尾1000文字に切り詰める。
30
49
  * 案内文(CLOUD_FAILURE_GUIDANCE)は appendCloudFailureGuidance が output の先頭に
@@ -45,7 +64,6 @@ export declare function buildTaskNotificationText(params: {
45
64
  id: number;
46
65
  title: string;
47
66
  url: string;
48
- tokenText: string;
49
67
  output?: string;
50
68
  cloud?: {
51
69
  sessionId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.142.1",
3
+ "version": "0.142.3",
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",