claude-task-worker 0.142.1 → 0.142.2

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,9 @@ 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
2240
  async function send(payload) {
2241
2241
  if (!WEBHOOK_URL) return;
2242
2242
  try {
@@ -2257,59 +2257,100 @@ function parseUsageResponse(body) {
2257
2257
  sevenDayResetsAt: body.seven_day?.resets_at ?? null
2258
2258
  };
2259
2259
  }
2260
- function extractToken(credentials) {
2261
- if (typeof credentials === "string") return credentials;
2262
- return credentials.claudeAiOauth?.accessToken ?? credentials.oauth_token ?? credentials.access_token ?? "";
2260
+ function extractCredential(credentials) {
2261
+ if (typeof credentials === "string") return { token: credentials };
2262
+ return {
2263
+ token: credentials.claudeAiOauth?.accessToken ?? credentials.oauth_token ?? credentials.access_token ?? "",
2264
+ expiresAt: credentials.claudeAiOauth?.expiresAt
2265
+ };
2263
2266
  }
2264
- async function getOAuthToken() {
2267
+ async function getStoredCredential() {
2265
2268
  try {
2266
2269
  const { stdout } = await execAsync('security find-generic-password -s "Claude Code-credentials" -w');
2267
- return extractToken(JSON.parse(stdout.trim()));
2270
+ return extractCredential(JSON.parse(stdout.trim()));
2268
2271
  } catch {
2269
2272
  const raw = readFileSync2(join3(homedir3(), ".claude", ".credentials.json"), "utf-8");
2270
- return extractToken(JSON.parse(raw));
2273
+ return extractCredential(JSON.parse(raw));
2274
+ }
2275
+ }
2276
+ function rateLimitBackoffMs(retryAfter, now = Date.now()) {
2277
+ if (retryAfter) {
2278
+ const seconds = Number(retryAfter);
2279
+ if (Number.isFinite(seconds) && seconds > 0) return seconds * 1e3;
2280
+ const date = Date.parse(retryAfter);
2281
+ if (!Number.isNaN(date) && date > now) return date - now;
2271
2282
  }
2283
+ return USAGE_RATE_LIMIT_DEFAULT_BACKOFF_SECONDS * 1e3;
2272
2284
  }
2273
- function readUsageCache() {
2285
+ function readBackoffUntil() {
2274
2286
  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
- }
2287
+ return Number(JSON.parse(readFileSync2(USAGE_BACKOFF_PATH, "utf-8")).until) || 0;
2280
2288
  } catch {
2289
+ return 0;
2281
2290
  }
2282
- return null;
2283
2291
  }
2284
- function writeUsageCache(data) {
2292
+ function writeBackoffUntil(until) {
2285
2293
  try {
2286
- writeFileSync4(USAGE_CACHE_PATH, JSON.stringify({ timestamp: Date.now(), data }));
2294
+ const tmp = `${USAGE_BACKOFF_PATH}.${process.pid}.tmp`;
2295
+ writeFileSync4(tmp, JSON.stringify({ until: Math.max(until, readBackoffUntil()) }));
2296
+ renameSync3(tmp, USAGE_BACKOFF_PATH);
2287
2297
  } catch {
2288
2298
  }
2289
2299
  }
2290
- async function fetchUsageInfo(fresh) {
2291
- const cached = fresh ? null : readUsageCache();
2292
- if (cached) return cached;
2300
+ async function fetchUsageInfo() {
2301
+ const backoffUntil = readBackoffUntil();
2302
+ if (Date.now() < backoffUntil) {
2303
+ console.error(`[slack] Usage API rate limited; skipping until ${new Date(backoffUntil).toISOString()}`);
2304
+ return null;
2305
+ }
2293
2306
  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}`);
2307
+ const envToken = process.env.CLAUDE_CODE_OAUTH_TOKEN?.trim();
2308
+ if (envToken) {
2309
+ const res = await requestUsage(envToken);
2310
+ if (res.status !== 401 && res.status !== 403) return await handleUsageResponse(res);
2311
+ console.error(
2312
+ `[slack] Usage API rejected CLAUDE_CODE_OAUTH_TOKEN (${res.status}); falling back to stored credentials`
2313
+ );
2314
+ }
2315
+ const { token, expiresAt } = await getStoredCredential();
2316
+ if (!token) {
2317
+ console.error("[slack] No OAuth token found (set CLAUDE_CODE_OAUTH_TOKEN or sign in to Claude Code)");
2303
2318
  return null;
2304
2319
  }
2305
- const data = parseUsageResponse(await res.json());
2306
- writeUsageCache(data);
2307
- return data;
2320
+ if (expiresAt !== void 0 && expiresAt <= Date.now()) {
2321
+ console.error(
2322
+ `[slack] OAuth token expired at ${new Date(expiresAt).toISOString()} (it is refreshed when claude runs)`
2323
+ );
2324
+ return null;
2325
+ }
2326
+ return await handleUsageResponse(await requestUsage(token));
2308
2327
  } catch (err) {
2309
2328
  console.error(`[slack] Failed to fetch usage info: ${err}`);
2310
2329
  return null;
2311
2330
  }
2312
2331
  }
2332
+ function requestUsage(token) {
2333
+ return fetch("https://api.anthropic.com/api/oauth/usage", {
2334
+ headers: {
2335
+ Authorization: `Bearer ${token}`,
2336
+ "anthropic-beta": "oauth-2025-04-20"
2337
+ }
2338
+ });
2339
+ }
2340
+ async function handleUsageResponse(res) {
2341
+ if (!res.ok) {
2342
+ const body = (await res.text().catch(() => "")).slice(0, 500);
2343
+ if (res.status === 429) {
2344
+ const until = Date.now() + rateLimitBackoffMs(res.headers.get("retry-after"));
2345
+ writeBackoffUntil(until);
2346
+ console.error(`[slack] Usage API returned 429; backing off until ${new Date(until).toISOString()}: ${body}`);
2347
+ } else {
2348
+ console.error(`[slack] Usage API returned ${res.status}: ${body}`);
2349
+ }
2350
+ return null;
2351
+ }
2352
+ return parseUsageResponse(await res.json());
2353
+ }
2313
2354
  function utilizationEmoji(value) {
2314
2355
  if (value < 50) return "\u{1F7E2}";
2315
2356
  if (value < 80) return "\u{1F7E1}";
@@ -2339,8 +2380,8 @@ function formatTokenLimitText(usage) {
2339
2380
  const values = [usage.fiveHourUtilization, usage.sevenDayUtilization].filter((v) => v !== null);
2340
2381
  return ` | ${utilizationEmoji(Math.max(...values))} ${parts.join(" / ")}`;
2341
2382
  }
2342
- async function buildTokenLimitText(fresh = false) {
2343
- const usage = await fetchUsageInfo(fresh);
2383
+ async function buildTokenLimitText() {
2384
+ const usage = await fetchUsageInfo();
2344
2385
  if (!usage) return null;
2345
2386
  writeRuncatUsage(usage);
2346
2387
  return formatTokenLimitText(usage);
@@ -2354,13 +2395,13 @@ function truncateOutputForNotification(output) {
2354
2395
  return `\u2026${output.slice(-1e3)}`;
2355
2396
  }
2356
2397
  function buildTaskNotificationText(params) {
2357
- const { status, workerName, repoName, id, title, url, tokenText, output, cloud } = params;
2398
+ const { status, workerName, repoName, id, title, url, output, cloud } = params;
2358
2399
  const emoji = status === "completed" ? "\u2705" : "\u274C";
2359
2400
  const label = status === "completed" ? "completed" : "failed";
2360
2401
  const truncatedOutput = output ? truncateOutputForNotification(output) : output;
2361
2402
  const outputBlock = truncatedOutput ? `
2362
2403
  \`\`\`${truncatedOutput}\`\`\`` : "";
2363
- const body = `${emoji} [${workerName}] ${repoName} | Task ${label}: <${url}|#${id} ${title}>${tokenText}${outputBlock}`;
2404
+ const body = `${emoji} [${workerName}] ${repoName} | Task ${label}: <${url}|#${id} ${title}>${outputBlock}`;
2364
2405
  let sessionUrlPrefix = "";
2365
2406
  if (cloud) {
2366
2407
  sessionUrlPrefix = cloud.sessionId?.trim() ? `https://claude.ai/code/${cloud.sessionId.trim()}
@@ -2369,7 +2410,6 @@ function buildTaskNotificationText(params) {
2369
2410
  return `${sessionUrlPrefix}${body}`;
2370
2411
  }
2371
2412
  async function notifyTaskCompleted(workerName, repoName, id, title, url, output, cloud) {
2372
- const tokenText = await buildTokenLimitText() ?? "";
2373
2413
  await send({
2374
2414
  text: buildTaskNotificationText({
2375
2415
  status: "completed",
@@ -2378,14 +2418,12 @@ async function notifyTaskCompleted(workerName, repoName, id, title, url, output,
2378
2418
  id,
2379
2419
  title,
2380
2420
  url,
2381
- tokenText,
2382
2421
  output,
2383
2422
  cloud
2384
2423
  })
2385
2424
  });
2386
2425
  }
2387
2426
  async function notifyTaskFailed(workerName, repoName, id, title, url, output, cloud) {
2388
- const tokenText = await buildTokenLimitText() ?? "";
2389
2427
  await send({
2390
2428
  text: buildTaskNotificationText({
2391
2429
  status: "failed",
@@ -2394,7 +2432,6 @@ async function notifyTaskFailed(workerName, repoName, id, title, url, output, cl
2394
2432
  id,
2395
2433
  title,
2396
2434
  url,
2397
- tokenText,
2398
2435
  output,
2399
2436
  cloud
2400
2437
  })
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-EYML7T7A.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-EYML7T7A.js";
59
59
  import {
60
60
  getPermissionMode,
61
61
  getRunMode,
package/dist/slack.d.ts CHANGED
@@ -16,15 +16,32 @@ export declare function parseUsageResponse(body: {
16
16
  five_hour?: UsageWindow | null;
17
17
  seven_day?: UsageWindow | null;
18
18
  }): UsageInfo;
19
+ interface CredentialPayload {
20
+ claudeAiOauth?: {
21
+ accessToken?: string;
22
+ expiresAt?: number;
23
+ };
24
+ oauth_token?: string;
25
+ access_token?: string;
26
+ }
27
+ export interface OAuthCredential {
28
+ token: string;
29
+ expiresAt?: number;
30
+ }
31
+ export declare function extractCredential(credentials: CredentialPayload | string): OAuthCredential;
32
+ /**
33
+ * 429 後に API を叩かない期間(ミリ秒)。Retry-After(秒 or HTTP-date)を優先し、
34
+ * 無い・読めないときは既定値。失敗のたびに叩き直すと制限が明けないため
35
+ */
36
+ export declare function rateLimitBackoffMs(retryAfter: string | null, now?: number): number;
19
37
  /** 返ってきた窓だけを並べる。どちらも無ければ空文字 */
20
38
  export declare function formatTokenLimitText(usage: UsageInfo): string;
21
39
  /**
22
40
  * 現在の利用状況を取得し、RunCat Neo 用スナップショットを更新したうえで通知に添えるテキストを返す。
23
- * ワーカーの完了/失敗通知はすべてここを通るため、タスクが終わるたびに RunCat 側も最新化される
24
- * (Slack webhook 未設定でも通知が no-op になるだけで、スナップショットの更新は行われる)。
41
+ * 呼ぶのは `usage` コマンドだけ(ワーカーの通知からは呼ばない。usage API のレート制限をワーカーと分け合わないため)。
25
42
  * 取得に失敗したときだけ null を返す(どの枠も返らない正常応答は空文字)。
26
43
  */
27
- export declare function buildTokenLimitText(fresh?: boolean): Promise<string | null>;
44
+ export declare function buildTokenLimitText(): Promise<string | null>;
28
45
  /**
29
46
  * 通知本文へ載せる output を末尾1000文字に切り詰める。
30
47
  * 案内文(CLOUD_FAILURE_GUIDANCE)は appendCloudFailureGuidance が output の先頭に
@@ -45,7 +62,6 @@ export declare function buildTaskNotificationText(params: {
45
62
  id: number;
46
63
  title: string;
47
64
  url: string;
48
- tokenText: string;
49
65
  output?: string;
50
66
  cloud?: {
51
67
  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.2",
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",