agent-dag 1.33.2 → 1.33.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/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-VG3Xh-3i.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-hRjhJVfb.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.3",
|
|
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": {
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
import { readFile } from "node:fs/promises";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { join } from "node:path";
|
|
22
|
-
import { run, runDetached, runInteractive } from "./exec.mjs";
|
|
22
|
+
import { looksMissing, run, runDetached, runInteractive } from "./exec.mjs";
|
|
23
23
|
import { invalidateClaudeAccountsCache } from "./claude-accounts.mjs";
|
|
24
|
+
import { cswapBin } from "./cswap-install.mjs";
|
|
24
25
|
|
|
25
26
|
// An OAuth code is short-lived at the source; there is no point holding a child
|
|
26
27
|
// open longer than a user would plausibly take to fetch one.
|
|
@@ -65,9 +66,13 @@ export function withStoreLock(fn) {
|
|
|
65
66
|
|
|
66
67
|
// ── shared helpers ───────────────────────────────────────────────────────────
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// cswapBin comes from cswap-install.mjs, which searches the places uv and pipx
|
|
70
|
+
// actually install to. This module used to answer the bare name "cswap"
|
|
71
|
+
// instead, which is fine on a Mac where ~/.local/bin is usually on PATH and
|
|
72
|
+
// wrong on Windows where it is usually not: every mutation here — share,
|
|
73
|
+
// import, remove, rename, reorder — failed with cmd.exe's "is not recognized",
|
|
74
|
+
// while the read-only half of the panel worked, because it was already using
|
|
75
|
+
// the resolver. Reported from Windows on 2026-08-14.
|
|
71
76
|
async function claudeBin() {
|
|
72
77
|
return process.env.AGENTS_DECK_CLAUDE ?? "claude";
|
|
73
78
|
}
|
|
@@ -224,7 +229,7 @@ export async function startLogin({ email } = {}) {
|
|
|
224
229
|
if (flow !== _login) return;
|
|
225
230
|
if (flow.state === "awaiting_url" || flow.state === "awaiting_code") {
|
|
226
231
|
flow.state = "failed";
|
|
227
|
-
flow.error = r.timedOut ? "the sign-in window expired" :
|
|
232
|
+
flow.error = r.timedOut ? "the sign-in window expired" : failureText(r, "claude auth login") || "sign-in ended without a code";
|
|
228
233
|
}
|
|
229
234
|
});
|
|
230
235
|
|
|
@@ -274,7 +279,7 @@ export async function submitLoginCode(code) {
|
|
|
274
279
|
}
|
|
275
280
|
if (!r.ok) {
|
|
276
281
|
flow.state = "failed";
|
|
277
|
-
flow.error = r.timedOut ? "the sign-in window expired" :
|
|
282
|
+
flow.error = r.timedOut ? "the sign-in window expired" : failureText(r, "claude auth login") || "the code was not accepted";
|
|
278
283
|
return { ok: false, reason: "login_failed", ...loginState() };
|
|
279
284
|
}
|
|
280
285
|
|
|
@@ -372,7 +377,7 @@ export async function shareAccount(num) {
|
|
|
372
377
|
if (!Number.isInteger(n) || n < 1 || n > 999) return { ok: false, reason: "bad_account" };
|
|
373
378
|
const r = await run(await cswapBin(), ["export", "-", "--account", String(n)], { timeout: CSWAP_TIMEOUT_MS });
|
|
374
379
|
if (!r.ok || !r.stdout.trim()) {
|
|
375
|
-
return { ok: false, reason: "export_failed", detail:
|
|
380
|
+
return { ok: false, reason: "export_failed", detail: failureText(r, "cswap export") };
|
|
376
381
|
}
|
|
377
382
|
return { ok: true, blob: wrapShare(r.stdout), expiresAt: Date.now() + SHARE_TTL_MS };
|
|
378
383
|
}
|
|
@@ -390,7 +395,7 @@ export async function importAccount(blob) {
|
|
|
390
395
|
endStdin(child);
|
|
391
396
|
|
|
392
397
|
const r = await child.done;
|
|
393
|
-
if (!r.ok) return { ok: false, reason: "import_failed", detail:
|
|
398
|
+
if (!r.ok) return { ok: false, reason: "import_failed", detail: failureText(r, "cswap import") };
|
|
394
399
|
|
|
395
400
|
const after = await readStore();
|
|
396
401
|
const slot = newSlot(before, after);
|
|
@@ -429,7 +434,7 @@ export async function removeAccount(num) {
|
|
|
429
434
|
});
|
|
430
435
|
const r = await child.done;
|
|
431
436
|
invalidateClaudeAccountsCache();
|
|
432
|
-
if (!r.ok) return { ok: false, reason: "remove_failed", detail:
|
|
437
|
+
if (!r.ok) return { ok: false, reason: "remove_failed", detail: failureText(r, "cswap remove") };
|
|
433
438
|
// Exit 0 without the prompt means cswap declined for its own reason — a
|
|
434
439
|
// live session on that account, most often — and printed why.
|
|
435
440
|
if (!answered) return { ok: false, reason: "not_confirmed", detail: firstUseful(r.stdout || r.stderr) };
|
|
@@ -447,7 +452,7 @@ export async function setAlias(num, alias) {
|
|
|
447
452
|
invalidateClaudeAccountsCache();
|
|
448
453
|
return r.ok
|
|
449
454
|
? { ok: true, output: firstUseful(r.stdout) }
|
|
450
|
-
: { ok: false, reason: "alias_failed", detail:
|
|
455
|
+
: { ok: false, reason: "alias_failed", detail: failureText(r, "cswap alias") };
|
|
451
456
|
});
|
|
452
457
|
}
|
|
453
458
|
|
|
@@ -460,12 +465,31 @@ export async function moveAccount(num, slot) {
|
|
|
460
465
|
invalidateClaudeAccountsCache();
|
|
461
466
|
return r.ok
|
|
462
467
|
? { ok: true, output: firstUseful(r.stdout) }
|
|
463
|
-
: { ok: false, reason: "move_failed", detail:
|
|
468
|
+
: { ok: false, reason: "move_failed", detail: failureText(r, "cswap move") };
|
|
464
469
|
});
|
|
465
470
|
}
|
|
466
471
|
|
|
467
472
|
// ── text ─────────────────────────────────────────────────────────────────────
|
|
468
473
|
|
|
474
|
+
/**
|
|
475
|
+
* What went wrong, in a sentence the user can act on.
|
|
476
|
+
*
|
|
477
|
+
* The missing-tool case is singled out because its own output is useless: on
|
|
478
|
+
* Windows it is cmd.exe's two-line "is not recognized …/operable program or
|
|
479
|
+
* batch file.", and `firstUseful` — which takes the LAST line, correctly for
|
|
480
|
+
* every other CLI — leaves the second half on screen by itself.
|
|
481
|
+
*/
|
|
482
|
+
export function failureText(r, what = "cswap") {
|
|
483
|
+
const out = `${r?.stderr ?? ""}\n${r?.stdout ?? ""}`;
|
|
484
|
+
const tool = String(what).split(" ")[0];
|
|
485
|
+
if (r?.code === "ENOENT" || looksMissing(out)) {
|
|
486
|
+
return tool === "claude"
|
|
487
|
+
? "the claude CLI could not be run: not on PATH. Set AGENTS_DECK_CLAUDE to its full path."
|
|
488
|
+
: "cswap could not be run: not on PATH, and not in the places uv and pipx install to. Set AGENTS_DECK_CSWAP to its full path.";
|
|
489
|
+
}
|
|
490
|
+
return firstUseful(out) || `${what} exited ${r?.code}`;
|
|
491
|
+
}
|
|
492
|
+
|
|
469
493
|
/** The line worth showing a user out of a CLI's output. */
|
|
470
494
|
export function firstUseful(text) {
|
|
471
495
|
const lines = stripTerminalEscapes(text)
|
|
@@ -488,7 +512,7 @@ export function addFailureText(r) {
|
|
|
488
512
|
if (/keychain/i.test(text)) {
|
|
489
513
|
return `${text} — start agents-deck from a Terminal window rather than a background service.`;
|
|
490
514
|
}
|
|
491
|
-
return
|
|
515
|
+
return failureText(r, "cswap add");
|
|
492
516
|
}
|
|
493
517
|
|
|
494
518
|
async function waitFor(get, timeoutMs, stepMs = 100) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { run, runDetached } from "./exec.mjs";
|
|
11
11
|
import { bootstrapUv, existingBootstrappedUv } from "./uv-bootstrap.mjs";
|
|
12
12
|
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { join } from "node:path";
|
|
13
|
+
import { join, posix as posixPath, win32 as winPath } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
|
|
16
16
|
const INSTALL_TIMEOUT_MS = 180_000; // uv resolves + builds a Python env
|
|
@@ -34,20 +34,49 @@ const MARKER = join(homedir(), ".agents-deck", ".cswap-update-check");
|
|
|
34
34
|
* Re-resolved when a lookup fails so an install during this process is picked
|
|
35
35
|
* up without a restart.
|
|
36
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* Every place an installer is known to leave cswap. Pure, and the platform is a
|
|
39
|
+
* parameter, so the Windows list can be checked from a Mac — which is the only
|
|
40
|
+
* way this list stays right, since it exists entirely for machines the author is
|
|
41
|
+
* not sitting at.
|
|
42
|
+
*/
|
|
43
|
+
export function cswapCandidates(platform = process.platform, env = process.env, home = homedir()) {
|
|
44
|
+
// The path flavour follows the PLATFORM ARGUMENT, not the host: node's `join`
|
|
45
|
+
// would emit forward slashes when this is exercised from a Mac, which is both
|
|
46
|
+
// wrong for the caller and invisible in a test.
|
|
47
|
+
const { join } = platform === "win32" ? winPath : posixPath;
|
|
48
|
+
const exe = platform === "win32" ? "cswap.exe" : "cswap";
|
|
49
|
+
const dirs = [];
|
|
50
|
+
// Explicit configuration first: someone who set these means them.
|
|
51
|
+
if (env.UV_TOOL_BIN_DIR) dirs.push(env.UV_TOOL_BIN_DIR);
|
|
52
|
+
if (env.XDG_BIN_HOME) dirs.push(env.XDG_BIN_HOME);
|
|
53
|
+
// Where `uv tool install` and `pipx install` put executables, everywhere.
|
|
54
|
+
dirs.push(join(home, ".local", "bin"));
|
|
55
|
+
if (platform === "win32") {
|
|
56
|
+
// pipx before 1.5, and any `pip install --user`. APPDATA is respected when
|
|
57
|
+
// set because a roaming profile moves it off the home directory.
|
|
58
|
+
dirs.push(join(env.APPDATA || join(home, "AppData", "Roaming"), "Python", "Scripts"));
|
|
59
|
+
dirs.push(join(env.LOCALAPPDATA || join(home, "AppData", "Local"), "Programs", "Python", "Scripts"));
|
|
60
|
+
dirs.push(join(home, "scoop", "shims"));
|
|
61
|
+
} else {
|
|
62
|
+
dirs.push(join(home, ".pyenv", "shims"));
|
|
63
|
+
// uv keeps the tool's own venv here and only symlinks into ~/.local/bin; if
|
|
64
|
+
// that link was never made, this is still a working executable.
|
|
65
|
+
dirs.push(join(home, ".local", "share", "uv", "tools", "claude-swap", "bin"));
|
|
66
|
+
dirs.push("/opt/homebrew/bin", "/usr/local/bin");
|
|
67
|
+
}
|
|
68
|
+
return dirs.map(d => join(d, exe));
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
let _bin = null;
|
|
38
72
|
export async function cswapBin() {
|
|
73
|
+
// An explicit path wins over everything and is never cached away — someone
|
|
74
|
+
// debugging a bad resolution needs it to take effect immediately.
|
|
75
|
+
if (process.env.AGENTS_DECK_CSWAP) return process.env.AGENTS_DECK_CSWAP;
|
|
39
76
|
if (_bin) return _bin;
|
|
40
77
|
if ((await run("cswap", ["--version"], { timeout: 8_000 })).ok) return (_bin = "cswap");
|
|
41
78
|
|
|
42
|
-
const
|
|
43
|
-
const candidates = [
|
|
44
|
-
join(homedir(), ".local", "bin", exe),
|
|
45
|
-
// pipx before 1.5 on Windows, and any pip --user install.
|
|
46
|
-
process.platform === "win32"
|
|
47
|
-
? join(homedir(), "AppData", "Roaming", "Python", "Scripts", exe)
|
|
48
|
-
: join(homedir(), ".pyenv", "shims", exe),
|
|
49
|
-
];
|
|
50
|
-
for (const c of candidates) {
|
|
79
|
+
for (const c of cswapCandidates()) {
|
|
51
80
|
if (existsSync(c) && (await run(c, ["--version"], { timeout: 8_000 })).ok) return (_bin = c);
|
|
52
81
|
}
|
|
53
82
|
return "cswap"; // not found; leave the bare name so errors read sensibly
|
package/src/server/exec.mjs
CHANGED
|
@@ -66,6 +66,26 @@ export const tryNext = (err) =>
|
|
|
66
66
|
Boolean(err) && (err.code === "ENOENT" || err.code === "EACCES" ||
|
|
67
67
|
err.code === "EINVAL" || err.code === "UNKNOWN");
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* cmd.exe's way of saying ENOENT.
|
|
71
|
+
*
|
|
72
|
+
* A .cmd or .bat candidate is launched THROUGH cmd.exe, and cmd.exe exists — so
|
|
73
|
+
* a missing tool is not a spawn error at all. It is a healthy shell exiting 1
|
|
74
|
+
* after printing two lines:
|
|
75
|
+
*
|
|
76
|
+
* 'cswap' is not recognized as an internal or external command,
|
|
77
|
+
* operable program or batch file.
|
|
78
|
+
*
|
|
79
|
+
* Read as a real failure, that stops the candidate loop early AND puts the
|
|
80
|
+
* second line — on its own, meaningless — in front of the user. Reported from
|
|
81
|
+
* Windows on 2026-08-14: the accounts panel said only "operable program or
|
|
82
|
+
* batch file." when sharing an account.
|
|
83
|
+
*/
|
|
84
|
+
export const looksMissing = (text) =>
|
|
85
|
+
/is not recognized as an internal or external command/i.test(String(text ?? "")) ||
|
|
86
|
+
/operable program or batch file/i.test(String(text ?? "")) ||
|
|
87
|
+
/The system cannot find the (?:path|file) specified/i.test(String(text ?? ""));
|
|
88
|
+
|
|
69
89
|
/**
|
|
70
90
|
* Run a command and collect its output. Never rejects, and never throws —
|
|
71
91
|
* failures come back as `{ ok: false }`, because every caller here is a poll or
|
|
@@ -86,11 +106,16 @@ export function run(cmd, args, { timeout = 20_000, maxBuffer = 4 << 20 } = {}) {
|
|
|
86
106
|
: { file: raw, args, opts: {} };
|
|
87
107
|
|
|
88
108
|
const done = (err, stdout, stderr) => {
|
|
89
|
-
|
|
109
|
+
// cmd.exe's "is not recognized" counts as "not this spelling" too, and
|
|
110
|
+
// it arrives as a normal non-zero exit rather than a spawn error.
|
|
111
|
+
const missing = Boolean(err) && looksMissing(`${stderr ?? ""}\n${stdout ?? ""}`);
|
|
112
|
+
if (err && (tryNext(err) || missing) && i + 1 < tries.length) return attempt(i + 1);
|
|
90
113
|
if (!err) resolved.set(cmd, raw);
|
|
91
114
|
resolve({
|
|
92
115
|
ok: !err,
|
|
93
|
-
|
|
116
|
+
// A tool cmd.exe could not find is missing, not "exited 1" — callers
|
|
117
|
+
// key their message off this.
|
|
118
|
+
code: missing ? "ENOENT" : (err?.code ?? 0),
|
|
94
119
|
killed: Boolean(err?.killed),
|
|
95
120
|
stdout: String(stdout ?? ""),
|
|
96
121
|
stderr: String(stderr ?? ""),
|
|
@@ -191,7 +216,19 @@ export function runInteractive(cmd, args, { timeout = 300_000, maxOutput = 256 <
|
|
|
191
216
|
const keep = (buf, text) => (buf + text).slice(-maxOutput);
|
|
192
217
|
child.stdout?.on("data", (d) => { const t = String(d); stdout = keep(stdout, t); emitLines(t); });
|
|
193
218
|
child.stderr?.on("data", (d) => { const t = String(d); stderr = keep(stderr, t); emitLines(t); });
|
|
194
|
-
child.on("close", (code) =>
|
|
219
|
+
child.on("close", (code) => {
|
|
220
|
+
// Same cmd.exe case as in `run`: exit 1 with "is not recognized" means
|
|
221
|
+
// this spelling does not exist, not that the tool failed.
|
|
222
|
+
if (code !== 0 && looksMissing(`${stderr}\n${stdout}`)) {
|
|
223
|
+
if (i + 1 < tries.length) {
|
|
224
|
+
stdout = ""; stderr = ""; pending = "";
|
|
225
|
+
child = null;
|
|
226
|
+
return attempt(i + 1);
|
|
227
|
+
}
|
|
228
|
+
return finish(-1, { code: "ENOENT" });
|
|
229
|
+
}
|
|
230
|
+
finish(code ?? -1, null);
|
|
231
|
+
});
|
|
195
232
|
};
|
|
196
233
|
attempt(0);
|
|
197
234
|
|