claude-task-worker 0.142.0 → 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 +2 -2
- package/dist/{chunk-725IRSKF.js → chunk-EYML7T7A.js} +79 -42
- package/dist/index.js +2 -2
- package/dist/lib.js +1 -1
- package/dist/slack.d.ts +21 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ claude-task-worker <command> [--epic <issue-number>]... [--label <label>]... [--
|
|
|
140
140
|
| `install` / `update` | 上記「セットアップ」を参照 |
|
|
141
141
|
| `cloud-setup [--force]` | クラウド VM 側の準備(下記「`--cloud`」を参照) |
|
|
142
142
|
| `list-workers` | プリセットとカスタムの全ワーカーを名前・種別・有効/無効・ロード元で表示(`workerFiles` のロード失敗時は exit 1) |
|
|
143
|
-
| `usage` | Claude API 使用状況(5時間/7日間の利用率とリセット時刻)を表示し、Slack にも通知。以降は
|
|
143
|
+
| `usage` | Claude API 使用状況(5時間/7日間の利用率とリセット時刻)を表示し、Slack にも通知。以降は600秒(10分)ごとに取得し直して表示・RunCat 用スナップショットを更新し続ける(Ctrl-C で終了。Slack 通知は起動時のみ)。プランによって返らない窓(Ultra の5時間枠など)は省いて表示する |
|
|
144
144
|
| `version` | CLI のバージョンを表示(`--version` / `-v` も可) |
|
|
145
145
|
|
|
146
146
|
### `--epic <issue-number>`
|
|
@@ -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
|
-
|
|
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
|
|
2238
|
-
var
|
|
2239
|
-
var
|
|
2237
|
+
var USAGE_POLL_INTERVAL_SECONDS = 600;
|
|
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
|
|
2261
|
-
if (typeof credentials === "string") return credentials;
|
|
2262
|
-
return
|
|
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
|
|
2267
|
+
async function getStoredCredential() {
|
|
2265
2268
|
try {
|
|
2266
2269
|
const { stdout } = await execAsync('security find-generic-password -s "Claude Code-credentials" -w');
|
|
2267
|
-
return
|
|
2270
|
+
return extractCredential(JSON.parse(stdout.trim()));
|
|
2268
2271
|
} catch {
|
|
2269
2272
|
const raw = readFileSync2(join3(homedir3(), ".claude", ".credentials.json"), "utf-8");
|
|
2270
|
-
return
|
|
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
|
|
2285
|
+
function readBackoffUntil() {
|
|
2274
2286
|
try {
|
|
2275
|
-
|
|
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
|
|
2292
|
+
function writeBackoffUntil(until) {
|
|
2285
2293
|
try {
|
|
2286
|
-
|
|
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(
|
|
2291
|
-
const
|
|
2292
|
-
if (
|
|
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
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
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
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
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(
|
|
2343
|
-
const usage = await fetchUsageInfo(
|
|
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,
|
|
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}>${
|
|
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-
|
|
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(
|
|
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
package/dist/slack.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** `usage` コマンドが利用状況をポーリングする間隔 */
|
|
2
|
-
export declare const USAGE_POLL_INTERVAL_SECONDS =
|
|
2
|
+
export declare const USAGE_POLL_INTERVAL_SECONDS = 600;
|
|
3
3
|
export declare function send(payload: Record<string, unknown>): Promise<void>;
|
|
4
4
|
export interface UsageInfo {
|
|
5
5
|
fiveHourUtilization: number | null;
|
|
@@ -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
|
-
*
|
|
24
|
-
* (Slack webhook 未設定でも通知が no-op になるだけで、スナップショットの更新は行われる)。
|
|
41
|
+
* 呼ぶのは `usage` コマンドだけ(ワーカーの通知からは呼ばない。usage API のレート制限をワーカーと分け合わないため)。
|
|
25
42
|
* 取得に失敗したときだけ null を返す(どの枠も返らない正常応答は空文字)。
|
|
26
43
|
*/
|
|
27
|
-
export declare function buildTokenLimitText(
|
|
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;
|