agent-dag 1.33.41 → 1.33.43

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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>agents-deck</title>
7
7
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
8
- <script type="module" crossorigin src="/assets/index-DNFdr9c6.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CkZD1gvK.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-CF2yG9oG.css">
10
10
  </head>
11
11
  <body>
package/hook/hook.js CHANGED
@@ -11,16 +11,6 @@ const path = require("path");
11
11
  const http = require("http");
12
12
  const os = require("os");
13
13
 
14
- // Hard cap so a stuck server can never wedge the host CLI.
15
- setTimeout(() => process.exit(0), 1500);
16
-
17
- // The deck reads the Claude quota by running `claude --print /usage`, which is
18
- // a full Claude Code invocation and therefore fires these hooks. Reporting it
19
- // drew a session onto the canvas for every quota poll — no prompt, no tools,
20
- // a few seconds long — so the deck filled up with its own measurements. The
21
- // probe sets this in the environment and hooks inherit it.
22
- if (process.env.AGENTS_DECK_INTERNAL === "1") process.exit(0);
23
-
24
14
  // Single shared discovery dir — Claude Code and Codex CLI both register here
25
15
  // via the installer. Lets one running agent-dag server receive both providers.
26
16
  //
@@ -48,79 +38,144 @@ function normPath(p) {
48
38
  return r;
49
39
  }
50
40
 
41
+ /**
42
+ * Does this platform's filesystem treat two spellings that differ only in case
43
+ * as the same directory? Exported for tests: the platform is a parameter so
44
+ * both answers can be checked from either kind of machine.
45
+ *
46
+ * Windows always does, and macOS does by default (APFS and HFS+ are formatted
47
+ * case-insensitive unless the user deliberately chose otherwise). Linux does
48
+ * not, and folding case there would be a bug of its own: /srv/Proj and
49
+ * /srv/proj are two real directories, and a deck scoped to one must not be
50
+ * handed the other's events.
51
+ *
52
+ * A case-sensitive macOS volume is therefore over-matched. That is the safe
53
+ * direction to be wrong in — the cost is a deck that also sees a sibling tree
54
+ * it was not scoped to, against the cost of the default configuration seeing
55
+ * nothing at all.
56
+ */
57
+ const foldsCase = (platform = process.platform) =>
58
+ platform === "win32" || platform === "darwin";
59
+
60
+ /**
61
+ * Is `cwd` the workspace directory or somewhere inside it?
62
+ *
63
+ * Both sides arrive already resolved, but resolved is not the same as
64
+ * comparable. Neither path.resolve nor the JS fs.realpathSync canonicalizes
65
+ * character case, so the drive letter and every component keep whatever case
66
+ * the process that reported them happened to use — `c:\proj` from one shell,
67
+ * `C:\Proj` from another, for one directory. A raw === / startsWith then says
68
+ * "not in the workspace", the hook posts to nobody, and a scoped deck stays
69
+ * empty with no error printed anywhere. Re-resolving through the platform's
70
+ * own path flavour also settles separators and a trailing one, so
71
+ * `C:/proj/` and `C:\proj` compare equal too.
72
+ *
73
+ * The platform is a parameter, following spawnSpec/isBatch in
74
+ * src/server/exec.mjs, so the Windows rule is testable from a POSIX machine.
75
+ */
76
+ function cwdInWorkspace(cwd, workspace, platform = process.platform) {
77
+ const p = platform === "win32" ? path.win32 : path.posix;
78
+ const fold = s => (foldsCase(platform) ? s.toLowerCase() : s);
79
+ const a = fold(p.resolve(cwd));
80
+ const b = fold(p.resolve(workspace));
81
+ if (a === b) return true;
82
+ // A root ("C:\", "/") already ends in the separator; appending a second one
83
+ // would match nothing.
84
+ return a.startsWith(b.endsWith(p.sep) ? b : b + p.sep);
85
+ }
86
+
51
87
  function isAlive(pid) {
52
88
  try { process.kill(pid, 0); return true; }
53
89
  catch (e) { return e && e.code === "EPERM"; }
54
90
  }
55
91
 
56
- let input = "";
57
- process.stdin.setEncoding("utf8");
58
- process.stdin.on("data", c => { input += c; });
59
- process.stdin.on("end", () => {
60
- let parsed;
61
- try { parsed = JSON.parse(input); } catch { return process.exit(0); }
62
- const cwd = parsed && parsed.cwd;
63
- if (!cwd) return process.exit(0);
64
-
65
- // Stamp provider so the server / reducer can branch on it without
66
- // re-sniffing payload shape.
67
- if (parsed && typeof parsed === "object" && !parsed.provider) {
68
- parsed.provider = PROVIDER;
69
- }
70
- const taggedInput = JSON.stringify(parsed);
71
-
72
- const resolvedCwd = normPath(cwd);
73
-
74
- let files;
75
- try {
76
- files = fs.readdirSync(DIR).filter(f => f.endsWith(".json"));
77
- } catch { return process.exit(0); }
78
- if (!files.length) return process.exit(0);
79
-
80
- const matches = [];
81
- for (const file of files) {
82
- let d;
83
- try { d = JSON.parse(fs.readFileSync(path.join(DIR, file), "utf8")); } catch { continue; }
84
- if (typeof d.workspace !== "string" || !d.pid || !d.port) continue;
85
-
86
- if (!isAlive(d.pid)) {
87
- try { fs.unlinkSync(path.join(DIR, file)); } catch {}
88
- continue;
92
+ function main() {
93
+ // Hard cap so a stuck server can never wedge the host CLI.
94
+ setTimeout(() => process.exit(0), 1500);
95
+
96
+ // The deck reads the Claude quota by running `claude --print /usage`, which is
97
+ // a full Claude Code invocation and therefore fires these hooks. Reporting it
98
+ // drew a session onto the canvas for every quota poll — no prompt, no tools,
99
+ // a few seconds long — so the deck filled up with its own measurements. The
100
+ // probe sets this in the environment and hooks inherit it.
101
+ if (process.env.AGENTS_DECK_INTERNAL === "1") process.exit(0);
102
+
103
+ let input = "";
104
+ process.stdin.setEncoding("utf8");
105
+ process.stdin.on("data", c => { input += c; });
106
+ process.stdin.on("end", () => {
107
+ let parsed;
108
+ try { parsed = JSON.parse(input); } catch { return process.exit(0); }
109
+ const cwd = parsed && parsed.cwd;
110
+ if (!cwd) return process.exit(0);
111
+
112
+ // Stamp provider so the server / reducer can branch on it without
113
+ // re-sniffing payload shape.
114
+ if (parsed && typeof parsed === "object" && !parsed.provider) {
115
+ parsed.provider = PROVIDER;
89
116
  }
90
-
91
- if (d.workspace === "") {
92
- matches.push({ d, wsLen: 0 });
93
- continue;
117
+ const taggedInput = JSON.stringify(parsed);
118
+
119
+ const resolvedCwd = normPath(cwd);
120
+
121
+ let files;
122
+ try {
123
+ files = fs.readdirSync(DIR).filter(f => f.endsWith(".json"));
124
+ } catch { return process.exit(0); }
125
+ if (!files.length) return process.exit(0);
126
+
127
+ const matches = [];
128
+ for (const file of files) {
129
+ let d;
130
+ try { d = JSON.parse(fs.readFileSync(path.join(DIR, file), "utf8")); } catch { continue; }
131
+ if (typeof d.workspace !== "string" || !d.pid || !d.port) continue;
132
+
133
+ if (!isAlive(d.pid)) {
134
+ try { fs.unlinkSync(path.join(DIR, file)); } catch {}
135
+ continue;
136
+ }
137
+
138
+ if (d.workspace === "") {
139
+ matches.push({ d, wsLen: 0 });
140
+ continue;
141
+ }
142
+ const ws = normPath(d.workspace);
143
+ if (cwdInWorkspace(resolvedCwd, ws)) {
144
+ matches.push({ d, wsLen: ws.length });
145
+ }
94
146
  }
95
- const ws = normPath(d.workspace);
96
- if (resolvedCwd === ws || resolvedCwd.startsWith(ws + path.sep)) {
97
- matches.push({ d, wsLen: ws.length });
147
+
148
+ if (!matches.length) return process.exit(0);
149
+
150
+ matches.sort((a, b) => b.wsLen - a.wsLen);
151
+ const bestLen = matches[0].wsLen;
152
+ const targets = matches.filter(m => m.wsLen === bestLen);
153
+
154
+ let pending = targets.length;
155
+ const done = () => { if (--pending <= 0) process.exit(0); };
156
+
157
+ for (const { d } of targets) {
158
+ let settled = false;
159
+ const finish = () => { if (settled) return; settled = true; done(); };
160
+ const req = http.request({
161
+ hostname: "127.0.0.1",
162
+ port: d.port,
163
+ path: "/api/event",
164
+ method: "POST",
165
+ headers: { "Content-Type": "application/json" },
166
+ timeout: 1000,
167
+ }, res => { res.resume(); res.on("end", finish); });
168
+ req.on("error", finish);
169
+ req.on("timeout", () => req.destroy());
170
+ req.write(taggedInput);
171
+ req.end();
98
172
  }
99
- }
173
+ });
174
+ }
100
175
 
101
- if (!matches.length) return process.exit(0);
102
-
103
- matches.sort((a, b) => b.wsLen - a.wsLen);
104
- const bestLen = matches[0].wsLen;
105
- const targets = matches.filter(m => m.wsLen === bestLen);
106
-
107
- let pending = targets.length;
108
- const done = () => { if (--pending <= 0) process.exit(0); };
109
-
110
- for (const { d } of targets) {
111
- let settled = false;
112
- const finish = () => { if (settled) return; settled = true; done(); };
113
- const req = http.request({
114
- hostname: "127.0.0.1",
115
- port: d.port,
116
- path: "/api/event",
117
- method: "POST",
118
- headers: { "Content-Type": "application/json" },
119
- timeout: 1000,
120
- }, res => { res.resume(); res.on("end", finish); });
121
- req.on("error", finish);
122
- req.on("timeout", () => req.destroy());
123
- req.write(taggedInput);
124
- req.end();
125
- }
126
- });
176
+ // The host CLI always runs this file as the process entry point — the command
177
+ // the installer writes is `"<node>" "<...>/hook.js" --provider <name>`. Under a
178
+ // require() it exports the matching rule and starts nothing, which is what lets
179
+ // the rule be tested without a 1.5s exit timer in the test runner.
180
+ module.exports = { cwdInWorkspace, foldsCase };
181
+ if (require.main === module) main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.33.41",
3
+ "version": "1.33.43",
4
4
  "description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@ import { spawn, spawnSync } from "node:child_process";
15
15
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
16
16
  import path from "node:path";
17
17
  import os from "node:os";
18
+ import { spawnSpec } from "./exec.mjs";
18
19
 
19
20
  const CACHE_MS = 120_000; // 2 min — modal is manual-open; cheap to keep warm
20
21
  const TIMEOUT_MS = 90_000;
@@ -27,12 +28,26 @@ const MARKER = path.join(CACHE_DIR, ".last-update-check");
27
28
 
28
29
  const _cache = new Map(); // key `${since}|${until}` → { result, at }
29
30
 
30
- // npm is a .cmd shim on Windows, which spawn can only launch through a shell.
31
- // Everywhere else shell:false — with a shell, Node warns that arguments are
32
- // concatenated rather than escaped, and the warning lands in our startup
33
- // banner now that these run at boot.
34
- const NPM = process.platform === "win32" ? "npm.cmd" : "npm";
35
- const NPM_SHELL = process.platform === "win32";
31
+ // npm is a .cmd shim on Windows, which spawn can only launch through cmd.exe.
32
+ // `shell: true` is the tempting way to get there and the wrong one: Node then
33
+ // joins file and args with single spaces and no quoting, so on a profile like
34
+ // C:\Users\John Smith the `--prefix <CACHE_DIR>` below arrived as
35
+ // `--prefix C:\Users\John` plus a bogus package spec `Smith\.agents-deck\...`,
36
+ // npm exited non-zero, and the managed install never materialised. spawnSpec
37
+ // routes the .cmd through cmd.exe with every argument quoted, and hands back
38
+ // the argument vector untouched everywhere else.
39
+ function npmSpec(args, platform = process.platform) {
40
+ return spawnSpec(platform === "win32" ? "npm.cmd" : "npm", args, platform);
41
+ }
42
+
43
+ /**
44
+ * What `spawn` gets for `npm install ccusage@<spec>`.
45
+ * Exported for tests: the platform is a parameter so the Windows command line
46
+ * can be checked from any OS.
47
+ */
48
+ export const installSpec = (spec = "latest", platform = process.platform) =>
49
+ npmSpec(["install", `ccusage@${spec}`, "--prefix", CACHE_DIR,
50
+ "--no-save", "--no-audit", "--no-fund", "--loglevel", "error"], platform);
36
51
 
37
52
  let _installing = null; // Promise guard so concurrent calls share one install
38
53
  let _checkedThisRun = false; // only kick the daily check once per process boot
@@ -68,11 +83,11 @@ function resolveEntry() {
68
83
  // the first-run cold path (we must have a binary before we can answer).
69
84
  function installSync(spec = "latest") {
70
85
  mkdirSync(CACHE_DIR, { recursive: true });
86
+ const { file, args, opts } = installSpec(spec);
71
87
  const r = spawnSync(
72
- NPM,
73
- ["install", `ccusage@${spec}`, "--prefix", CACHE_DIR,
74
- "--no-save", "--no-audit", "--no-fund", "--loglevel", "error"],
75
- { shell: NPM_SHELL, windowsHide: true, timeout: INSTALL_TIMEOUT_MS, encoding: "utf8" },
88
+ file,
89
+ args,
90
+ { windowsHide: true, timeout: INSTALL_TIMEOUT_MS, encoding: "utf8", ...opts },
76
91
  );
77
92
  if (r.status !== 0) {
78
93
  throw new Error(`npm install ccusage failed: ${(r.stderr || "").trim() || r.status}`);
@@ -83,11 +98,11 @@ function installSync(spec = "latest") {
83
98
  function installAsync(spec = "latest") {
84
99
  try {
85
100
  mkdirSync(CACHE_DIR, { recursive: true });
101
+ const { file, args, opts } = installSpec(spec);
86
102
  const child = spawn(
87
- NPM,
88
- ["install", `ccusage@${spec}`, "--prefix", CACHE_DIR,
89
- "--no-save", "--no-audit", "--no-fund", "--loglevel", "error"],
90
- { shell: NPM_SHELL, windowsHide: true, detached: false, stdio: "ignore" },
103
+ file,
104
+ args,
105
+ { windowsHide: true, detached: false, stdio: "ignore", ...opts },
91
106
  );
92
107
  child.on("error", () => {});
93
108
  child.unref?.();
@@ -118,8 +133,8 @@ function maybeBackgroundUpdate(installedVersion) {
118
133
  _checkedThisRun = true;
119
134
  touchMarker();
120
135
  try {
121
- const child = spawn(NPM, ["view", "ccusage", "version"],
122
- { shell: NPM_SHELL, windowsHide: true });
136
+ const { file, args, opts } = npmSpec(["view", "ccusage", "version"]);
137
+ const child = spawn(file, args, { windowsHide: true, ...opts });
123
138
  let out = "";
124
139
  child.stdout.on("data", d => { out += d; });
125
140
  child.on("error", () => {});