ccdeck 3.22.15 → 3.23.1
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 +53 -10
- package/THIRD_PARTY_NOTICES.md +123 -15
- package/bin/deck.js +264 -21
- package/dist/web/assets/BrowserWatchModal-B00oF8_I.js +1 -0
- package/dist/web/assets/UsageHistoryModal-IkFiv0Vg.js +1 -0
- package/dist/web/assets/index-DgrvXP9K.css +1 -0
- package/dist/web/assets/index-Q2E4ePHr.js +296 -0
- package/dist/web/index.html +9 -5
- package/hook/hook.js +631 -99
- package/package.json +1 -1
- package/release-notes.json +62 -1
- package/src/server/activity.mjs +42 -4
- package/src/server/args.mjs +30 -4
- package/src/server/browser-history.mjs +164 -38
- package/src/server/browser-presence.mjs +60 -0
- package/src/server/browser-react.mjs +160 -7
- package/src/server/browser-watch-store.mjs +319 -17
- package/src/server/browser-watch.mjs +304 -60
- package/src/server/ccusage.mjs +20 -4
- package/src/server/claude-accounts.mjs +79 -34
- package/src/server/claude-identity.mjs +121 -0
- package/src/server/cswap-admin.mjs +160 -133
- package/src/server/cswap-auto.mjs +86 -7
- package/src/server/cswap-install.mjs +280 -43
- package/src/server/deck-home.mjs +1 -1
- package/src/server/deck-prefs.mjs +244 -36
- package/src/server/deck-probe.mjs +1 -1
- package/src/server/exec.mjs +84 -3
- package/src/server/global-install.mjs +13 -0
- package/src/server/index.mjs +1454 -184
- package/src/server/installer.mjs +136 -8
- package/src/server/lan-about.mjs +8 -3
- package/src/server/lan-engine.mjs +335 -47
- package/src/server/lan-socket.mjs +384 -57
- package/src/server/lan-sync.mjs +472 -22
- package/src/server/log-writer.mjs +575 -2
- package/src/server/login-service.mjs +31 -4
- package/src/server/open-url.mjs +22 -2
- package/src/server/output-watch.mjs +59 -8
- package/src/server/purge-key.mjs +135 -0
- package/src/server/quota.mjs +112 -7
- package/src/server/running-deck.mjs +15 -1
- package/src/server/self-update.mjs +98 -4
- package/src/server/session-recap.mjs +89 -0
- package/src/server/static-cache.mjs +67 -0
- package/src/server/stop-deck.mjs +2 -2
- package/src/server/store-lock.mjs +51 -0
- package/src/server/supervisor.mjs +1 -1
- package/src/server/system-metrics.mjs +143 -17
- package/dist/web/assets/index-BYqkKWPC.js +0 -274
- package/dist/web/assets/index-s_dF8YYA.css +0 -1
package/src/server/installer.mjs
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
// sessions reach the same server through the rollout watcher instead, so one
|
|
6
6
|
// running server still sees both CLIs. Re-runs are safe; entries are tagged
|
|
7
7
|
// with __agent-dag and de-duped.
|
|
8
|
-
import { readFile, mkdir, unlink, rename, open, stat, chmod, realpath, readlink } from "node:fs/promises";
|
|
8
|
+
import { readFile, mkdir, unlink, rename, open, stat, chmod, realpath, readlink, utimes } from "node:fs/promises";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { join, resolve, dirname } from "node:path";
|
|
11
11
|
import { setTimeout as delay } from "node:timers/promises";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { claudeConfigDir } from "./claude-dir.mjs";
|
|
14
14
|
import { CODEX_HOME } from "./codex-dir.mjs";
|
|
15
|
+
import { canonicalLogPath } from "./log-writer.mjs";
|
|
15
16
|
import { shellQuoteArg } from "./exec.mjs";
|
|
16
17
|
import { PRODUCT } from "./brand.mjs";
|
|
17
18
|
|
|
@@ -416,10 +417,31 @@ async function installHookScript(installDir) {
|
|
|
416
417
|
return dst;
|
|
417
418
|
}
|
|
418
419
|
|
|
420
|
+
/**
|
|
421
|
+
* The entry Claude Code and Codex run, and the one number in it that is a
|
|
422
|
+
* promise about time.
|
|
423
|
+
*
|
|
424
|
+
* `timeout` is in SECONDS and it is a kill: when it expires the host CLI takes
|
|
425
|
+
* the hook down where it stands, which on a PreToolUse means the deck is handed
|
|
426
|
+
* a truncated body and the user waits the whole of it before the tool call
|
|
427
|
+
* proceeds. So the declared value has to be strictly larger than the worst the
|
|
428
|
+
* hook can spend on itself, and it was not. hook.js caps itself at CAP_MS =
|
|
429
|
+
* 1900ms, that cap now runs from Node's start rather than from main()'s, and
|
|
430
|
+
* what is still outside it is only the `sh -c` fork/exec — but 1900 + a fork
|
|
431
|
+
* under load does not fit in 2000. Measured through the installed command
|
|
432
|
+
* shape with a deck that stops answering mid-run: 1.84s idle, 1.98-2.19s with
|
|
433
|
+
* the box loaded (#1018).
|
|
434
|
+
*
|
|
435
|
+
* 3 is a backstop, not a budget: the hook ends itself first in every case it
|
|
436
|
+
* can see, and this is what covers the ones it cannot — a payload large enough
|
|
437
|
+
* that parsing it starves the timer, or a filesystem thread that cannot be
|
|
438
|
+
* interrupted at all. hook-budget.test.ts pins this number against hook.js's
|
|
439
|
+
* own cap so raising one without the other fails.
|
|
440
|
+
*/
|
|
419
441
|
function buildHookEntry(command) {
|
|
420
442
|
return {
|
|
421
443
|
[MARK_KEY]: true,
|
|
422
|
-
hooks: [{ type: "command", command, timeout:
|
|
444
|
+
hooks: [{ type: "command", command, timeout: 3 }],
|
|
423
445
|
};
|
|
424
446
|
}
|
|
425
447
|
|
|
@@ -667,6 +689,59 @@ export function hasCodexInstalled() {
|
|
|
667
689
|
return existsSync(CODEX_DIR);
|
|
668
690
|
}
|
|
669
691
|
|
|
692
|
+
/**
|
|
693
|
+
* Whether an older deck's Codex forwarders are still in hooks.json, found
|
|
694
|
+
* without touching the file: `{ settingsPath, count }` when they are, null when
|
|
695
|
+
* they are not or when the file cannot be read.
|
|
696
|
+
*
|
|
697
|
+
* #253 stopped installing Codex hooks and #317 deleted the recipe, but a
|
|
698
|
+
* machine that ran a deck from before either still has up to nine marked
|
|
699
|
+
* entries in $CODEX_HOME/hooks.json, each pointing at a copy of the forwarder
|
|
700
|
+
* that is still on disk. Wherever Codex honours that file, every one of them
|
|
701
|
+
* posts to `/api/event` — which takes a post without a token — while the
|
|
702
|
+
* rollout watcher reads the same session off disk, so the session is ingested
|
|
703
|
+
* twice: both copies reach the event ring and events.jsonl, and only the ones
|
|
704
|
+
* that land inside the reducer's redelivery windows are folded on the card.
|
|
705
|
+
* uninstallHooks takes them out, and `--uninstall` is its only caller; nothing
|
|
706
|
+
* else ever looked (#983).
|
|
707
|
+
*
|
|
708
|
+
* READ ONLY, and that is the decision rather than an oversight. This runs on
|
|
709
|
+
* every boot, and a boot writes nothing under the Codex directory — bin/deck.js
|
|
710
|
+
* says so where it decides whether to watch Codex at all. The one retirement a
|
|
711
|
+
* boot does perform on another tool's file, the finish-sound hook above, rides
|
|
712
|
+
* on a write of Claude Code's settings.json that the boot was making anyway,
|
|
713
|
+
* and installHooks says that riding along is what makes it safe: one write,
|
|
714
|
+
* compared against the bytes just read, converging on every later boot. A sweep
|
|
715
|
+
* of hooks.json would have none of that. It would be the deck's first
|
|
716
|
+
* unasked-for write to a file it no longer manages, made on every machine that
|
|
717
|
+
* has Codex, to cure a condition only upgraded machines have. So this finds
|
|
718
|
+
* them, the banner names them with the command that removes them, and the user
|
|
719
|
+
* decides — the shape `--uninstall` already gives the LAN key: named, not
|
|
720
|
+
* removed, unless somebody asked.
|
|
721
|
+
*
|
|
722
|
+
* A file that cannot be read answers null rather than throwing. The boot must
|
|
723
|
+
* not die over a Codex file, and "your hooks are in there" said of a file
|
|
724
|
+
* nobody could parse would be a guess; `--uninstall` reports that case itself,
|
|
725
|
+
* out loud, when it is run.
|
|
726
|
+
*/
|
|
727
|
+
export async function leftoverCodexHooks() {
|
|
728
|
+
const settingsPath = PROVIDERS.codex.settingsPath;
|
|
729
|
+
let settings;
|
|
730
|
+
try {
|
|
731
|
+
({ settings } = await readSettingsForWrite(settingsPath));
|
|
732
|
+
} catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const hooks = settings?.hooks;
|
|
736
|
+
if (!hooks || typeof hooks !== "object") return null;
|
|
737
|
+
let count = 0;
|
|
738
|
+
for (const group of Object.values(hooks)) {
|
|
739
|
+
if (!Array.isArray(group)) continue;
|
|
740
|
+
for (const g of group) if (isOurEntry(g)) count++;
|
|
741
|
+
}
|
|
742
|
+
return count > 0 ? { settingsPath, count } : null;
|
|
743
|
+
}
|
|
744
|
+
|
|
670
745
|
/**
|
|
671
746
|
* The events log as the record spells it: an absolute path, or null when this
|
|
672
747
|
* deck writes none. Shared by the writer and by ensureDiscovery's comparison,
|
|
@@ -676,6 +751,21 @@ function persistField(persist) {
|
|
|
676
751
|
return typeof persist === "string" && persist !== "" ? persist : null;
|
|
677
752
|
}
|
|
678
753
|
|
|
754
|
+
/**
|
|
755
|
+
* The Codex tree as the record spells it: the canonical path of the CODEX_HOME
|
|
756
|
+
* this process reads, or null for a --no-codex deck, which reads none. Shared by
|
|
757
|
+
* the writer and by ensureDiscovery's comparison for the reason persistField
|
|
758
|
+
* is, and exported for a start's own shape in bin/deck.js — so a start asking
|
|
759
|
+
* "does the running deck read my tree" spells the tree exactly the way the
|
|
760
|
+
* record it compares against does. Worked out on every call rather than once at
|
|
761
|
+
* load: it is one realpath on a five-second heartbeat, and a tree that did not
|
|
762
|
+
* exist when the deck started — the first `codex login` creates it — is then
|
|
763
|
+
* named the way it is named once it does.
|
|
764
|
+
*/
|
|
765
|
+
export function codexHomeField(codex) {
|
|
766
|
+
return codex !== false ? canonicalLogPath(CODEX_DIR) : null;
|
|
767
|
+
}
|
|
768
|
+
|
|
679
769
|
// Every deck's token lives in this directory, and writeFileAtomic's temp file is
|
|
680
770
|
// created beside its target with whatever the umask allows — 0644 on most
|
|
681
771
|
// machines — so for the moment before the rename the token would sit in a
|
|
@@ -731,6 +821,23 @@ export async function writeDiscovery({ port, workspace, token, persist = null, c
|
|
|
731
821
|
// than win it and record a rollout it is not even reading. See
|
|
732
822
|
// writesCodexLog in src/server/log-writer.mjs.
|
|
733
823
|
codex: codex !== false,
|
|
824
|
+
// And WHOSE rollouts. `codex: true` says this deck tails them and never said
|
|
825
|
+
// from where, while CODEX_HOME moves the whole tree — #375 was five modules
|
|
826
|
+
// disagreeing about where it points. Two decks sharing one events.jsonl can
|
|
827
|
+
// therefore be reading two different trees, and the election grouped them
|
|
828
|
+
// anyway, because an unscoped deck's workspace contains every cwd and this
|
|
829
|
+
// was the one field that could have told them apart. The lower-port deck won
|
|
830
|
+
// a rollout it had never opened, the deck reading it stood down, and the
|
|
831
|
+
// shared log recorded nothing of that session (#982).
|
|
832
|
+
//
|
|
833
|
+
// Canonical rather than as spelled, for the reason canonicalLogPath gives
|
|
834
|
+
// the log path above (#793): a symlinked ~/.codex, an 8.3-shortened
|
|
835
|
+
// USERPROFILE or /tmp against /private/tmp would otherwise put two decks
|
|
836
|
+
// reading one tree into two groups and elect them both — the duplicate the
|
|
837
|
+
// election exists to end. Only this published copy is canonicalised.
|
|
838
|
+
// codex-dir.mjs goes on reading and writing through the spelling the user
|
|
839
|
+
// chose, which it has to for a symlinked home (see codexHome() there).
|
|
840
|
+
codexHome: codexHomeField(codex),
|
|
734
841
|
// Does this deck run Browser Watch? The watch elects a single writer among
|
|
735
842
|
// the decks on a machine, and it elected on port alone — so an older ccdeck
|
|
736
843
|
// that predates the feature won the election by having the lower port and
|
|
@@ -791,13 +898,17 @@ export function discoveryPath() {
|
|
|
791
898
|
* the deck received zero events while looking perfectly healthy. Re-asserting
|
|
792
899
|
* is cheap (one small read), so the deck checks rather than assumes.
|
|
793
900
|
*
|
|
794
|
-
* A file this process wrote is left alone, mode
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
901
|
+
* A file this process wrote is left alone, mode and contents included — only its
|
|
902
|
+
* mtime moves, stamped on every check; see the note where it is. Anything else —
|
|
903
|
+
* no file, unreadable, another pid, a stale port, token, events log, Codex
|
|
904
|
+
* setting or Codex tree, Claude setting, or a version left by the deck this
|
|
905
|
+
* process replaced — is replaced. Every field another deck decides by is compared, the
|
|
798
906
|
* log path included: leave one out and a record missing it would pass as ours
|
|
799
907
|
* forever, which for the log path means no deck can tell which of them share a
|
|
800
|
-
* file and they all write their own copy of every event again.
|
|
908
|
+
* file and they all write their own copy of every event again. The Codex tree
|
|
909
|
+
* is compared strictly for the same reason: a record from before the field
|
|
910
|
+
* existed has none, and `undefined` must read as "rewrite", never as a
|
|
911
|
+
* --no-codex deck's null.
|
|
801
912
|
*/
|
|
802
913
|
export async function ensureDiscovery({ port, workspace, token, persist = null, codex = true, claude = true, version = "", parent = null }) {
|
|
803
914
|
const file = discoveryPath();
|
|
@@ -810,9 +921,19 @@ export async function ensureDiscovery({ port, workspace, token, persist = null,
|
|
|
810
921
|
&& (d.token ?? "") === (token ?? "")
|
|
811
922
|
&& (d.persist ?? null) === persistField(persist)
|
|
812
923
|
&& d.codex === (codex !== false)
|
|
924
|
+
&& d.codexHome === codexHomeField(codex)
|
|
813
925
|
&& d.claude === (claude !== false)
|
|
814
926
|
&& (d.version ?? "") === (typeof version === "string" ? version : "")
|
|
815
927
|
&& (d.parent ?? null) === (Number.isInteger(parent) ? parent : null)) {
|
|
928
|
+
// STAMPED, though nothing in it changed. The mtime is how a reader tells a
|
|
929
|
+
// record some deck is still keeping from one whose deck is gone while its
|
|
930
|
+
// pid lives on under another process: hook.js unlinks a record whose port
|
|
931
|
+
// lets both challenge deadlines pass only once this stamp is a minute old
|
|
932
|
+
// (#1069), which a deck running this check every five seconds never lets
|
|
933
|
+
// happen. Best-effort — a stamp that cannot be written leaves the record
|
|
934
|
+
// exactly as every deck before this one left it.
|
|
935
|
+
const now = new Date();
|
|
936
|
+
await utimes(file, now, now).catch(() => {});
|
|
816
937
|
return { file, rewritten: false };
|
|
817
938
|
}
|
|
818
939
|
} catch { /* missing, unreadable or corrupt — rewritten below */ }
|
|
@@ -909,4 +1030,11 @@ export { AGENT_DAG_DIR, CLAUDE_DIR, CODEX_DIR };
|
|
|
909
1030
|
// repo for the same reasons settings.json is, and "never rename onto a link" is
|
|
910
1031
|
// one rule: codex-auth.mjs called a realpath of its own before this existed, and
|
|
911
1032
|
// two spellings of a rule are two things that can drift.
|
|
912
|
-
|
|
1033
|
+
// `stripBom` goes out on the same argument at the smallest scale. Two stores
|
|
1034
|
+
// now move an unparseable file aside instead of writing defaults over it —
|
|
1035
|
+
// deck-prefs.mjs since #1002 and browser-watch-store.mjs since #1003 — and all
|
|
1036
|
+
// three readers have to mean the same thing by "a BOM is not damage". If they
|
|
1037
|
+
// do not, a prefs.json or a state.json somebody opened in Notepad gets
|
|
1038
|
+
// quarantined for a mark settings.json has ignored since this function was
|
|
1039
|
+
// written.
|
|
1040
|
+
export { readSettingsForWrite, writeFileAtomic, renameWithRetry, createTemp, resolveWriteTarget, stripBom };
|
package/src/server/lan-about.mjs
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
// first thing somebody hunting for a known-broken version would want, and
|
|
8
8
|
// nobody on the network is owed it. It rides the manifest exchange, which only
|
|
9
9
|
// happens between two decks that have each accepted the other — and it is
|
|
10
|
-
// sealed with that connection's key
|
|
11
|
-
// the
|
|
10
|
+
// sealed with that connection's key on its own. Between two decks that both
|
|
11
|
+
// seal (#810) the frame around it is sealed too, and this is a second seal
|
|
12
|
+
// inside the first; toward a deck from before that, the frames around it are
|
|
13
|
+
// still JSON in the clear on the wire, and this seal is the only one there is.
|
|
12
14
|
//
|
|
13
15
|
// BOTH WAYS. The deck that dials sends its own card with the question, and the
|
|
14
16
|
// deck that answers sends its own with the answer. A deck that only calls in —
|
|
@@ -85,7 +87,10 @@ export function aboutThisDeck({
|
|
|
85
87
|
* `\p{Cc}` is the Unicode class for exactly those, C0 and C1 both. */
|
|
86
88
|
function field(v, max, shape = null) {
|
|
87
89
|
if (typeof v !== "string") return null;
|
|
88
|
-
|
|
90
|
+
// `\p{Cf}` too — the FORMAT class the control class does not cover, holding
|
|
91
|
+
// U+202E and the directional isolates. `os` is free-form and 48 characters
|
|
92
|
+
// wide, so it is the longest peer-chosen string the panel draws.
|
|
93
|
+
const flat = v.replace(/\p{Cc}/gu, " ").replace(/\p{Cf}/gu, "").replace(/\s+/g, " ").trim();
|
|
89
94
|
const s = [...flat].slice(0, max).join("").trim();
|
|
90
95
|
return s && (!shape || shape.test(s)) ? s : null;
|
|
91
96
|
}
|