ccdeck 3.43.0 → 3.43.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 +1 -1
- package/dist/web/assets/{BrowserWatchModal-BPws0psH.js → BrowserWatchModal-DZTMfoGp.js} +1 -1
- package/dist/web/assets/{RadioPanel-CDl7ZImm.js → RadioPanel-DFqW-MUV.js} +1 -1
- package/dist/web/assets/{TrafficRadar-D1hp6BgZ.js → TrafficRadar-CPzsjc2O.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-DPcdpBpI.js → UsageHistoryModal-CfVS-N_Z.js} +1 -1
- package/dist/web/assets/{app-9p127oSR.js → app-HpFVXK_g.js} +5 -5
- package/dist/web/assets/{index-Bkx27-wy.js → index-DqxnkNs2.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/release-notes.json +3 -1
- package/src/server/account-choice.mjs +9 -5
- package/src/server/cswap-auto-choice.mjs +20 -20
- package/src/server/cswap-auto-loop.mjs +28 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./app-
|
|
1
|
+
import"./app-HpFVXK_g.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})();
|
package/dist/web/index.html
CHANGED
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
document.documentElement.setAttribute("data-density", density === "tightest" || density === "tighter" || density === "compact" || density === "comfortable" ? density : "default");
|
|
78
78
|
})();
|
|
79
79
|
</script>
|
|
80
|
-
<script type="module" crossorigin src="/assets/index-
|
|
81
|
-
<link rel="modulepreload" crossorigin href="/assets/app-
|
|
80
|
+
<script type="module" crossorigin src="/assets/index-DqxnkNs2.js"></script>
|
|
81
|
+
<link rel="modulepreload" crossorigin href="/assets/app-HpFVXK_g.js">
|
|
82
82
|
<link rel="stylesheet" crossorigin href="/assets/app-DsHSufBt.css">
|
|
83
83
|
</head>
|
|
84
84
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccdeck",
|
|
3
|
-
"version": "3.43.
|
|
3
|
+
"version": "3.43.3",
|
|
4
4
|
"description": "Local dashboard for Claude Code and Codex CLI sessions: which Claude Code session needs input, every tool call and Claude Code subagent live, token cost and quota, several Claude accounts. Run npx ccdeck.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/release-notes.json
CHANGED
|
@@ -95,7 +95,7 @@ export function readingCurrent(account, models, now) {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Whether Auto-switch may land on this account: signed in, in rotation, not
|
|
98
|
-
*
|
|
98
|
+
* reserved by a local `cswap run` session, not one claude-swap has
|
|
99
99
|
* quarantined, read recently, and comfortably under the threshold on every
|
|
100
100
|
* window that counts. An account with no 5-hour or weekly window (an API key)
|
|
101
101
|
* is never a target here; the engine still decides about those.
|
|
@@ -140,11 +140,12 @@ function soonestWeekly(accounts, ctx, before = null) {
|
|
|
140
140
|
* somebody just set a home and expects to be taken there.
|
|
141
141
|
* threshold the engine's, in percent
|
|
142
142
|
* models countedModels(...) of the engine's model setting
|
|
143
|
-
* busy keys of
|
|
144
|
-
*
|
|
143
|
+
* busy keys of accounts reserved by local `cswap run` sessions;
|
|
144
|
+
* activity reported by other paired devices does not exclude them
|
|
145
145
|
* quarantined keys of the accounts claude-swap's engine has quarantined
|
|
146
146
|
* sinceMove ms since the last move, the engine's or this one's
|
|
147
|
-
* cooldownMs the engine's cooldown
|
|
147
|
+
* cooldownMs the engine's cooldown for proactive moves; fully exhausted
|
|
148
|
+
* windows can be escaped immediately
|
|
148
149
|
* optional whether moves below the threshold may be made at all: only
|
|
149
150
|
* while the engine itself is on `best`
|
|
150
151
|
*
|
|
@@ -154,7 +155,7 @@ export function chooseAccount({ accounts, choice, threshold, models = [], busy =
|
|
|
154
155
|
const next = choice?.next === "weekly" ? "weekly" : "room";
|
|
155
156
|
const homeKey = choice?.home ?? null;
|
|
156
157
|
if (next === "room" && !homeKey) return null;
|
|
157
|
-
if (
|
|
158
|
+
if (!Number.isFinite(threshold)) return null;
|
|
158
159
|
|
|
159
160
|
const active = accounts.find(a => a.active);
|
|
160
161
|
// The engine reads the account it is on every minute or so; if the store
|
|
@@ -162,6 +163,9 @@ export function chooseAccount({ accounts, choice, threshold, models = [], busy =
|
|
|
162
163
|
if (!active || !readingCurrent(active, models, now)) return null;
|
|
163
164
|
const used = usedOf(active, models);
|
|
164
165
|
if (used == null) return null;
|
|
166
|
+
// Like the engine, an exhausted window must be escapable immediately.
|
|
167
|
+
// Ordinary threshold moves and optional returns still keep the cooldown.
|
|
168
|
+
if (sinceMove < cooldownMs && used < 100) return null;
|
|
165
169
|
|
|
166
170
|
const ctx = { threshold, models, busy, quarantined, now };
|
|
167
171
|
const home = homeKey ? accounts.find(a => a.key === homeKey) ?? null : null;
|
|
@@ -30,7 +30,7 @@ import { backupRoot, fetchClaudeAccounts, slotNumber, switchClaudeAccount } from
|
|
|
30
30
|
import { readCswapConfig } from "./cswap-auto-readers.mjs";
|
|
31
31
|
import { accountKey } from "./lan-copies.mjs";
|
|
32
32
|
import { isProcessAlive } from "./deck-probe.mjs";
|
|
33
|
-
import { chooseAccount, countedModels, NEXT_CHOICES } from "./account-choice.mjs";
|
|
33
|
+
import { chooseAccount, countedModels, readingCurrent, NEXT_CHOICES } from "./account-choice.mjs";
|
|
34
34
|
|
|
35
35
|
/** Where the choice is kept: beside prefs.json, in the deck's own data. */
|
|
36
36
|
export function choicePath(home = deckDataDir()) {
|
|
@@ -146,19 +146,6 @@ async function engineState(accounts) {
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
/** The accounts the decks this one is paired with say they are working on.
|
|
150
|
-
* One login live on two machines races for every renewal (README, "One login
|
|
151
|
-
* live in one place"), so a tick never moves onto one of them. */
|
|
152
|
-
async function accountsOnOtherDecks() {
|
|
153
|
-
try {
|
|
154
|
-
const { lanEngine } = await import("./lan-deck.mjs");
|
|
155
|
-
const peers = lanEngine.status()?.peers ?? [];
|
|
156
|
-
return new Set(peers.map(p => p?.offers?.current?.key).filter(k => typeof k === "string"));
|
|
157
|
-
} catch {
|
|
158
|
-
return new Set();
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
149
|
/** The name claude-swap gives an account's `cswap run` profile after its
|
|
163
150
|
* slot number: the email, NFC, with anything but ASCII letters, digits and
|
|
164
151
|
* `._-` turned into `_` (claude_swap/session.py, slugify_email). */
|
|
@@ -212,7 +199,7 @@ const refOf = a => (a ? { number: a.num, email: a.email ?? null } : null);
|
|
|
212
199
|
* roster it decided on, which noteEngineTick needs to name where the engine
|
|
213
200
|
* went, and null when nothing is set.
|
|
214
201
|
*/
|
|
215
|
-
export async function chooseBeforeEngine({ now = Date.now() } = {}) {
|
|
202
|
+
export async function chooseBeforeEngine({ now = Date.now(), refreshHome = true } = {}) {
|
|
216
203
|
const choice = await readChoice();
|
|
217
204
|
if (choice.next === "room" && !choice.home) return { result: null, accounts: null };
|
|
218
205
|
const roster = await fetchClaudeAccounts();
|
|
@@ -237,26 +224,39 @@ export async function chooseBeforeEngine({ now = Date.now() } = {}) {
|
|
|
237
224
|
const threshold = numberOr(cfg["autoswitch.threshold"]?.value, 90);
|
|
238
225
|
const models = countedModels(cfg["autoswitch.model"]?.value);
|
|
239
226
|
const engine = await engineState(accounts);
|
|
227
|
+
const lastMoveAt = Math.max(choice.lastMoveAt, engine.lastSwitchAt);
|
|
228
|
+
const cooldownMs = numberOr(cfg["autoswitch.cooldownSeconds"]?.value, 300) * 1000;
|
|
229
|
+
const cooldownUntil = lastMoveAt + cooldownMs;
|
|
240
230
|
const pick = chooseAccount({
|
|
241
231
|
accounts,
|
|
242
232
|
choice: { next: choice.next, home: choice.home?.key ?? null, manual, goHome },
|
|
243
233
|
threshold,
|
|
244
234
|
models,
|
|
245
|
-
busy:
|
|
235
|
+
busy: await accountsInCswapRun(accounts),
|
|
246
236
|
quarantined: engine.quarantined,
|
|
247
|
-
sinceMove: now -
|
|
248
|
-
cooldownMs
|
|
237
|
+
sinceMove: now - lastMoveAt,
|
|
238
|
+
cooldownMs,
|
|
249
239
|
optional: (cfg["autoswitch.strategy"]?.value ?? "best") === "best",
|
|
250
240
|
now,
|
|
251
241
|
});
|
|
252
242
|
if (!pick) {
|
|
253
243
|
await updateChoice(keep);
|
|
254
|
-
return { result: null, accounts };
|
|
244
|
+
return { result: null, accounts, cooldownUntil };
|
|
245
|
+
}
|
|
246
|
+
// Give Home one collection pass before a fresh weekly candidate can
|
|
247
|
+
// preempt it. The loop retries with refreshHome=false after its dry run:
|
|
248
|
+
// a failed/backed-off collection must not prevent leaving a full account.
|
|
249
|
+
const home = choice.home ? accounts.find(a => a.key === choice.home.key) : null;
|
|
250
|
+
if (refreshHome && pick.trigger !== "home" && home && !home.active &&
|
|
251
|
+
!home.disabled && home.alive !== false && !home.error && !home.reauth &&
|
|
252
|
+
!readingCurrent(home, models, now)) {
|
|
253
|
+
await updateChoice(keep);
|
|
254
|
+
return { result: null, accounts, cooldownUntil };
|
|
255
255
|
}
|
|
256
256
|
const moved = await switchClaudeAccount(pick.to.num);
|
|
257
257
|
if (!moved.ok) {
|
|
258
258
|
await updateChoice(keep);
|
|
259
|
-
return { result: null, accounts };
|
|
259
|
+
return { result: null, accounts, cooldownUntil };
|
|
260
260
|
}
|
|
261
261
|
// The move happened, so where it left the user and when are true whatever
|
|
262
262
|
// was set meanwhile; a home still to be gone to is the current choice's.
|
|
@@ -102,8 +102,35 @@ async function runAutoTick() {
|
|
|
102
102
|
if (await externalAutoRunning()) return { skipped: "external-engine" };
|
|
103
103
|
// The deck's own rules first. A move they make is the tick; when they have
|
|
104
104
|
// nothing to say — or cannot say it — the engine decides, as it always did.
|
|
105
|
-
|
|
105
|
+
let chose = await chooseBeforeEngine().catch(() => ({ result: null, accounts: null }));
|
|
106
106
|
if (chose.result) return { chose: chose.result };
|
|
107
|
+
// The store may still have a reading from before the active account
|
|
108
|
+
// reached its limit. The engine polls before deciding; letting
|
|
109
|
+
// its real tick run now would switch by `best` before our weekly/home
|
|
110
|
+
// rules ever saw that reading. Poll without moving first, then give the
|
|
111
|
+
// configured rules the refreshed roster under this same store lock.
|
|
112
|
+
// No rule configured (accounts === null) keeps the original single run.
|
|
113
|
+
if (chose.accounts) {
|
|
114
|
+
const preview = await run(await cswapBin(), ["auto", "--once", "--json", "--dry-run"], { timeout: TICK_TIMEOUT_MS });
|
|
115
|
+
if (preview.timedOut || (!preview.ok && !preview.stdout)) return preview;
|
|
116
|
+
if (!_enabled) return { skipped: "disabled" };
|
|
117
|
+
if (await externalAutoRunning()) return { skipped: "external-engine" };
|
|
118
|
+
invalidateClaudeAccountsCache();
|
|
119
|
+
chose = await chooseBeforeEngine({ refreshHome: false }).catch(() => ({ result: null, accounts: null }));
|
|
120
|
+
if (chose.result) return { chose: chose.result };
|
|
121
|
+
// A deck-managed `cswap switch` does not advance the engine's own
|
|
122
|
+
// lastSwitchAt. Its fallback must keep our cooldown too, or a manual
|
|
123
|
+
// return to the full account can immediately escape the weekly rule
|
|
124
|
+
// through the engine's most-room choice. Still allow emergency exits.
|
|
125
|
+
const decision = summarise(preview.stdout);
|
|
126
|
+
if (Date.now() < chose.cooldownUntil && decision.trigger !== "at-limit" && decision.trigger !== "failover") {
|
|
127
|
+
return { chose: {
|
|
128
|
+
ok: true, ...decision, event: "no-switch", switched: false,
|
|
129
|
+
trigger: null, reason: "cooldown", from: null, to: null,
|
|
130
|
+
detail: "Waiting for the account-switch cooldown",
|
|
131
|
+
} };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
107
134
|
const out = await run(await cswapBin(), ["auto", "--once", "--json"], { timeout: TICK_TIMEOUT_MS });
|
|
108
135
|
return { ...out, accounts: chose.accounts };
|
|
109
136
|
});
|