agent-dag 1.33.47 → 1.33.49
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-DXPYK_Sr.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-CF2yG9oG.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.33.
|
|
3
|
+
"version": "1.33.49",
|
|
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,7 +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
|
+
import { killTree, spawnSpec } from "./exec.mjs";
|
|
19
19
|
|
|
20
20
|
const CACHE_MS = 120_000; // 2 min — modal is manual-open; cheap to keep warm
|
|
21
21
|
const TIMEOUT_MS = 90_000;
|
|
@@ -188,7 +188,9 @@ async function runCcusage(args) {
|
|
|
188
188
|
});
|
|
189
189
|
let out = "", err = "";
|
|
190
190
|
const timer = setTimeout(() => {
|
|
191
|
-
child.
|
|
191
|
+
// The npx fallback runs through a shell, so on Windows `child` is cmd.exe
|
|
192
|
+
// and npx is a grandchild that a plain kill would leave downloading.
|
|
193
|
+
killTree(child);
|
|
192
194
|
reject(new Error("ccusage timed out"));
|
|
193
195
|
}, TIMEOUT_MS);
|
|
194
196
|
child.stdout.on("data", d => { out += d; });
|
|
@@ -158,7 +158,27 @@ export function extractLoginUrl(text) {
|
|
|
158
158
|
|
|
159
159
|
// The prompt the CLI blocks on. Matched rather than assumed: writing a code
|
|
160
160
|
// into a child that is not asking for one would send it somewhere unknown.
|
|
161
|
-
const CODE_PROMPT = /paste code here/
|
|
161
|
+
const CODE_PROMPT = /paste code here/gi;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* How many times one line of output asks for the code.
|
|
165
|
+
*
|
|
166
|
+
* Per LINE, not per delivery. The prompt ends without a newline, so exec.mjs
|
|
167
|
+
* re-offers the same unterminated line on every chunk as it grows — and both
|
|
168
|
+
* streams share that buffer, so a progress dot, a `\r` spinner frame on stderr,
|
|
169
|
+
* or simply the prompt split across two writes all arrive as the ask again,
|
|
170
|
+
* a few characters longer. Counting an ask whenever the text merely CHANGED, as
|
|
171
|
+
* this used to, turned any of those into a phantom second prompt, and a second
|
|
172
|
+
* prompt is exactly how submitLoginCode hears "that code was rejected": a
|
|
173
|
+
* login the CLI was busy completing came back as `code_rejected`, with the
|
|
174
|
+
* account unregistered and the live credentials left switched.
|
|
175
|
+
*
|
|
176
|
+
* A real re-ask writes the prompt again — on a fresh line, or after a `\r`
|
|
177
|
+
* redraw of this one — so it shows up as another match, not as a longer tail.
|
|
178
|
+
*/
|
|
179
|
+
export function countCodePrompts(line) {
|
|
180
|
+
return (stripTerminalEscapes(line).match(CODE_PROMPT) ?? []).length;
|
|
181
|
+
}
|
|
162
182
|
|
|
163
183
|
/**
|
|
164
184
|
* The whole login, as one object, because the browser talks to it twice: once
|
|
@@ -219,23 +239,27 @@ export async function startLogin({ email } = {}) {
|
|
|
219
239
|
// exit, it just asks again, so waiting for exit would hang for the whole
|
|
220
240
|
// five-minute window on a typo.
|
|
221
241
|
prompts: 0,
|
|
222
|
-
lastPromptText: "",
|
|
223
242
|
};
|
|
224
243
|
const flow = _login;
|
|
225
244
|
|
|
226
|
-
|
|
245
|
+
// How many asks the line currently being written has already contributed.
|
|
246
|
+
// The prompt is an unterminated line, re-delivered as it grows, so the same
|
|
247
|
+
// ask arrives over and over — and once more, whole, when something else
|
|
248
|
+
// finally ends the line.
|
|
249
|
+
let countedOnLine = 0;
|
|
250
|
+
|
|
251
|
+
child.onLine((line, partial) => {
|
|
227
252
|
if (!flow.url) {
|
|
228
253
|
const url = extractLoginUrl(line);
|
|
229
254
|
if (url) { flow.url = url; flow.state = "awaiting_code"; }
|
|
230
255
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (clean !== flow.lastPromptText) { flow.prompts += 1; flow.lastPromptText = clean; }
|
|
236
|
-
} else if (clean.trim()) {
|
|
237
|
-
flow.lastPromptText = "";
|
|
256
|
+
const asks = countCodePrompts(line);
|
|
257
|
+
if (asks > countedOnLine) {
|
|
258
|
+
flow.prompts += asks - countedOnLine;
|
|
259
|
+
countedOnLine = asks;
|
|
238
260
|
}
|
|
261
|
+
// A newline ended that line; whatever comes next is a new one.
|
|
262
|
+
if (!partial) countedOnLine = 0;
|
|
239
263
|
});
|
|
240
264
|
// The child dying before the code was accepted is a failure of the login, not
|
|
241
265
|
// of the deck; say so rather than leaving the dialog spinning.
|
package/src/server/exec.mjs
CHANGED
|
@@ -78,6 +78,47 @@ export function viaCmd(file, args) {
|
|
|
78
78
|
export const spawnSpec = (file, args, platform = process.platform) =>
|
|
79
79
|
isBatch(file, platform) ? viaCmd(file, args) : { file, args, opts: {} };
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Stop a child AND everything it started.
|
|
83
|
+
*
|
|
84
|
+
* On POSIX the child is the tool, so a signal to it is the whole job and this
|
|
85
|
+
* is exactly the `child.kill()` it replaces. On Windows a .cmd or .bat runs
|
|
86
|
+
* THROUGH cmd.exe (see viaCmd), so the tool — node, for the claude and npm
|
|
87
|
+
* shims — is a grandchild, and a kill is one TerminateProcess against the
|
|
88
|
+
* wrapper. Windows terminates no descendants and libuv's job object lets them
|
|
89
|
+
* break away, so the wrapper vanished and the work carried on: every cancelled
|
|
90
|
+
* or expired `claude auth login` left a node.exe blocked forever on the stdin
|
|
91
|
+
* pipe this process holds, and because that node.exe kept the inherited stdout
|
|
92
|
+
* handle open, the wrapper's 'close' never arrived either — the run that was
|
|
93
|
+
* reported dead never settled.
|
|
94
|
+
*
|
|
95
|
+
* `taskkill /T` is the descendant walk Windows does have, and `/F` is what
|
|
96
|
+
* stops a console app that is not pumping its message queue. Taking it from
|
|
97
|
+
* System32 rather than from PATH matters here: PATH is the user's, and this is
|
|
98
|
+
* the program we hand a pid to kill. If it cannot run at all, the plain kill
|
|
99
|
+
* still happens, which is no worse than before.
|
|
100
|
+
*
|
|
101
|
+
* A process group would be the tidier answer and is the wrong one on Windows:
|
|
102
|
+
* `detached` there only means CREATE_NEW_PROCESS_GROUP, and Node cannot signal
|
|
103
|
+
* a group — `process.kill(-pid)` is POSIX-only.
|
|
104
|
+
*/
|
|
105
|
+
export function killTree(child, signal) {
|
|
106
|
+
const plain = () => { try { child?.kill(signal); } catch { /* already gone */ } };
|
|
107
|
+
if (process.platform !== "win32" || !child?.pid) return plain();
|
|
108
|
+
try {
|
|
109
|
+
const root = process.env.SystemRoot || process.env.systemroot;
|
|
110
|
+
const exe = root ? `${root}\\System32\\taskkill.exe` : "taskkill";
|
|
111
|
+
const killer = spawn(exe, ["/pid", String(child.pid), "/T", "/F"], {
|
|
112
|
+
stdio: "ignore", windowsHide: true,
|
|
113
|
+
});
|
|
114
|
+
killer.on("error", plain);
|
|
115
|
+
killer.on("exit", (code) => { if (code !== 0) plain(); });
|
|
116
|
+
killer.unref?.();
|
|
117
|
+
} catch {
|
|
118
|
+
plain();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
81
122
|
// Reasons to try the next candidate spelling rather than give up. EINVAL and
|
|
82
123
|
// UNKNOWN show up on Windows for a file that exists but cannot be executed the
|
|
83
124
|
// way it was asked for; both mean "not this one", not "no such tool".
|
|
@@ -124,7 +165,15 @@ export function run(cmd, args, { timeout = 20_000, maxBuffer = 4 << 20 } = {}) {
|
|
|
124
165
|
? viaCmd(raw, args)
|
|
125
166
|
: { file: raw, args, opts: {} };
|
|
126
167
|
|
|
168
|
+
// execFile's own `timeout` ends with one signal to the process it
|
|
169
|
+
// started, which for a batch candidate is the cmd.exe wrapper: the
|
|
170
|
+
// deadline was reported as enforced while the tool underneath went on
|
|
171
|
+
// running. A batch candidate is timed here instead, on the whole tree.
|
|
172
|
+
const tree = isBatch(raw);
|
|
173
|
+
let timer = null, timedOut = false;
|
|
174
|
+
|
|
127
175
|
const done = (err, stdout, stderr) => {
|
|
176
|
+
clearTimeout(timer);
|
|
128
177
|
// cmd.exe's "is not recognized" counts as "not this spelling" too, and
|
|
129
178
|
// it arrives as a normal non-zero exit rather than a spawn error.
|
|
130
179
|
const missing = Boolean(err) && looksMissing(`${stderr ?? ""}\n${stdout ?? ""}`);
|
|
@@ -135,14 +184,21 @@ export function run(cmd, args, { timeout = 20_000, maxBuffer = 4 << 20 } = {}) {
|
|
|
135
184
|
// A tool cmd.exe could not find is missing, not "exited 1" — callers
|
|
136
185
|
// key their message off this.
|
|
137
186
|
code: missing ? "ENOENT" : (err?.code ?? 0),
|
|
138
|
-
killed
|
|
187
|
+
// Our own tree kill is not `killed` as far as execFile can tell, so
|
|
188
|
+
// a timeout reports the same either way.
|
|
189
|
+
killed: Boolean(err?.killed) || timedOut,
|
|
139
190
|
stdout: String(stdout ?? ""),
|
|
140
191
|
stderr: String(stderr ?? ""),
|
|
141
192
|
});
|
|
142
193
|
};
|
|
143
194
|
|
|
144
195
|
try {
|
|
145
|
-
execFile(file, argv,
|
|
196
|
+
const cp = execFile(file, argv,
|
|
197
|
+
{ timeout: tree ? 0 : timeout, shell: false, windowsHide: true, maxBuffer, ...opts }, done);
|
|
198
|
+
if (tree) {
|
|
199
|
+
timer = setTimeout(() => { timedOut = true; killTree(cp); }, timeout);
|
|
200
|
+
timer.unref?.();
|
|
201
|
+
}
|
|
146
202
|
} catch (err) {
|
|
147
203
|
// Synchronous throw — the EINVAL case. Same handling as a callback
|
|
148
204
|
// error; letting it propagate here is what crashed the server, because
|
|
@@ -210,7 +266,7 @@ export function runInteractive(cmd, args, { timeout = 300_000, maxOutput = 256 <
|
|
|
210
266
|
|
|
211
267
|
const timer = setTimeout(() => {
|
|
212
268
|
timedOut = true;
|
|
213
|
-
|
|
269
|
+
killTree(child);
|
|
214
270
|
}, timeout);
|
|
215
271
|
timer.unref?.();
|
|
216
272
|
|
|
@@ -287,9 +343,11 @@ export function runInteractive(cmd, args, { timeout = 300_000, maxOutput = 256 <
|
|
|
287
343
|
end() {
|
|
288
344
|
try { child?.stdin?.end(); } catch { /* already closed */ }
|
|
289
345
|
},
|
|
346
|
+
/** Stop the run. On Windows that means the tool under the cmd.exe wrapper
|
|
347
|
+
* too — see killTree; a cancelled sign-in used to leave it running. */
|
|
290
348
|
kill() {
|
|
291
349
|
killed = true;
|
|
292
|
-
|
|
350
|
+
killTree(child);
|
|
293
351
|
},
|
|
294
352
|
onLine(cb) { lineSubs.push(cb); },
|
|
295
353
|
done,
|
|
@@ -25,6 +25,7 @@ import { accessSync, constants as FS, existsSync, mkdirSync, readFileSync, write
|
|
|
25
25
|
import { spawn } from "node:child_process";
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
27
|
import { dirname, join, resolve } from "node:path";
|
|
28
|
+
import { killTree } from "./exec.mjs";
|
|
28
29
|
|
|
29
30
|
// Once an hour, not once a day.
|
|
30
31
|
//
|
|
@@ -395,7 +396,10 @@ export function startUpgrade({ pkgRoot, name = "agents-deck" }) {
|
|
|
395
396
|
child.stdout.on("data", d => keepTail(String(d)));
|
|
396
397
|
child.stderr.on("data", d => keepTail(String(d)));
|
|
397
398
|
|
|
398
|
-
|
|
399
|
+
// npm is a .cmd shim on Windows and is therefore spawned through a shell, so
|
|
400
|
+
// `child` is cmd.exe and npm itself is a grandchild — a plain kill would
|
|
401
|
+
// report the install as timed out while it carried on writing to node_modules.
|
|
402
|
+
const timer = setTimeout(() => killTree(child), INSTALL_TIMEOUT_MS);
|
|
399
403
|
timer.unref?.();
|
|
400
404
|
|
|
401
405
|
child.on("error", (e) => {
|