ccdeck 3.43.4 → 3.44.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.
Files changed (75) hide show
  1. package/README.md +69 -17
  2. package/THIRD_PARTY_NOTICES.md +43 -7
  3. package/bin/cli/commands.js +613 -0
  4. package/bin/cli/help.js +26 -3
  5. package/bin/cli/one-shot.js +2 -2
  6. package/bin/cli/picker.js +86 -0
  7. package/bin/cli/startup.js +13 -0
  8. package/bin/deck.js +16 -1
  9. package/dist/web/assets/{BrowserWatchModal-nOxhGOO4.js → BrowserWatchModal-DMIUygWz.js} +1 -1
  10. package/dist/web/assets/{RadioPanel-BLfpQ1Iw.js → RadioPanel-DNB_AHSs.js} +1 -1
  11. package/dist/web/assets/TrafficRadar-Ch0LVdSl.js +8 -0
  12. package/dist/web/assets/UsageHistoryModal-Du0ZkIJ1.js +1 -0
  13. package/dist/web/assets/app-CQIstkMF.js +318 -0
  14. package/dist/web/assets/app-Ch-0CYlK.css +1 -0
  15. package/dist/web/assets/{index-Ch4ks76J.js → index-BQbOfxbL.js} +1 -1
  16. package/dist/web/index.html +3 -3
  17. package/package.json +2 -1
  18. package/release-notes.json +34 -0
  19. package/src/server/account-notify.mjs +206 -9
  20. package/src/server/account-pick.mjs +160 -0
  21. package/src/server/account-projects.mjs +55 -15
  22. package/src/server/account-routes.mjs +47 -4
  23. package/src/server/account-watch.mjs +48 -4
  24. package/src/server/activity.mjs +3 -2
  25. package/src/server/args.mjs +112 -2
  26. package/src/server/autoswitch-failover.mjs +138 -0
  27. package/src/server/claude-accounts.mjs +115 -14
  28. package/src/server/claude-dir.mjs +142 -5
  29. package/src/server/claude-identity.mjs +23 -0
  30. package/src/server/cli-lines.mjs +224 -0
  31. package/src/server/codex-cost-usage.mjs +67 -0
  32. package/src/server/codex-enrichment.mjs +53 -9
  33. package/src/server/codex-review-policy.d.mts +4 -0
  34. package/src/server/codex-review-policy.mjs +11 -0
  35. package/src/server/codex-skills.mjs +146 -0
  36. package/src/server/codex-watch.mjs +5 -2
  37. package/src/server/cswap-auto-choice.mjs +53 -0
  38. package/src/server/cswap-auto-loop.mjs +100 -9
  39. package/src/server/deck-client.mjs +112 -0
  40. package/src/server/deck-prefs.mjs +16 -5
  41. package/src/server/event-pipeline.mjs +19 -3
  42. package/src/server/event-routes.mjs +15 -0
  43. package/src/server/exec.mjs +6 -2
  44. package/src/server/index.mjs +17 -1
  45. package/src/server/installer.mjs +149 -8
  46. package/src/server/lan-engine.mjs +2 -2
  47. package/src/server/lan-sync.mjs +3 -3
  48. package/src/server/lan-wire.mjs +3 -3
  49. package/src/server/login-guard.mjs +162 -0
  50. package/src/server/login-service.mjs +3 -0
  51. package/src/server/pinned-build.mjs +5 -0
  52. package/src/server/prefs-routes.mjs +4 -0
  53. package/src/server/request-gates.mjs +7 -0
  54. package/src/server/session-read-gate.mjs +5 -3
  55. package/src/server/session-summary.mjs +281 -0
  56. package/src/server/skill-hook.mjs +162 -0
  57. package/src/server/stop-continue-gate.d.mts +8 -0
  58. package/src/server/stop-continue-gate.mjs +77 -0
  59. package/src/server/stop-continue.mjs +544 -0
  60. package/src/server/stop-failure.d.mts +10 -0
  61. package/src/server/stop-failure.mjs +97 -0
  62. package/src/server/swap-log.mjs +19 -10
  63. package/src/server/tool-audit-report.mjs +5 -3
  64. package/src/server/tool-audit-routes.mjs +5 -3
  65. package/src/server/tool-audit-skills.mjs +82 -0
  66. package/src/server/tool-audit.mjs +53 -11
  67. package/src/server/traffic-radar-capture.mjs +10 -1
  68. package/src/server/traffic-radar-monitor.mjs +4 -2
  69. package/src/server/traffic-radar-routes.mjs +13 -0
  70. package/src/server/traffic-radar-service.mjs +133 -0
  71. package/src/server/usage-day.mjs +1 -1
  72. package/dist/web/assets/TrafficRadar-BFrObpjW.js +0 -1
  73. package/dist/web/assets/UsageHistoryModal-DCnFf1vi.js +0 -1
  74. package/dist/web/assets/app-BiJ8TfUl.css +0 -1
  75. package/dist/web/assets/app-DBsNB-2y.js +0 -312
@@ -0,0 +1,162 @@
1
+ // Whether the deck may add the UserPromptExpansion hook that Skill activity
2
+ // counts typed slash commands with (#2181) — and, until somebody asks for it,
3
+ // the answer is no.
4
+ //
5
+ // WHY THIS ENTRY IS GATED HARDER THAN STOPFAILURE. A hook event name Claude
6
+ // Code does not know is not skipped by every version. Up to 2.1.100 it made
7
+ // Claude Code ignore the WHOLE settings.json — the user's permissions, env,
8
+ // model and every hook in it, ours included — without a word on screen. 2.1.101
9
+ // changed that ("an unrecognized hook event name in settings.json no longer
10
+ // causes the entire file to be ignored", Claude Code's CHANGELOG.md, 2.1.101).
11
+ // UserPromptExpansion itself arrived later still: the first npm release whose
12
+ // CLI carries the name, and whose Agent SDK lists it in HOOK_EVENTS, is 2.1.116
13
+ // (2.1.114 has neither, and there is no 2.1.115). Checked offline against the
14
+ // published packages, with a scratch config holding the deck's own entry shape:
15
+ //
16
+ // 2.1.100 the whole file ignored — no hook fired, no env applied
17
+ // 2.1.101 the file read, the entry ignored
18
+ // 2.1.114 the file read, the entry ignored
19
+ // 2.1.116 the file read, the entry fired on a typed custom command
20
+ //
21
+ // So the gate is the later of the two: 2.1.116, which is where the entry is
22
+ // both harmless and useful. Below it the entry buys nothing, and below 2.1.101
23
+ // it costs the user every setting they have.
24
+ //
25
+ // WHAT HAS TO BE TRUE before the entry is written:
26
+ // - the deck was started with AGENTS_DECK_SKILL_HOOK=1 — never by default;
27
+ // - at least one Claude Code was found, the way the deck finds `claude`: the
28
+ // candidate list (claude-dir.mjs) and AGENTS_DECK_CLAUDE;
29
+ // - EVERY one found says which version it is, and every one is 2.1.116 or
30
+ // newer. An old CLI anywhere on the machine still reads the same file, and
31
+ // one that will not say its version is treated as an old one.
32
+ // A version is read off disk first (claude-dir.mjs, no child process). Only
33
+ // for an install whose layout does not name one — a wrapper script, an unusual
34
+ // prefix — and only once the user has opted in, is that binary asked
35
+ // `--version`, by its full path and as argv, and believed only when it answers
36
+ // the way Claude Code does: `<x.y.z> (Claude Code)`.
37
+ //
38
+ // Every boot re-decides. A machine that stops passing — the opt-in removed, a
39
+ // Claude Code rolled back, a second, older one installed — has the entry taken
40
+ // back out by the next start (installer.mjs), and the user's own
41
+ // UserPromptExpansion hooks are left where they are.
42
+ import { claudeCodeInstalls, claudeVersionAtLeast } from "./claude-dir.mjs";
43
+ import { run } from "./exec.mjs";
44
+
45
+ export const USER_PROMPT_EXPANSION = "UserPromptExpansion";
46
+
47
+ /** The first Claude Code that knows the event: 2.1.116 (npm). */
48
+ export const USER_PROMPT_EXPANSION_SINCE = "2.1.116";
49
+
50
+ /** The first Claude Code that skips an unknown hook event instead of ignoring
51
+ * the whole settings.json: 2.1.101 (CHANGELOG.md). */
52
+ export const UNKNOWN_EVENT_TOLERATED_SINCE = "2.1.101";
53
+
54
+ /** The gate: a version that is past both of the above. */
55
+ export const SKILL_HOOK_SINCE = claudeVersionAtLeast(USER_PROMPT_EXPANSION_SINCE, UNKNOWN_EVENT_TOLERATED_SINCE)
56
+ ? USER_PROMPT_EXPANSION_SINCE
57
+ : UNKNOWN_EVENT_TOLERATED_SINCE;
58
+
59
+ /** Set to "1" to ask for the entry. Anything else, or nothing, is no. */
60
+ export const SKILL_HOOK_ENV = "AGENTS_DECK_SKILL_HOOK";
61
+
62
+ /** Whether the person starting the deck asked for the entry. Only the exact
63
+ * string "1": a typo or a "true" does not turn it on. */
64
+ export function skillHookOptedIn(env = process.env) {
65
+ return env?.[SKILL_HOOK_ENV] === "1";
66
+ }
67
+
68
+ /** A `--version` that is not quick is not Claude Code's fast path. */
69
+ const PROBE_TIMEOUT_MS = 5_000;
70
+
71
+ /** What Claude Code prints for `--version`, e.g. `2.1.116 (Claude Code)`. A
72
+ * first line in any other shape — a wrapper's own version, an error, an
73
+ * update notice — is no answer. */
74
+ const VERSION_LINE_RE = /^(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]*)?) \(Claude Code\)$/;
75
+
76
+ /** The version in Claude Code's own `--version` output, or undefined. */
77
+ export function parseClaudeVersionOutput(text) {
78
+ const first = String(text ?? "").split(/\r?\n/).map(l => l.trim()).find(l => l !== "");
79
+ const m = first === undefined ? null : VERSION_LINE_RE.exec(first);
80
+ return m ? m[1] : undefined;
81
+ }
82
+
83
+ async function askVersion(runImpl, bin, timeout) {
84
+ try {
85
+ const r = await runImpl(bin, ["--version"], { timeout });
86
+ return r?.ok ? parseClaudeVersionOutput(r.stdout) : undefined;
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * The decision, and why:
94
+ *
95
+ * { install: false, state: "off", reason: "not-enabled" }
96
+ * { install: false, state: "skipped", reason: "no-claude" | "version-unknown" | "too-old", versions }
97
+ * { install: true, state: "on", versions }
98
+ *
99
+ * `versions` is what was found, in the order found, with `null` for an install
100
+ * that did not say. `since` is the gate. Never throws: a probe that fails is a
101
+ * version that is unknown, which is a refusal.
102
+ *
103
+ * Every dependency is injected so the suite can lay out any machine: `installs`
104
+ * is claudeCodeInstalls()'s answer, `runImpl` is exec.mjs's run.
105
+ */
106
+ export async function skillHookDecision({
107
+ env = process.env,
108
+ optedIn = skillHookOptedIn(env),
109
+ installs,
110
+ runImpl = run,
111
+ timeout = PROBE_TIMEOUT_MS,
112
+ } = {}) {
113
+ const since = SKILL_HOOK_SINCE;
114
+ // Off is decided before anything is looked at: a deck that was not asked
115
+ // spends nothing on the question, and spawns nothing.
116
+ if (!optedIn) return { install: false, state: "off", reason: "not-enabled", since, versions: [] };
117
+ let found;
118
+ try {
119
+ found = Array.isArray(installs) ? [...installs] : claudeCodeInstalls();
120
+ } catch {
121
+ found = [];
122
+ }
123
+ // AGENTS_DECK_CLAUDE is the other way the deck finds `claude` — the sign-in
124
+ // and the account panel run it ahead of the candidate list
125
+ // (claude-identity.mjs) — so it is one more Claude Code that reads this
126
+ // settings.json. Its layout is not read: it is asked.
127
+ const configured = typeof env?.AGENTS_DECK_CLAUDE === "string" ? env.AGENTS_DECK_CLAUDE.trim() : "";
128
+ if (configured && !found.some(i => i.bin === configured)) found.push({ bin: configured, version: undefined });
129
+ if (found.length === 0) return { install: false, state: "skipped", reason: "no-claude", since, versions: [] };
130
+ const versions = [];
131
+ for (const { bin, version } of found) {
132
+ const v = version ?? (typeof bin === "string" && bin ? await askVersion(runImpl, bin, timeout) : undefined);
133
+ versions.push(v ?? null);
134
+ }
135
+ if (versions.some(v => v === null)) return { install: false, state: "skipped", reason: "version-unknown", since, versions };
136
+ if (!versions.every(v => claudeVersionAtLeast(v, since))) {
137
+ return { install: false, state: "skipped", reason: "too-old", since, versions };
138
+ }
139
+ return { install: true, state: "on", since, versions };
140
+ }
141
+
142
+ /**
143
+ * The boot report's row for the entry — `{ ok, detail }` — or null when there
144
+ * is nothing to say, which is the default: nobody asked for it, and a deck
145
+ * that was not asked prints exactly what it printed before. `dash` is the
146
+ * screen's own (bin/cli/screen.js), so the line carries no punctuation a
147
+ * legacy console cannot draw.
148
+ */
149
+ export function skillHookStatus(decision, dash = "-") {
150
+ if (!decision || decision.state === "off") return null;
151
+ const found = (decision.versions ?? []).map(v => v ?? "unknown").join(", ");
152
+ if (decision.state === "on") return { ok: true, detail: `on ${dash} counts typed slash commands (Claude Code ${found})` };
153
+ const why = {
154
+ "no-claude": "no Claude Code found",
155
+ "version-unknown": `a Claude Code here does not say its version (found: ${found})`,
156
+ "too-old": `needs Claude Code ${decision.since} or newer everywhere (found: ${found})`,
157
+ "settings-shape": "the hooks in settings.json are not in a shape the deck can add to",
158
+ "backup-failed": "settings.json could not be backed up first",
159
+ raced: "settings.json changed while installing; the next start tries again",
160
+ }[decision.reason] ?? "not allowed";
161
+ return { ok: false, detail: `not installed ${dash} ${why}` };
162
+ }
@@ -0,0 +1,8 @@
1
+ import type { StopFacts } from "./stop-failure.mjs";
2
+ export interface LoginEvidence { ok: boolean; at: number; room: boolean | null }
3
+ export type GateReason = "no-login" | "before-stop" | "no-room";
4
+ export interface Gate { ready: boolean; reason: GateReason | null }
5
+ export const CONTINUE_PROMPT: string;
6
+ export const REPEAT_HOLD_MS: number;
7
+ export function loginEvidence(reading: unknown): LoginEvidence | null;
8
+ export function continueGate(stop: Pick<StopFacts, "kind" | "at"> | null | undefined, login: LoginEvidence | null | undefined): Gate;
@@ -0,0 +1,77 @@
1
+ // When a turn that stopped on an expired login or a usage limit may be run
2
+ // again (#2193), read once for both halves of the deck: the page draws the
3
+ // Continue button from it, and the server refuses a press — and holds back the
4
+ // automatic continue — on the same answer. The page imports this file the way
5
+ // it imports stop-failure.mjs, so the two cannot come to disagree about one
6
+ // stop.
7
+ //
8
+ // WHAT COUNTS AS "A WORKING LOGIN IS ACTIVE AGAIN". The deck's newest reading
9
+ // of the active account's usage (quota.mjs), taken AFTER the stop and without
10
+ // an error. That reading is made with the very credentials a session uses —
11
+ // Claude Code's own token, `claude --print /usage`, or claude-swap's copy of
12
+ // the active account — so a login that is still dead cannot produce one, and
13
+ // one from before the stop says nothing about the login now. A switch of
14
+ // account invalidates the reading, so the next one is about the new account.
15
+ //
16
+ // And the account has to have room: a working login on an account at 100% of
17
+ // its 5-hour or 7-day window would stop the turn again on a usage limit at its
18
+ // first request.
19
+ //
20
+ // Pure. Nothing here reads a clock, a file or a process.
21
+
22
+ /** What the deck sends a background session to continue the turn that
23
+ * failed: Claude Code's own words for it, the default of
24
+ * CLAUDE_CODE_RESUME_PROMPT (code.claude.com/docs/en/env-vars), which is what
25
+ * Claude Code sends when it continues an interrupted turn by itself. */
26
+ export const CONTINUE_PROMPT = "Continue from where you left off.";
27
+
28
+ /**
29
+ * A session the deck continued automatically, which then stops again on a
30
+ * login or a limit inside this window, is not continued automatically again
31
+ * until somebody acts on it — a prompt typed into it, or a press of Continue.
32
+ * That is the loop the setting must never become: a login that works for one
33
+ * request and dies on the next would otherwise be continued, stop, and be
34
+ * continued again for as long as the deck runs.
35
+ */
36
+ export const REPEAT_HOLD_MS = 60 * 60_000;
37
+
38
+ /**
39
+ * The quota reading as the gate reads it: `{ ok, at, room }`, or null when
40
+ * there is no reading at all.
41
+ *
42
+ * ok the reading is a real one, not a failure to read
43
+ * at when its numbers were collected — the reading's own clock, which is
44
+ * the one that can be compared with a stop's (a reading the deck is
45
+ * only holding over is still as old as it says)
46
+ * room both of the account's windows are under 100%, or null when the
47
+ * reading names neither
48
+ */
49
+ export function loginEvidence(reading) {
50
+ if (!reading || typeof reading !== "object") return null;
51
+ const at = Number(reading.fetchedAt);
52
+ const when = Number.isFinite(at) && at > 0 ? at : 0;
53
+ if (reading.ok !== true || !when) return { ok: false, at: when, room: null };
54
+ const pcts = [reading.session5hPct, reading.week7dPct].filter(v => typeof v === "number" && Number.isFinite(v));
55
+ return { ok: true, at: when, room: pcts.length ? pcts.every(p => p < 100) : null };
56
+ }
57
+
58
+ /**
59
+ * Whether the turn `stop` ended may be continued now, and when not, why:
60
+ *
61
+ * "no-login" there is no working reading of the active account at all
62
+ * "before-stop" the newest one was taken before the stop, so it cannot say
63
+ * the login works now
64
+ * "no-room" it works, and its 5-hour or 7-day window is full
65
+ *
66
+ * An API error that is neither — an overloaded server, a refused request — is
67
+ * not about the login, so nothing here holds it back. The automatic continue
68
+ * never acts on one (stop-continue.mjs); a press may.
69
+ */
70
+ export function continueGate(stop, login) {
71
+ if (!stop || typeof stop !== "object") return { ready: false, reason: "no-login" };
72
+ if (stop.kind !== "login" && stop.kind !== "usage-limit") return { ready: true, reason: null };
73
+ if (!login || !login.ok) return { ready: false, reason: "no-login" };
74
+ if (!(login.at > stop.at)) return { ready: false, reason: "before-stop" };
75
+ if (login.room === false) return { ready: false, reason: "no-room" };
76
+ return { ready: true, reason: null };
77
+ }