ccdeck 3.26.1 → 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.
@@ -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-CO8B_qBe.js"></script>
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.1",
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": {
@@ -45,7 +45,8 @@
45
45
  "3.23.3",
46
46
  "3.23.4",
47
47
  "3.26.0",
48
- "3.26.1"
48
+ "3.26.1",
49
+ "3.26.3"
49
50
  ],
50
51
  "3.25.0": [
51
52
  {
@@ -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. Three reads, none of them privileged —
35
- * measured: `Get-NetConnectionProfile`, `Get-NetFirewallProfile` and
36
- * `Get-NetFirewallApplicationFilter` all answer for an ordinary user, while
37
- * `Get-NetFirewallPortFilter` returns "Access is denied". So the rule this
38
- * looks for is scoped to a PROGRAM rather than to a port, which is also the
39
- * only kind that keeps working: the sync listener's port is ephemeral and is
40
- * a different number after every restart. */
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
- "$rules = @(Get-NetFirewallApplicationFilter | 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 } })",
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
  }
@@ -479,6 +492,16 @@ export function reachability({ platform, probe, aliases = [], exePath = "", inbo
479
492
  if (ruleCovers(probe.rules, name)) {
480
493
  return { blocked: false, why: "rule present", category, alias: net?.alias ?? "" };
481
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;
482
505
  return {
483
506
  blocked: true,
484
507
  why: "no inbound rule",