@shumkov/orchestra 0.2.0 → 0.3.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.
- package/lib/claude-bin.js +41 -0
- package/package.json +1 -1
package/lib/claude-bin.js
CHANGED
|
@@ -238,10 +238,51 @@ function ensureVendoredClaudeBin(version, { logger = console } = {}) {
|
|
|
238
238
|
return { ok: true, path: vendored, vendored: true };
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Check Claude CLI OAuth health from the credentials file — FREE (no API call,
|
|
243
|
+
* no model spawn). The CLI stores its login in ~/.claude/.credentials.json under
|
|
244
|
+
* `claudeAiOauth`, with two expiries: the short-lived `expiresAt` (access token,
|
|
245
|
+
* which the CLI silently auto-refreshes) and `refreshTokenExpiresAt` (the HARD
|
|
246
|
+
* limit — once the refresh token itself ages out, auto-refresh 401s and every
|
|
247
|
+
* channels-backend turn wedges INVISIBLY: claude fires UserPromptSubmit then
|
|
248
|
+
* dies with "OAuth access token has expired", which never reaches polygram's
|
|
249
|
+
* classifier, so it degrades to a silent "⏱ went quiet"). We watch the
|
|
250
|
+
* refresh-token expiry so the daemon can refuse turns with a clear message
|
|
251
|
+
* instead of wedging.
|
|
252
|
+
*
|
|
253
|
+
* Pure + injectable (`home`/`now`) for tests.
|
|
254
|
+
*
|
|
255
|
+
* @param {{home?:string, now?:number, warnWithinMs?:number}} [opts]
|
|
256
|
+
* @returns {{state:'healthy'|'expiring'|'expired'|'unknown', reason?:string,
|
|
257
|
+
* refreshTokenExpiresAt:?number, msLeft:?number, daysLeft:?number}}
|
|
258
|
+
*/
|
|
259
|
+
function checkClaudeAuthHealth({ home = os.homedir(), now = Date.now(), warnWithinMs = 3 * 86_400_000 } = {}) {
|
|
260
|
+
const credPath = path.join(home, '.claude', '.credentials.json');
|
|
261
|
+
let creds;
|
|
262
|
+
try {
|
|
263
|
+
creds = JSON.parse(fs.readFileSync(credPath, 'utf8'));
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// Missing/unreadable → can't prove expiry. Report 'unknown' (caller logs it)
|
|
266
|
+
// rather than 'expired', so a transient read error never hard-refuses traffic.
|
|
267
|
+
return { state: 'unknown', reason: `credentials unreadable: ${err.code || err.message}`, refreshTokenExpiresAt: null, msLeft: null, daysLeft: null };
|
|
268
|
+
}
|
|
269
|
+
const exp = creds && creds.claudeAiOauth && creds.claudeAiOauth.refreshTokenExpiresAt;
|
|
270
|
+
if (typeof exp !== 'number') {
|
|
271
|
+
return { state: 'unknown', reason: 'no claudeAiOauth.refreshTokenExpiresAt', refreshTokenExpiresAt: null, msLeft: null, daysLeft: null };
|
|
272
|
+
}
|
|
273
|
+
const msLeft = exp - now;
|
|
274
|
+
const daysLeft = Math.round((msLeft / 86_400_000) * 10) / 10;
|
|
275
|
+
let state = 'healthy';
|
|
276
|
+
if (msLeft <= 0) state = 'expired';
|
|
277
|
+
else if (msLeft <= warnWithinMs) state = 'expiring';
|
|
278
|
+
return { state, refreshTokenExpiresAt: exp, msLeft, daysLeft };
|
|
279
|
+
}
|
|
280
|
+
|
|
241
281
|
module.exports = {
|
|
242
282
|
resolvePinnedClaudeBin,
|
|
243
283
|
verifyPinnedClaudeBin,
|
|
244
284
|
ensureVendoredClaudeBin,
|
|
245
285
|
vendorDir,
|
|
246
286
|
CLAUDE_CLI_PINNED_VERSION,
|
|
287
|
+
checkClaudeAuthHealth,
|
|
247
288
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shumkov/orchestra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Drive interactive Claude Code CLI sessions: spawn, inject messages via the MCP channels bridge, observe turns, recover. The transport-agnostic session engine extracted from polygram, shared by polygram (Telegram) and water (WhatsApp).",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|