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.
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
process.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
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": {
|
package/src/server/ccusage.mjs
CHANGED
|
@@ -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
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
122
|
-
|
|
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", () => {});
|