ccdeck 3.26.0 → 3.26.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 +20 -1
- package/bin/agent-dag.js +23 -1
- package/dist/web/assets/{BrowserWatchModal-C5ouM-hi.js → BrowserWatchModal-CjOjANxG.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-D5mC-WNb.js → UsageHistoryModal-DfPKQO6q.js} +1 -1
- package/dist/web/assets/{index-DfvLgdJz.js → index-DZzBylIp.js} +15 -15
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/release-notes.json +3 -1
- package/src/server/ccusage.mjs +33 -1
- package/src/server/lan-reach.mjs +58 -17
package/dist/web/index.html
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
stored === "light" || (stored !== "dark" && light) ? "light" : "dark");
|
|
45
45
|
})();
|
|
46
46
|
</script>
|
|
47
|
-
<script type="module" crossorigin src="/assets/index-
|
|
47
|
+
<script type="module" crossorigin src="/assets/index-DZzBylIp.js"></script>
|
|
48
48
|
<link rel="stylesheet" crossorigin href="/assets/index-DlaHgzye.css">
|
|
49
49
|
</head>
|
|
50
50
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccdeck",
|
|
3
|
-
"version": "3.26.
|
|
3
|
+
"version": "3.26.3",
|
|
4
4
|
"description": "Live dashboard for Claude Code and Codex — which agent is waiting on you, tool calls, token spend and every Claude Code subagent on one calm canvas. Run it with npx ccdeck.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/release-notes.json
CHANGED
package/src/server/ccusage.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// the current call always serves from the already-installed copy. If the
|
|
12
12
|
// managed install is missing/broken we fall back to the old npx path so the
|
|
13
13
|
// feature still works on a fresh machine.
|
|
14
|
+
import { createRequire } from "node:module";
|
|
14
15
|
import { spawn } from "node:child_process";
|
|
15
16
|
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
16
17
|
import path from "node:path";
|
|
@@ -738,12 +739,43 @@ function discardDamagedInstall(runner, err) {
|
|
|
738
739
|
export const userSpec = (file, args = [], platform = process.platform) =>
|
|
739
740
|
spawnSpec(file, args, platform);
|
|
740
741
|
|
|
742
|
+
/**
|
|
743
|
+
* The platform binary ccusage's own cli.js would run, resolved the way cli.js
|
|
744
|
+
* resolves it (`@ccusage/ccusage-<platform>-<arch>/bin/ccusage[.exe]`, from
|
|
745
|
+
* the package's own directory), or null when there is none.
|
|
746
|
+
*
|
|
747
|
+
* WINDOWS ONLY, and for one reason: cli.js starts that binary WITHOUT hiding
|
|
748
|
+
* its console. The deck runs with no console of its own (detached, or inside
|
|
749
|
+
* the desktop app), so the binary is given a new one — and with Windows
|
|
750
|
+
* Terminal as the default terminal that is a window flashing up on every
|
|
751
|
+
* usage read, measured on a Windows 10 box as a CASCADIA_HOSTING_WINDOW_CLASS
|
|
752
|
+
* hosting ccusage. Running the binary directly, with windowsHide, is what
|
|
753
|
+
* cli.js does minus the window. Elsewhere nothing flashes, and the wrapper
|
|
754
|
+
* stays in the path it has always been in.
|
|
755
|
+
*
|
|
756
|
+
* Exported for tests; `resolve` is injectable.
|
|
757
|
+
*/
|
|
758
|
+
export function nativeCcusage(entry, platform = process.platform, arch = process.arch, resolve = null) {
|
|
759
|
+
if (platform !== "win32" || typeof entry !== "string" || !entry) return null;
|
|
760
|
+
const pkg = arch === "arm64" ? "@ccusage/ccusage-win32-arm64" : arch === "x64" ? "@ccusage/ccusage-win32-x64" : null;
|
|
761
|
+
if (!pkg) return null;
|
|
762
|
+
try {
|
|
763
|
+
const req = resolve ?? createRequire(entry).resolve;
|
|
764
|
+
return req(`${pkg}/bin/ccusage.exe`);
|
|
765
|
+
} catch {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
741
770
|
// One attempt with one runner. No branch gets a shell. The managed install is
|
|
742
771
|
// `node <entry> …`, which never needed one; the user's own copy and the npx
|
|
743
772
|
// fallback are routed through spawnSpec instead — see npxSpec and userSpec, and
|
|
744
773
|
// note that `args` here ends in whatever /api/ccusage was asked for.
|
|
745
774
|
function runOnce(runner, args) {
|
|
746
|
-
const
|
|
775
|
+
const native = runner.kind === "node" ? nativeCcusage(runner.entry) : null;
|
|
776
|
+
const { file, args: full, opts } = native
|
|
777
|
+
? { file: native, args, opts: {} }
|
|
778
|
+
: runner.kind === "node"
|
|
747
779
|
? { file: process.execPath, args: [runner.entry, ...args], opts: {} }
|
|
748
780
|
: runner.kind === "path"
|
|
749
781
|
? userSpec(runner.file, args)
|
package/src/server/lan-reach.mjs
CHANGED
|
@@ -31,25 +31,35 @@
|
|
|
31
31
|
// suite can run it on any platform. That is the same division lan-sync.mjs and
|
|
32
32
|
// lan-socket.mjs already keep, for the same reason.
|
|
33
33
|
|
|
34
|
-
/** What the probe asks Windows for.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
34
|
+
/** What the probe asks Windows for. `Get-NetConnectionProfile` and
|
|
35
|
+
* `Get-NetFirewallProfile` answer for an ordinary user; `Get-NetFirewallPortFilter`
|
|
36
|
+
* returns "Access is denied", which is why the rule this looks for is scoped
|
|
37
|
+
* to a PROGRAM rather than a port — also the only kind that keeps working,
|
|
38
|
+
* since the sync listener's port is ephemeral and different after every
|
|
39
|
+
* restart. `Get-NetFirewallApplicationFilter` answers on many machines but is
|
|
40
|
+
* denied on some domain-managed ones, so it is wrapped and its failure carried
|
|
41
|
+
* through as `rulesReadable=$false` rather than silently read as "no rule". */
|
|
41
42
|
export const PROBE_PS = [
|
|
42
43
|
"$ErrorActionPreference='SilentlyContinue'",
|
|
43
44
|
"$nets = @(Get-NetConnectionProfile | ForEach-Object { [pscustomobject]@{ alias=$_.InterfaceAlias; category=[string]$_.NetworkCategory; v4=[string]$_.IPv4Connectivity } })",
|
|
44
45
|
"$profiles = @(Get-NetFirewallProfile | ForEach-Object { [pscustomobject]@{ name=[string]$_.Name; enabled=[bool]$_.Enabled } })",
|
|
45
|
-
|
|
46
|
+
// AN EMPTY LIST HAS TWO MEANINGS, AND THEY ARE OPPOSITE. `Get-NetFirewallApplicationFilter`
|
|
47
|
+
// answers for an ordinary user on some machines and throws "Access is denied"
|
|
48
|
+
// on others (a domain-managed box, measured). With errors silenced it came
|
|
49
|
+
// back empty either way — and empty was read as "no rule", so a machine whose
|
|
50
|
+
// rule the deck simply could not see was told, wrongly, that it had none, and
|
|
51
|
+
// handed a command that added one more every time it was run. Wrapped so the
|
|
52
|
+
// throw is caught and reported as `rulesReadable=$false`: the difference
|
|
53
|
+
// between "there is no rule" and "I was not allowed to look".
|
|
54
|
+
"$rulesReadable=$true",
|
|
55
|
+
"try { $rules = @(Get-NetFirewallApplicationFilter -ErrorAction Stop | Where-Object { $_.Program -eq $env:CCDECK_EXE } | ForEach-Object { $_ | Get-NetFirewallRule } | ForEach-Object { [pscustomobject]@{ direction=[string]$_.Direction; action=[string]$_.Action; enabled=[bool]$_.Enabled; profile=[string]$_.Profile } }) } catch { $rulesReadable=$false; $rules=@() }",
|
|
46
56
|
// WHICH INTERFACE IS THE LAN, asked of the routing table rather than guessed
|
|
47
57
|
// from a name. The beacon goes to 255.255.255.255, so the interface that
|
|
48
58
|
// carries the broadcast IS the one this feature lives or dies on — and on a
|
|
49
59
|
// machine with Tailscale beside wifi the two have different categories, so
|
|
50
60
|
// picking the wrong one reports Private and clears a machine that is blocked.
|
|
51
61
|
"$bcast = [string](Find-NetRoute -RemoteIPAddress 255.255.255.255 | Select-Object -First 1 -ExpandProperty InterfaceAlias)",
|
|
52
|
-
"[pscustomobject]@{ nets=$nets; profiles=$profiles; rules=$rules; bcast=$bcast } | ConvertTo-Json -Depth 4 -Compress",
|
|
62
|
+
"[pscustomobject]@{ nets=$nets; profiles=$profiles; rules=$rules; rulesReadable=$rulesReadable; bcast=$bcast } | ConvertTo-Json -Depth 4 -Compress",
|
|
53
63
|
].join("; ");
|
|
54
64
|
|
|
55
65
|
/**
|
|
@@ -88,6 +98,9 @@ export function readProbe(stdout) {
|
|
|
88
98
|
enabled: r.enabled === true,
|
|
89
99
|
profile: typeof r.profile === "string" ? r.profile : "",
|
|
90
100
|
})),
|
|
101
|
+
// True unless the probe said it could not read the rules — and true for an
|
|
102
|
+
// older probe that did not report it, which kept working as it always did.
|
|
103
|
+
rulesReadable: raw.rulesReadable !== false,
|
|
91
104
|
bcast: typeof raw.bcast === "string" ? raw.bcast.trim() : "",
|
|
92
105
|
};
|
|
93
106
|
}
|
|
@@ -171,15 +184,31 @@ export function ruleCovers(rules, profileName) {
|
|
|
171
184
|
* wrong one for the router at home, and this module cannot tell which one
|
|
172
185
|
* somebody is sitting in. The panel says so beside it.
|
|
173
186
|
*/
|
|
174
|
-
export function fixSteps({ category, alias, exePath }) {
|
|
187
|
+
export function fixSteps({ category, alias, exePath, rules = [] }) {
|
|
175
188
|
const steps = [];
|
|
176
|
-
|
|
189
|
+
const name = profileFor(category);
|
|
190
|
+
if (name === "Public" && alias) {
|
|
177
191
|
steps.push(`Set-NetConnectionProfile -InterfaceAlias "${alias}" -NetworkCategory Private`);
|
|
178
192
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
193
|
+
// THE PROFILE OF THE NETWORK THE DECK IS ON, NOT ALWAYS PRIVATE. A company
|
|
194
|
+
// LAN joined to a domain is `DomainAuthenticated`, which the firewall answers
|
|
195
|
+
// with its Domain profile, and a rule scoped to Private never applies there.
|
|
196
|
+
// Measured on a Windows box whose Ethernet is DomainAuthenticated: the rule
|
|
197
|
+
// was pasted, the panel went on saying there was none — correctly — and a
|
|
198
|
+
// second paste made a second rule that did nothing either. Domain and
|
|
199
|
+
// Private together, so a laptop that goes home keeps working there too.
|
|
200
|
+
// Public is never added: a Public network gets the category line above.
|
|
201
|
+
const profile = name === "Domain" ? "Domain,Private" : "Private";
|
|
202
|
+
// AND A RULE THAT IS ALREADY THERE IS WIDENED, NOT JOINED BY ANOTHER. The
|
|
203
|
+
// probe only returns rules for this program, so an inbound allow rule in
|
|
204
|
+
// `rules` is one of ours on the wrong profile, and a new rule beside it is
|
|
205
|
+
// the duplicate this was reported with.
|
|
206
|
+
const ours = (Array.isArray(rules) ? rules : [])
|
|
207
|
+
.some(r => r?.enabled && String(r.direction).toLowerCase() === "inbound" && String(r.action).toLowerCase() === "allow");
|
|
208
|
+
steps.push(ours
|
|
209
|
+
? `Get-NetFirewallApplicationFilter -Program "${exePath}" | Get-NetFirewallRule | Set-NetFirewallRule -Profile ${profile}`
|
|
210
|
+
: "New-NetFirewallRule -DisplayName \"ccdeck (local network)\" -Direction Inbound"
|
|
211
|
+
+ ` -Program "${exePath}" -Action Allow -Profile ${profile}`);
|
|
183
212
|
return steps;
|
|
184
213
|
}
|
|
185
214
|
|
|
@@ -463,6 +492,16 @@ export function reachability({ platform, probe, aliases = [], exePath = "", inbo
|
|
|
463
492
|
if (ruleCovers(probe.rules, name)) {
|
|
464
493
|
return { blocked: false, why: "rule present", category, alias: net?.alias ?? "" };
|
|
465
494
|
}
|
|
495
|
+
// COULD NOT READ THE RULES, SO CANNOT SAY THERE IS NONE. An empty list here
|
|
496
|
+
// is "Access is denied", not "no rule" (see PROBE_PS), so claiming a missing
|
|
497
|
+
// rule would be a guess dressed as a read — and the command that guess hands
|
|
498
|
+
// over adds a duplicate rule on a machine that may already be covered. On a
|
|
499
|
+
// Public network the fix is the category, which was read reliably, so that
|
|
500
|
+
// verdict still stands; anywhere else the honest answer is the measurement,
|
|
501
|
+
// which `refreshReach` falls back to when this returns null.
|
|
502
|
+
// Only an EMPTY list is ambiguous — a rule in hand was plainly readable,
|
|
503
|
+
// whatever the flag says.
|
|
504
|
+
if (!probe.rulesReadable && probe.rules.length === 0 && name !== "Public") return null;
|
|
466
505
|
return {
|
|
467
506
|
blocked: true,
|
|
468
507
|
why: "no inbound rule",
|
|
@@ -477,8 +516,10 @@ export function reachability({ platform, probe, aliases = [], exePath = "", inbo
|
|
|
477
516
|
// workaround below obvious rather than magic.
|
|
478
517
|
text: name === "Public"
|
|
479
518
|
? "This network is set to Public, and Windows drops what other decks send. They can still hear this deck — that is why one of them may already show it."
|
|
480
|
-
:
|
|
481
|
-
|
|
519
|
+
: name === "Domain" && probe.rules.length
|
|
520
|
+
? "Windows lets this deck in on Private networks only, and this one is a company (Domain) network, so it drops what other decks send. They can still hear it — that is why one of them may already show this machine."
|
|
521
|
+
: "Windows has no inbound rule for this deck, so it drops what other decks send. They can still hear it — that is why one of them may already show this machine.",
|
|
522
|
+
steps: fixSteps({ category, alias: net?.alias ?? "", exePath, rules: probe.rules }),
|
|
482
523
|
};
|
|
483
524
|
}
|
|
484
525
|
|