charness 3.4.0 → 3.5.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/CHANGELOG.md +27 -0
- package/README.md +73 -5
- package/dist/adapters/claudeCliAdapter.js +2 -2
- package/dist/adapters/claudeCliAdapter.js.map +1 -1
- package/dist/adapters/claudeConfigDir.d.ts +18 -0
- package/dist/adapters/claudeConfigDir.d.ts.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.d.ts +8 -1
- package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.js +24 -6
- package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
- package/dist/adapters/createAdapter.d.ts +2 -1
- package/dist/adapters/createAdapter.d.ts.map +1 -1
- package/dist/adapters/createAdapter.js +16 -0
- package/dist/adapters/createAdapter.js.map +1 -1
- package/dist/agentLogin/agentLogin.d.ts +18 -1
- package/dist/agentLogin/agentLogin.d.ts.map +1 -1
- package/dist/agentLogin/agentLogin.js +59 -18
- package/dist/agentLogin/agentLogin.js.map +1 -1
- package/dist/apiErrorRetry.d.ts +3 -1
- package/dist/apiErrorRetry.d.ts.map +1 -1
- package/dist/apiErrorRetry.js +11 -1
- package/dist/apiErrorRetry.js.map +1 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +176 -23
- package/dist/bot.js.map +1 -1
- package/dist/cli/claudeProfiles.d.ts +1 -1
- package/dist/cli/claudeProfiles.d.ts.map +1 -1
- package/dist/cli/claudeProfiles.js +11 -6
- package/dist/cli/claudeProfiles.js.map +1 -1
- package/dist/connectors/telegram/commands/claudeAccounts.d.ts +6 -2
- package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/claudeAccounts.js +127 -28
- package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -1
- package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/fallbackChain.js +6 -2
- package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/de.js +28 -1
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +28 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +28 -1
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +28 -1
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/hi.d.ts.map +1 -1
- package/dist/i18n/hi.js +28 -1
- package/dist/i18n/hi.js.map +1 -1
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ja.js +28 -1
- package/dist/i18n/ja.js.map +1 -1
- package/dist/i18n/ka.d.ts.map +1 -1
- package/dist/i18n/ka.js +28 -1
- package/dist/i18n/ka.js.map +1 -1
- package/dist/i18n/pt.d.ts.map +1 -1
- package/dist/i18n/pt.js +28 -1
- package/dist/i18n/pt.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +28 -1
- package/dist/i18n/ru.js.map +1 -1
- package/dist/i18n/uk.d.ts.map +1 -1
- package/dist/i18n/uk.js +28 -1
- package/dist/i18n/uk.js.map +1 -1
- package/dist/i18n/uz.d.ts.map +1 -1
- package/dist/i18n/uz.js +28 -1
- package/dist/i18n/uz.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +28 -1
- package/dist/i18n/zh.js.map +1 -1
- package/dist/scheduler/mcpSurface.d.ts.map +1 -1
- package/dist/scheduler/mcpSurface.js +4 -2
- package/dist/scheduler/mcpSurface.js.map +1 -1
- package/dist/state.d.ts +31 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +36 -0
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/anthropicApiKeyCheck.d.ts +19 -0
- package/dist/utils/anthropicApiKeyCheck.d.ts.map +1 -0
- package/dist/utils/anthropicApiKeyCheck.js +41 -0
- package/dist/utils/anthropicApiKeyCheck.js.map +1 -0
- package/dist/utils/claudeAccountPickerPlan.d.ts +6 -1
- package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -1
- package/dist/utils/claudeAccountPickerPlan.js +5 -1
- package/dist/utils/claudeAccountPickerPlan.js.map +1 -1
- package/dist/utils/claudeApiKeyHelper.d.ts +19 -0
- package/dist/utils/claudeApiKeyHelper.d.ts.map +1 -0
- package/dist/utils/claudeApiKeyHelper.js +32 -0
- package/dist/utils/claudeApiKeyHelper.js.map +1 -0
- package/dist/utils/claudeApiSpend.d.ts +73 -0
- package/dist/utils/claudeApiSpend.d.ts.map +1 -0
- package/dist/utils/claudeApiSpend.js +82 -0
- package/dist/utils/claudeApiSpend.js.map +1 -0
- package/dist/utils/claudeCompactHook.d.ts +11 -6
- package/dist/utils/claudeCompactHook.d.ts.map +1 -1
- package/dist/utils/claudeCompactHook.js +28 -11
- package/dist/utils/claudeCompactHook.js.map +1 -1
- package/dist/utils/claudeLimitSwitch.d.ts +2 -1
- package/dist/utils/claudeLimitSwitch.d.ts.map +1 -1
- package/dist/utils/claudeLimitSwitch.js +3 -2
- package/dist/utils/claudeLimitSwitch.js.map +1 -1
- package/dist/utils/claudeProfileUsageLines.d.ts +18 -3
- package/dist/utils/claudeProfileUsageLines.d.ts.map +1 -1
- package/dist/utils/claudeProfileUsageLines.js +23 -2
- package/dist/utils/claudeProfileUsageLines.js.map +1 -1
- package/dist/utils/claudeProfiles.d.ts +57 -2
- package/dist/utils/claudeProfiles.d.ts.map +1 -1
- package/dist/utils/claudeProfiles.js +96 -3
- package/dist/utils/claudeProfiles.js.map +1 -1
- package/dist/utils/claudeStreamJson.d.ts +4 -1
- package/dist/utils/claudeStreamJson.d.ts.map +1 -1
- package/dist/utils/claudeStreamJson.js +6 -0
- package/dist/utils/claudeStreamJson.js.map +1 -1
- package/dist/utils/claudeTranscriptContent.d.ts +8 -0
- package/dist/utils/claudeTranscriptContent.d.ts.map +1 -1
- package/dist/utils/claudeTranscriptContent.js +44 -0
- package/dist/utils/claudeTranscriptContent.js.map +1 -1
- package/dist/utils/claudeUsage.d.ts +22 -6
- package/dist/utils/claudeUsage.d.ts.map +1 -1
- package/dist/utils/claudeUsage.js +12 -5
- package/dist/utils/claudeUsage.js.map +1 -1
- package/dist/utils/claudeUsageRefresher.d.ts +6 -0
- package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
- package/dist/utils/claudeUsageRefresher.js +2 -2
- package/dist/utils/claudeUsageRefresher.js.map +1 -1
- package/dist/utils/fallbackChain.d.ts +15 -8
- package/dist/utils/fallbackChain.d.ts.map +1 -1
- package/dist/utils/fallbackChain.js +30 -13
- package/dist/utils/fallbackChain.js.map +1 -1
- package/dist/utils/limitsBoardPlan.d.ts +4 -2
- package/dist/utils/limitsBoardPlan.d.ts.map +1 -1
- package/dist/utils/limitsBoardPlan.js +12 -5
- package/dist/utils/limitsBoardPlan.js.map +1 -1
- package/dist/utils/recvPreviewRedaction.d.ts +7 -2
- package/dist/utils/recvPreviewRedaction.d.ts.map +1 -1
- package/dist/utils/recvPreviewRedaction.js +10 -4
- package/dist/utils/recvPreviewRedaction.js.map +1 -1
- package/dist/utils/threadStatusReport.d.ts +12 -1
- package/dist/utils/threadStatusReport.d.ts.map +1 -1
- package/dist/utils/threadStatusReport.js +13 -4
- package/dist/utils/threadStatusReport.js.map +1 -1
- package/package.json +1 -1
package/dist/bot.js
CHANGED
|
@@ -215,6 +215,8 @@ const claudeUsageRefresher_1 = require("./utils/claudeUsageRefresher");
|
|
|
215
215
|
const openCodeUsage_1 = require("./utils/openCodeUsage");
|
|
216
216
|
const usageRefreshBudget_1 = require("./utils/usageRefreshBudget");
|
|
217
217
|
const claudeLimitSwitch_1 = require("./utils/claudeLimitSwitch");
|
|
218
|
+
const anthropicApiKeyCheck_1 = require("./utils/anthropicApiKeyCheck");
|
|
219
|
+
const claudeApiKeyHelper_1 = require("./utils/claudeApiKeyHelper");
|
|
218
220
|
const fallbackSwitchNotice_1 = require("./utils/fallbackSwitchNotice");
|
|
219
221
|
const adapterSwitchSessionIds_1 = require("./utils/adapterSwitchSessionIds");
|
|
220
222
|
const fallbackChain_1 = require("./utils/fallbackChain");
|
|
@@ -226,6 +228,8 @@ const operatorSwitchPlan_1 = require("./utils/operatorSwitchPlan");
|
|
|
226
228
|
const backendSwitchPlan_1 = require("./utils/backendSwitchPlan");
|
|
227
229
|
const selfUpdate_1 = require("./connectors/telegram/commands/selfUpdate");
|
|
228
230
|
const stt_1 = require("./connectors/telegram/commands/stt");
|
|
231
|
+
const claudeApiSpend_1 = require("./utils/claudeApiSpend");
|
|
232
|
+
const claudeProfileUsageLines_1 = require("./utils/claudeProfileUsageLines");
|
|
229
233
|
const resolveBinary_1 = require("./utils/resolveBinary");
|
|
230
234
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
231
235
|
// ENV parsing & fatal validation
|
|
@@ -877,6 +881,12 @@ const limitResumeNoticePinnedMessageId = new Map();
|
|
|
877
881
|
* Cleared by {@link cancelApiRetry} (the user took over / the session went away).
|
|
878
882
|
*/
|
|
879
883
|
const autoContinueOffNoticedThreads = new Set();
|
|
884
|
+
/**
|
|
885
|
+
* @description Threads that already got the «API credits used up» notice for the CURRENT episode
|
|
886
|
+
* ({@link handleBalanceExhausted}): that flow arms no retry either, so — like the OFF notice above — a repeated frame
|
|
887
|
+
* of the same error needs its own guard. Cleared by {@link cancelApiRetry} (the user took over / the session went away).
|
|
888
|
+
*/
|
|
889
|
+
const balanceExhaustedNoticedThreads = new Set();
|
|
880
890
|
/**
|
|
881
891
|
* The OPEN «switch» offer per thread (`utils/fallbackChain.ts`): the limit episode the buttons on the notice
|
|
882
892
|
* were posted for, the rung the topic was on and the candidate list the buttons index into. A tap is honoured
|
|
@@ -1142,6 +1152,11 @@ const wedgeRecoveriesInFlight = new Set();
|
|
|
1142
1152
|
*/
|
|
1143
1153
|
function handleApiError(key, cls, options = {}) {
|
|
1144
1154
|
const k = (0, sessionKey_1.keyToString)(key);
|
|
1155
|
+
// A used-up API credit balance: no wait refills it — the account is marked, and the chain or the operator takes over.
|
|
1156
|
+
if (cls.kind === 'usageLimit' && cls.isBalanceExhausted === true) {
|
|
1157
|
+
void handleBalanceExhausted(key, cls);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1145
1160
|
// An unconfirmed usage limit is probed first; it reaches the wait below only once confirmed.
|
|
1146
1161
|
if (cls.kind === 'usageLimit' && cls.isUnconfirmed === true && handleUnconfirmedLimit(key))
|
|
1147
1162
|
return;
|
|
@@ -1236,7 +1251,7 @@ function armApiRetryTimer(key, arming) {
|
|
|
1236
1251
|
// bottom (debounced; no-op when no question is pending).
|
|
1237
1252
|
onThreadActivityWhileQuestionPending(key);
|
|
1238
1253
|
void state
|
|
1239
|
-
.setApiRetry(key, { kind: arming.kind, attempt: arming.attempt, fireAt: arming.fireAt, ...(arming.isLimitProbe ? { isLimitProbe: true } : {}) })
|
|
1254
|
+
.setApiRetry(key, { kind: arming.kind, attempt: arming.attempt, fireAt: arming.fireAt, ...(arming.isLimitProbe ? { isLimitProbe: true } : {}), ...(arming.resumeOn ? { resumeOn: arming.resumeOn } : {}) })
|
|
1240
1255
|
.catch(e => console.error('[apiRetry] persist failed:', e));
|
|
1241
1256
|
clearSavedFiredLimitProbe(key);
|
|
1242
1257
|
const timer = setTimeout(() => {
|
|
@@ -1252,6 +1267,7 @@ function armApiRetryTimer(key, arming) {
|
|
|
1252
1267
|
fireAt: arming.fireAt,
|
|
1253
1268
|
limitWait: arming.limitWait,
|
|
1254
1269
|
isLimitProbe: arming.isLimitProbe,
|
|
1270
|
+
resumeOn: arming.resumeOn,
|
|
1255
1271
|
});
|
|
1256
1272
|
}
|
|
1257
1273
|
/**
|
|
@@ -1443,15 +1459,90 @@ async function announceUsageLimitOrSwitch(key, cls, episodeId, initialWait, atte
|
|
|
1443
1459
|
}
|
|
1444
1460
|
}
|
|
1445
1461
|
const texts = getLimitWaitTexts(key, wait, attempt, resumeOn);
|
|
1446
|
-
const keyboard = plan.action === 'offer'
|
|
1447
|
-
? telegraf_1.Markup.inlineKeyboard(plan.candidates.map((entry, index) => [
|
|
1448
|
-
telegraf_1.Markup.button.callback((0, i18n_1.t)('fallback.switchButton', { label: formatFallbackEntry(entry) }), (0, fallbackChain_1.buildFallbackSwitchCallbackData)(episodeId, index)),
|
|
1449
|
-
]))
|
|
1450
|
-
: undefined;
|
|
1462
|
+
const keyboard = plan.action === 'offer' ? buildFallbackSwitchKeyboard(episodeId, plan.candidates) : undefined;
|
|
1451
1463
|
const { isPlainNoticePosted } = await announceLimitWait(key, wait, texts, isPlainNoticeDue, keyboard);
|
|
1452
1464
|
if (keyboard && !isPlainNoticePosted)
|
|
1453
1465
|
await replyToThread(key, (0, i18n_1.t)('fallback.switchOffer'), keyboard);
|
|
1454
1466
|
}
|
|
1467
|
+
/** The «🔁 Switch to …» buttons of a limit episode's notice, one per candidate (best first; the index is into that episode's list). */
|
|
1468
|
+
function buildFallbackSwitchKeyboard(episodeId, candidates) {
|
|
1469
|
+
return telegraf_1.Markup.inlineKeyboard(candidates.map((entry, index) => [
|
|
1470
|
+
telegraf_1.Markup.button.callback((0, i18n_1.t)('fallback.switchButton', { label: formatFallbackEntry(entry) }), (0, fallbackChain_1.buildFallbackSwitchCallbackData)(episodeId, index)),
|
|
1471
|
+
]));
|
|
1472
|
+
}
|
|
1473
|
+
/** Whose API credits a balance notice names: a Claude account `«console»`, an OpenCode `provider/model`. */
|
|
1474
|
+
function formatBalanceOwner(from) {
|
|
1475
|
+
if (from === null)
|
|
1476
|
+
return (0, i18n_1.t)('fallback.backendClaude');
|
|
1477
|
+
return from.backend === 'claude' ? `«${from.profile}»` : (0, fallbackChain_1.buildOpenCodeModelLabel)(from);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* @description A used-up API credit balance on `key` («Credit balance is too low», `isBalanceExhausted`): nothing resets
|
|
1481
|
+
* it, so no wait for a reset is armed and no hourly retry runs. The Claude account is marked out of credits
|
|
1482
|
+
* (`creditExhaustedAt`, until the operator picks it or signs it in again, or a billed turn on it goes through), the parked
|
|
1483
|
+
* episode is settled (a restart does not run this again), and the fallback chain decides as for a limit, with that
|
|
1484
|
+
* account out: switching ON moves the topic at once; OFF puts «switch to …» buttons on the notice. With no rung free
|
|
1485
|
+
* now, the topic waits only for a rung that comes back at a KNOWN time (the earliest reset, and only with «wait for
|
|
1486
|
+
* reset» on) and resumes there; otherwise the notice is all, and an open request is answered and no longer woken. A
|
|
1487
|
+
* repeat of the same episode (another frame, another scrape of the line, a scheduled run into the same balance) says
|
|
1488
|
+
* nothing in the topic again; a request opened since — a scheduled run's — is still answered and no longer woken, unless
|
|
1489
|
+
* a wait for another rung's reset is armed.
|
|
1490
|
+
*/
|
|
1491
|
+
async function handleBalanceExhausted(key, cls) {
|
|
1492
|
+
const k = (0, sessionKey_1.keyToString)(key);
|
|
1493
|
+
markLimitEpisodeSettled(key);
|
|
1494
|
+
const position = getTopicFallbackPosition(key);
|
|
1495
|
+
if (position?.backend === 'claude')
|
|
1496
|
+
state.setClaudeProfileCreditExhausted(position.profile, new Date().toISOString());
|
|
1497
|
+
const notice = `${(0, i18n_1.t)('apiRetry.balanceExhaustedNotice', { name: formatBalanceOwner(position) })}${getBalanceSpentLine(position)}`;
|
|
1498
|
+
const texts = { plainNotice: notice, answerBody: (0, i18n_1.t)('requests.limit.answerBalanceExhaustedNotice') };
|
|
1499
|
+
const stopped = { kind: 'autoResumeOff' };
|
|
1500
|
+
if (balanceExhaustedNoticedThreads.has(k)) {
|
|
1501
|
+
// Like the «wait for reset» OFF repeat: without it the wake-up engine re-prods that request into the same refused
|
|
1502
|
+
// balance and ends in an alert.
|
|
1503
|
+
if (getArmedApiRetry(key) === null)
|
|
1504
|
+
await announceLimitWait(key, stopped, texts, false);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
balanceExhaustedNoticedThreads.add(k);
|
|
1508
|
+
const episodeId = Date.now();
|
|
1509
|
+
let plan = { action: 'none', from: position, earliest: null };
|
|
1510
|
+
try {
|
|
1511
|
+
plan = await planFallbackSwitch(key, cls, episodeId);
|
|
1512
|
+
}
|
|
1513
|
+
catch (e) {
|
|
1514
|
+
console.warn(`[fallback] ${k}: the switch plan after a used-up balance failed:`, e instanceof Error ? e.message : e);
|
|
1515
|
+
}
|
|
1516
|
+
if (plan.action === 'auto') {
|
|
1517
|
+
const to = plan.to;
|
|
1518
|
+
await replyToThread(key, notice);
|
|
1519
|
+
const outcome = await switchTopicToFallbackEntry(key, plan.from, to, { isAnnounced: true }).catch((e) => {
|
|
1520
|
+
console.warn(`[fallback] ${k}: the auto-switch to ${formatFallbackEntry(to)} failed:`, e instanceof Error ? e.message : e);
|
|
1521
|
+
return { isSwitched: false };
|
|
1522
|
+
});
|
|
1523
|
+
// The notice is already in the topic; an open request still hears that the work stopped.
|
|
1524
|
+
if (!outcome.isSwitched)
|
|
1525
|
+
await announceLimitWait(key, stopped, texts, false);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (plan.action === 'offer') {
|
|
1529
|
+
const keyboard = buildFallbackSwitchKeyboard(episodeId, plan.candidates);
|
|
1530
|
+
const { isPlainNoticePosted } = await announceLimitWait(key, stopped, texts, true, keyboard);
|
|
1531
|
+
if (!isPlainNoticePosted)
|
|
1532
|
+
await replyToThread(key, (0, i18n_1.t)('fallback.switchOffer'), keyboard);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (plan.earliest && state.checkIsAutoContinueOnLimitEnabled(key)) {
|
|
1536
|
+
const fireAt = plan.earliest.untilMs + apiErrorRetry_1.resetBufferMs;
|
|
1537
|
+
const wait = { kind: 'afterReset', resetAt: plan.earliest.untilMs, fireAt };
|
|
1538
|
+
clearLimitResumeNotice(key);
|
|
1539
|
+
armApiRetryTimer(key, { kind: 'usageLimit', attempt: 1, fireAt, delayMs: Math.max(fireAt - Date.now(), 0), limitWait: wait, resumeOn: plan.earliest.entry });
|
|
1540
|
+
const waitTexts = getLimitWaitTexts(key, wait, 1, plan.earliest.entry);
|
|
1541
|
+
await announceLimitWait(key, wait, { plainNotice: `${notice}\n\n${waitTexts.plainNotice}`, answerBody: waitTexts.answerBody }, true);
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
await announceLimitWait(key, stopped, texts, true);
|
|
1545
|
+
}
|
|
1455
1546
|
/**
|
|
1456
1547
|
* @description Re-aim the ARMED usage-limit wait of `key` at the rung that comes back first: the timer moves to
|
|
1457
1548
|
* that rung's reset (+ the usual buffer), and the persisted retry record carries the rung (`resumeOn`), so the
|
|
@@ -1550,6 +1641,7 @@ function cancelApiRetry(key) {
|
|
|
1550
1641
|
// error — so neither can re-notify per message.
|
|
1551
1642
|
clearLimitResumeNotice(key);
|
|
1552
1643
|
autoContinueOffNoticedThreads.delete(k);
|
|
1644
|
+
balanceExhaustedNoticedThreads.delete(k);
|
|
1553
1645
|
fallbackSwitchOffers.delete(k);
|
|
1554
1646
|
// NOTE: intentionally does NOT touch the logged-out notice. `cancelApiRetry`
|
|
1555
1647
|
// fires on EVERY inbound message (user takeover); clearing the auth notice here
|
|
@@ -1741,6 +1833,8 @@ function clearLimitResumeNotice(key) {
|
|
|
1741
1833
|
* sign-in driver built here), so the driver reaches it through this late-bound slot.
|
|
1742
1834
|
*/
|
|
1743
1835
|
let claudeProfileAuthStatusSink = null;
|
|
1836
|
+
/** Where an API key pasted into an API account's sign-in goes: the `/account` module too, through the same late-bound idiom. */
|
|
1837
|
+
let claudeProfileApiKeySink = null;
|
|
1744
1838
|
// Out-of-band sign-in drivers (`/login`, `/connect` OAuth): `agentLogin/README.md`.
|
|
1745
1839
|
const { cancelPendingLogin, checkIsAuthLoginAwaitingCode, cancelClaudeAuthLogin, startClaudeAuthLogin, submitClaudeAuthLoginCode, checkIsOpenCodeOAuthAwaitingReply, cancelOpenCodeOAuthLogin, startOpenCodeOAuthLogin, submitOpenCodeOAuthReply, checkIsAnyLoginInProgress, } = (0, agentLogin_1.createAgentLogin)({
|
|
1746
1840
|
replyToThread,
|
|
@@ -1748,6 +1842,7 @@ const { cancelPendingLogin, checkIsAuthLoginAwaitingCode, cancelClaudeAuthLogin,
|
|
|
1748
1842
|
clearAuthNotice,
|
|
1749
1843
|
execFileAsync,
|
|
1750
1844
|
onClaudeProfileAuthStatus: (key, profile, status) => claudeProfileAuthStatusSink?.(key, profile, status) ?? Promise.resolve(),
|
|
1845
|
+
onClaudeProfileApiKey: (key, profile, apiKey) => claudeProfileApiKeySink?.(key, profile, apiKey) ?? Promise.resolve((0, i18n_1.t)('account.apiKeyNoDirectory', { name: profile.name })),
|
|
1751
1846
|
});
|
|
1752
1847
|
/**
|
|
1753
1848
|
* @description Buffers prompts typed while an agent session is still booting so
|
|
@@ -5773,14 +5868,19 @@ function resolveThreadClaudeConfigDir(key) {
|
|
|
5773
5868
|
}
|
|
5774
5869
|
return dir;
|
|
5775
5870
|
}
|
|
5776
|
-
/**
|
|
5871
|
+
/**
|
|
5872
|
+
* `claude auth status --json` under `configDir` (`null` = `~/.claude`) as its sessions see it: never through an
|
|
5873
|
+
* `ANTHROPIC_API_KEY` of the bot's own, with the `apiKeyHelper` of a profile that holds a pasted API key; `null` when
|
|
5874
|
+
* the probe fails.
|
|
5875
|
+
*/
|
|
5777
5876
|
async function readClaudeAuthStatusUnder(configDir) {
|
|
5778
5877
|
const env = { ...process.env };
|
|
5779
5878
|
delete env.ANTHROPIC_API_KEY;
|
|
5780
5879
|
if (configDir !== null)
|
|
5781
5880
|
env[claudeProfiles_1.claudeConfigDirVariableName] = configDir;
|
|
5782
5881
|
try {
|
|
5783
|
-
const
|
|
5882
|
+
const args = [...(0, claudeApiKeyHelper_1.buildClaudeAuthProbeSettingsArgs)(configDir), 'auth', 'status', '--json'];
|
|
5883
|
+
const { stdout } = await execFileAsync((0, resolveBinary_1.resolveClaudeBinary)(), args, { timeout: claudeAuthStatusProbeTimeoutMs, env });
|
|
5784
5884
|
return (0, claudeProfiles_1.parseClaudeAuthStatus)(stdout);
|
|
5785
5885
|
}
|
|
5786
5886
|
catch (e) {
|
|
@@ -5843,9 +5943,12 @@ const openCodeUsagePoller = (0, openCodeUsage_1.createOpenCodeUsagePoller)({
|
|
|
5843
5943
|
*/
|
|
5844
5944
|
const claudeUsageRefresher = (0, claudeUsageRefresher_1.createClaudeUsageRefresher)({
|
|
5845
5945
|
listProfiles: () => [
|
|
5846
|
-
{
|
|
5946
|
+
{
|
|
5947
|
+
name: claudeProfiles_1.defaultClaudeProfileName, configDir: null,
|
|
5948
|
+
loggedIn: state.getClaudeProfileInfo(claudeProfiles_1.defaultClaudeProfileName)?.loggedIn, billing: state.getClaudeProfileInfo(claudeProfiles_1.defaultClaudeProfileName)?.billing,
|
|
5949
|
+
},
|
|
5847
5950
|
...Object.entries(state.getClaudeProfileRecords()).map(([name, record]) => ({
|
|
5848
|
-
name, configDir: (0, claudeProfiles_1.resolveClaudeProfileDir)((0, state_1.resolveDataDir)(), name), loggedIn: record.loggedIn,
|
|
5951
|
+
name, configDir: (0, claudeProfiles_1.resolveClaudeProfileDir)((0, state_1.resolveDataDir)(), name), loggedIn: record.loggedIn, billing: record.billing,
|
|
5849
5952
|
})),
|
|
5850
5953
|
],
|
|
5851
5954
|
getSnapshot: (name) => state.getClaudeProfileInfo(name)?.usage,
|
|
@@ -5881,6 +5984,27 @@ function onClaudeRateLimitEvent(configDir, event) {
|
|
|
5881
5984
|
state.setClaudeProfileLimitedUntil(name, null);
|
|
5882
5985
|
}
|
|
5883
5986
|
}
|
|
5987
|
+
/**
|
|
5988
|
+
* A json-stream turn's cost (`utils/claudeApiSpend.ts`): the topic's conversation baseline moves on for EVERY account
|
|
5989
|
+
* (a later API turn of the same conversation must count only itself), and the turn's spend adds to the account's only
|
|
5990
|
+
* when it is billed to API credits. A billed turn that went through ends a used-up-credits mark on whatever account it
|
|
5991
|
+
* ran on: the balance is back.
|
|
5992
|
+
*/
|
|
5993
|
+
function onClaudeTurnCost(key, configDir, report) {
|
|
5994
|
+
const turnCostUsd = (0, claudeApiSpend_1.computeClaudeTurnCostUsd)({ previous: state.getClaudeCostBaseline(key), ...report });
|
|
5995
|
+
state.setClaudeCostBaseline(key, { sessionId: report.sessionId, totalCostUsd: report.totalCostUsd });
|
|
5996
|
+
const name = resolveClaudeProfileNameForDir(configDir);
|
|
5997
|
+
if (name === null)
|
|
5998
|
+
return;
|
|
5999
|
+
const info = state.getClaudeProfileInfo(name);
|
|
6000
|
+
const outcome = (0, claudeApiSpend_1.getClaudeTurnCostOutcome)({
|
|
6001
|
+
billing: info?.billing ?? null, isCreditExhausted: info?.creditExhaustedAt !== undefined, isError: report.isError, turnCostUsd,
|
|
6002
|
+
});
|
|
6003
|
+
if (outcome.isCreditMarkCleared)
|
|
6004
|
+
state.setClaudeProfileCreditExhausted(name, null);
|
|
6005
|
+
if (outcome.spendUsd !== null)
|
|
6006
|
+
state.setClaudeProfileApiSpend(name, (0, claudeApiSpend_1.addClaudeApiSpend)(info?.apiSpend, outcome.spendUsd, new Date().toISOString()));
|
|
6007
|
+
}
|
|
5884
6008
|
/** The model a topic's Claude session runs (or last ran), for the usage ranking's choice of windows. */
|
|
5885
6009
|
function getThreadClaudeModel(key) {
|
|
5886
6010
|
return (0, createAdapter_1.getThreadAdapter)(key).getCurrentModel?.(key) ?? state.getAgent(key)?.model ?? null;
|
|
@@ -5888,7 +6012,10 @@ function getThreadClaudeModel(key) {
|
|
|
5888
6012
|
/** What `/status` names of a Claude profile: its last probed sign-in and email, and its limit mark (none until probed). */
|
|
5889
6013
|
function readStatusAccountSource(name) {
|
|
5890
6014
|
const info = state.getClaudeProfileInfo(name);
|
|
5891
|
-
return {
|
|
6015
|
+
return {
|
|
6016
|
+
name, email: info?.email ?? null, loggedIn: info?.loggedIn, limitedUntil: info?.limitedUntil ?? null,
|
|
6017
|
+
billing: info?.billing ?? null, creditExhaustedAt: info?.creditExhaustedAt ?? null, ...(info?.apiSpend ? { apiSpend: info.apiSpend } : {}),
|
|
6018
|
+
};
|
|
5892
6019
|
}
|
|
5893
6020
|
/** Whether the topic's agent is a Claude backend (a usage limit there is a limit of its account profile). */
|
|
5894
6021
|
function checkIsClaudeProfileBackend(key) {
|
|
@@ -5987,13 +6114,15 @@ async function planFallbackSwitch(key, cls, episodeId) {
|
|
|
5987
6114
|
const nowMs = Date.now();
|
|
5988
6115
|
const resetAtMs = cls.kind === 'usageLimit' ? cls.resetAt : undefined;
|
|
5989
6116
|
if (from.backend === 'claude') {
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
6117
|
+
// A used-up balance has no reset: its own flow marks the account (`creditExhaustedAt`) instead.
|
|
6118
|
+
if (cls.isBalanceExhausted !== true)
|
|
6119
|
+
state.setClaudeProfileLimitedUntil(from.profile, (0, claudeLimitSwitch_1.resolveClaudeLimitedUntil)({
|
|
6120
|
+
resetAtMs,
|
|
6121
|
+
usage: state.getClaudeProfileInfo(from.profile)?.usage,
|
|
6122
|
+
existingLimitedUntil: state.getClaudeProfileInfo(from.profile)?.limitedUntil ?? null,
|
|
6123
|
+
nowMs,
|
|
6124
|
+
fallbackMs: apiErrorRetry_1.usageLimitDefaultMs,
|
|
6125
|
+
}));
|
|
5997
6126
|
}
|
|
5998
6127
|
else {
|
|
5999
6128
|
const label = (0, fallbackChain_1.buildOpenCodeModelLabel)(from);
|
|
@@ -6023,8 +6152,11 @@ async function planFallbackSwitch(key, cls, episodeId) {
|
|
|
6023
6152
|
let earliest = null;
|
|
6024
6153
|
if (plan.action === 'none') {
|
|
6025
6154
|
const explicit = state.getFallbackChain();
|
|
6155
|
+
// Without a chain the implicit one is the signed-in SUBSCRIPTIONS: an account billed to API credits never ends a wait by itself.
|
|
6026
6156
|
const rungs = explicit.length > 0 ? explicit : from.backend === 'claude'
|
|
6027
|
-
? detection.claudeProfiles
|
|
6157
|
+
? detection.claudeProfiles
|
|
6158
|
+
.filter((profile) => profile.loggedIn !== false && profile.billing !== 'api')
|
|
6159
|
+
.map((profile) => ({ backend: 'claude', profile: profile.name, model: null }))
|
|
6028
6160
|
: [];
|
|
6029
6161
|
const found = (0, fallbackChain_1.pickEarliestResetRung)({ chain: rungs, current: from, detection, nowMs: Date.now() });
|
|
6030
6162
|
if (found && !found.isCurrent)
|
|
@@ -6045,9 +6177,23 @@ function getFallbackResetTime(from) {
|
|
|
6045
6177
|
return null;
|
|
6046
6178
|
return (0, localClock_1.formatLocalClockWithDateIfNotToday)(untilMs, Date.now());
|
|
6047
6179
|
}
|
|
6048
|
-
/**
|
|
6180
|
+
/**
|
|
6181
|
+
* The «switched» notice (`utils/fallbackSwitchNotice.ts`), with the reset of the exhausted rung when known — and, onto
|
|
6182
|
+
* an account billed to API credits, the line that it costs money per request and what it has spent so far.
|
|
6183
|
+
*/
|
|
6049
6184
|
function getFallbackSwitchedNotice(from, to, runningModel) {
|
|
6050
|
-
|
|
6185
|
+
const notice = (0, fallbackSwitchNotice_1.buildFallbackSwitchedNotice)({ from, to, resetTime: getFallbackResetTime(from), runningModel });
|
|
6186
|
+
const info = to.backend === 'claude' ? state.getClaudeProfileInfo(to.profile) : undefined;
|
|
6187
|
+
if (to.backend !== 'claude' || info?.billing !== 'api')
|
|
6188
|
+
return notice;
|
|
6189
|
+
return `${notice}\n${(0, i18n_1.t)('fallback.switchedToApi', { name: to.profile, spent: (0, claudeApiSpend_1.formatUsd)(info.apiSpend?.totalUsd ?? 0) })}`;
|
|
6190
|
+
}
|
|
6191
|
+
/** The spend line a «credits used up» notice ends with (what the account spent through the bot); `''` before any spend. */
|
|
6192
|
+
function getBalanceSpentLine(from) {
|
|
6193
|
+
const spend = from?.backend === 'claude' ? state.getClaudeProfileInfo(from.profile)?.apiSpend : undefined;
|
|
6194
|
+
if (!spend)
|
|
6195
|
+
return '';
|
|
6196
|
+
return `\n${(0, i18n_1.t)('apiRetry.balanceSpent', (0, claudeProfileUsageLines_1.formatClaudeApiSpendWords)(spend, getCurrentTimezone(), Date.now()))}`;
|
|
6051
6197
|
}
|
|
6052
6198
|
/**
|
|
6053
6199
|
* @description The model the topic's Claude session runs ACROSS a move onto `to`, for deciding whether the move
|
|
@@ -6314,6 +6460,10 @@ function buildBackendSwitchTargetOfRung(to) {
|
|
|
6314
6460
|
* The outcome tells the screen what to say; the question and the limit move's notice are already posted.
|
|
6315
6461
|
*/
|
|
6316
6462
|
async function switchTopicByOperator(key, to) {
|
|
6463
|
+
// The operator's own pick of an account whose API credits were used up is the act that ends that mark (topped up, or
|
|
6464
|
+
// trying anyway): a failing request marks it again.
|
|
6465
|
+
if (to.backend === 'claude')
|
|
6466
|
+
state.setClaudeProfileCreditExhausted(to.profile, null);
|
|
6317
6467
|
const currentFamily = getTopicBackendFamily(key);
|
|
6318
6468
|
const from = getTopicFallbackPosition(key);
|
|
6319
6469
|
const isUsageLimitWaitArmed = getArmedApiRetry(key)?.kind === 'usageLimit';
|
|
@@ -6418,7 +6568,7 @@ const { registerLimitsBoardCommands, registerLimitsBoardCallbacks } = (0, limits
|
|
|
6418
6568
|
runAutoSwitchCommand,
|
|
6419
6569
|
getArmedLimitRetryFireAt,
|
|
6420
6570
|
});
|
|
6421
|
-
const { registerClaudeAccountCommands, registerClaudeAccountCallbacks, checkIsAwaitingProfileName, cancelProfileNameCapture, submitProfileName, pickClaudeAccount, onClaudeProfileAuthStatus: recordClaudeProfileAuthStatus, } = (0, claudeAccounts_1.createClaudeAccounts)({
|
|
6571
|
+
const { registerClaudeAccountCommands, registerClaudeAccountCallbacks, checkIsAwaitingProfileName, cancelProfileNameCapture, submitProfileName, pickClaudeAccount, onClaudeProfileAuthStatus: recordClaudeProfileAuthStatus, onClaudeProfileApiKey: storeClaudeProfileApiKey, } = (0, claudeAccounts_1.createClaudeAccounts)({
|
|
6422
6572
|
...botCore,
|
|
6423
6573
|
getDataDir: state_1.resolveDataDir,
|
|
6424
6574
|
getInstanceTimezone: getCurrentTimezone,
|
|
@@ -6434,8 +6584,10 @@ const { registerClaudeAccountCommands, registerClaudeAccountCallbacks, checkIsAw
|
|
|
6434
6584
|
disconnectOpenCodeProvider: applyProviderDisconnect,
|
|
6435
6585
|
startOpenCodeConnect: showConnectMethodPicker,
|
|
6436
6586
|
applyAutoSwitch,
|
|
6587
|
+
verifyAnthropicApiKey: (apiKey) => (0, anthropicApiKeyCheck_1.verifyAnthropicApiKey)(apiKey),
|
|
6437
6588
|
});
|
|
6438
6589
|
claudeProfileAuthStatusSink = recordClaudeProfileAuthStatus;
|
|
6590
|
+
claudeProfileApiKeySink = storeClaudeProfileApiKey;
|
|
6439
6591
|
const { registerSttCommands, registerSttCallbacks, startSttAutoInstall, resumeSttInstallAtBoot, getAwaitedSttStep, checkIsAnySttInputAwaited, submitSttInput, cancelSttInput, } = (0, stt_1.createStt)({
|
|
6440
6592
|
...botCore,
|
|
6441
6593
|
getConfigPath: () => (0, sttSettings_1.getSttConfigPath)(),
|
|
@@ -7147,7 +7299,7 @@ bot.on((0, filters_1.message)('text'), async (ctx) => {
|
|
|
7147
7299
|
// `/account` «Add» asked for the new profile's name → this message is it (a bad name keeps the prompt
|
|
7148
7300
|
// armed and says so; a slash command above already disarmed it).
|
|
7149
7301
|
if (checkIsAwaitingProfileName(key) && !text.startsWith('/')) {
|
|
7150
|
-
await submitProfileName(key, text);
|
|
7302
|
+
await submitProfileName(key, text, ctx.message.message_id);
|
|
7151
7303
|
return;
|
|
7152
7304
|
}
|
|
7153
7305
|
// `/topic` asked for the new topic's name, or for the name of a folder to create → this message is the answer (a bad
|
|
@@ -11336,6 +11488,7 @@ async function startBot() {
|
|
|
11336
11488
|
// Every Claude spawn from here on reads the conversation's account profile (`/account`).
|
|
11337
11489
|
(0, createAdapter_1.registerClaudeConfigDirReader)(resolveThreadClaudeConfigDir);
|
|
11338
11490
|
(0, createAdapter_1.registerClaudeRateLimitSink)(onClaudeRateLimitEvent);
|
|
11491
|
+
(0, createAdapter_1.registerClaudeTurnCostSink)(onClaudeTurnCost);
|
|
11339
11492
|
// The usage cache: a first poll now (off the boot path), then the timer.
|
|
11340
11493
|
claudeUsageRefresher.start();
|
|
11341
11494
|
void claudeUsageRefresher.refreshAll();
|