agent-dag 1.35.0 → 1.35.1
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/README.md +19 -0
- package/bin/deck.js +61 -7
- package/dist/web/assets/index-BWurW9Rz.js +70 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/src/server/claude-dir.mjs +191 -7
- package/src/server/index.mjs +20 -1
- package/src/server/quota.mjs +7 -39
- package/dist/web/assets/index-CMZYhML1.js +0 -70
package/dist/web/index.html
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
|
|
41
41
|
})();
|
|
42
42
|
</script>
|
|
43
|
-
<script type="module" crossorigin src="/assets/index-
|
|
43
|
+
<script type="module" crossorigin src="/assets/index-BWurW9Rz.js"></script>
|
|
44
44
|
<link rel="stylesheet" crossorigin href="/assets/index-CRPobBZf.css">
|
|
45
45
|
</head>
|
|
46
46
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.35.
|
|
3
|
+
"version": "1.35.1",
|
|
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": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Where Claude Code
|
|
2
|
-
//
|
|
1
|
+
// Where Claude Code lives on this machine: its configuration directory, its
|
|
2
|
+
// binary, and the question of whether it is here at all.
|
|
3
3
|
//
|
|
4
4
|
// CLAUDE_CONFIG_DIR relocates that directory wholesale; it is a replacement for
|
|
5
5
|
// ~/.claude, not an overlay, so on a machine where it is set there is nothing in
|
|
@@ -12,11 +12,195 @@
|
|
|
12
12
|
// reads another. hook/hook.js repeats the rule inline rather than importing it:
|
|
13
13
|
// it is copied out of the package and run standalone by the host CLI, so it has
|
|
14
14
|
// no way back to this module, but it has to resolve the same directory.
|
|
15
|
+
//
|
|
16
|
+
// The binary list and the presence test moved in beside it for the same reason.
|
|
17
|
+
// The list used to sit in quota.mjs under the name quotaClaudeCandidates, where
|
|
18
|
+
// it read as a detail of one poller; it is nothing of the kind. It is the answer
|
|
19
|
+
// to "where does the `claude` command live", and the deck now asks that question
|
|
20
|
+
// at boot, before any quota exists, to decide whether this is a Claude machine
|
|
21
|
+
// at all. One list, one caller-visible name, no second spelling to drift.
|
|
22
|
+
import { existsSync } from "node:fs";
|
|
15
23
|
import { homedir } from "node:os";
|
|
16
|
-
import { join, resolve } from "node:path";
|
|
24
|
+
import { join, resolve, posix as posixPath, win32 as winPath } from "node:path";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Absolute path of the Claude Code config dir: $CLAUDE_CONFIG_DIR or ~/.claude.
|
|
28
|
+
*
|
|
29
|
+
* The environment and the home directory are parameters purely so callers that
|
|
30
|
+
* are already working from an injected environment can stay consistent with it;
|
|
31
|
+
* every caller in the deck passes nothing and gets the real machine's answer.
|
|
32
|
+
*/
|
|
33
|
+
export function claudeConfigDir(env = process.env, home = homedir()) {
|
|
34
|
+
const override = env.CLAUDE_CONFIG_DIR?.trim();
|
|
35
|
+
return override ? resolve(override) : join(home, ".claude");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every place the `claude` CLI is known to live, in the order to try them.
|
|
40
|
+
*
|
|
41
|
+
* Pure, and the platform, environment and home directory are parameters, so the
|
|
42
|
+
* Windows list can be checked from a Mac — which is the only way this list stays
|
|
43
|
+
* right, since it exists entirely for machines the author is not sitting at.
|
|
44
|
+
*/
|
|
45
|
+
export function claudeCliCandidates(platform = process.platform, env = process.env, home = homedir()) {
|
|
46
|
+
// The path flavour follows the PLATFORM ARGUMENT, not the host: node's `join`
|
|
47
|
+
// would emit forward slashes when the Windows list is built on a Mac.
|
|
48
|
+
const { join } = platform === "win32" ? winPath : posixPath;
|
|
49
|
+
if (platform !== "win32") {
|
|
50
|
+
return [
|
|
51
|
+
"claude",
|
|
52
|
+
join(home, ".local", "bin", "claude"),
|
|
53
|
+
"/usr/local/bin/claude",
|
|
54
|
+
"/opt/homebrew/bin/claude",
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
return [
|
|
58
|
+
// The native installer, which ships a bare claude.exe and NO .cmd shim. It
|
|
59
|
+
// was the one install this branch could not reach: the npm path below does
|
|
60
|
+
// not exist on such a machine, and the bare-name fallback used to be spelled
|
|
61
|
+
// `claude.cmd`, which cmd.exe cannot resolve to an .exe — PATHEXT supplies a
|
|
62
|
+
// missing extension, it never substitutes one that is already there.
|
|
63
|
+
join(home, ".local", "bin", "claude.exe"),
|
|
64
|
+
// `npm i -g @anthropic-ai/claude-code`. npm's global prefix is %APPDATA%\npm
|
|
65
|
+
// — Roaming rather than Local, deliberately, since it follows the user
|
|
66
|
+
// between machines — and APPDATA is read from the environment because a
|
|
67
|
+
// roaming profile puts it on a network share, not under the home directory.
|
|
68
|
+
join(env.APPDATA || join(home, "AppData", "Roaming"), "npm", "claude.cmd"),
|
|
69
|
+
// Last resort: the bare name, which cmd.exe resolves through PATH + PATHEXT
|
|
70
|
+
// and so finds claude.exe and claude.cmd alike.
|
|
71
|
+
"claude",
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The names Claude Code itself writes inside its config dir, and the deck never
|
|
77
|
+
* does. Presence of any one of them is proof Claude Code has actually run here.
|
|
78
|
+
*
|
|
79
|
+
* The exclusions are the entire point. `settings.json` and `agent-dag/` are OURS
|
|
80
|
+
* — installHooks writes the first and keepDiscovery creates the second — so a
|
|
81
|
+
* test that accepted them would answer "Claude is installed" on any machine this
|
|
82
|
+
* deck had ever been started on, including the Codex-only one this whole check
|
|
83
|
+
* exists to recognise. That is not a hypothetical: every Codex-only machine that
|
|
84
|
+
* ran an earlier ccdeck already has both sitting in a ~/.claude the deck created
|
|
85
|
+
* for itself.
|
|
86
|
+
*
|
|
87
|
+
* `.credentials.json` is in the list but cannot carry it alone: macOS keeps the
|
|
88
|
+
* OAuth token in the Keychain and writes no such file, so on the platform where
|
|
89
|
+
* a credentials test is most tempting it is always negative (see #360). It earns
|
|
90
|
+
* its place as one more way to say yes on Linux and Windows, never as the test.
|
|
91
|
+
*/
|
|
92
|
+
const CLAUDE_USE_MARKERS = [
|
|
93
|
+
"projects", // one directory per cwd, written from the first session on
|
|
94
|
+
"history.jsonl", // the prompt history, appended to on the first prompt
|
|
95
|
+
"statsig", // written at first launch, before any session completes
|
|
96
|
+
"todos",
|
|
97
|
+
"shell-snapshots",
|
|
98
|
+
"ide",
|
|
99
|
+
"plugins",
|
|
100
|
+
".credentials.json", // Linux + Windows OAuth store; absent on macOS by design
|
|
101
|
+
".claude.json", // Claude Code's own state file, when the config dir moved
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether the `claude` binary is on this machine — on PATH, or in one of the
|
|
106
|
+
* places its installers are known to put it.
|
|
107
|
+
*
|
|
108
|
+
* `run()` in exec.mjs can afford to hand a bare name to spawn and let the OS
|
|
109
|
+
* resolve it. This cannot: it has to answer without launching anything. Boot is
|
|
110
|
+
* the wrong place to spend a `claude --version` child process (quota.mjs
|
|
111
|
+
* measured those at ~3.0s each), and a spawn probe would also have to decide
|
|
112
|
+
* what a non-zero exit means, which is a question with no good answer. So the
|
|
113
|
+
* PATH walk is done by hand, with PATHEXT applied on Windows exactly as cmd.exe
|
|
114
|
+
* would — a `claude.cmd` shim from npm and a bare `claude.exe` from the native
|
|
115
|
+
* installer both have to count, and neither is spelled `claude`.
|
|
116
|
+
*/
|
|
117
|
+
export function claudeCliOnDisk({
|
|
118
|
+
platform = process.platform,
|
|
119
|
+
env = process.env,
|
|
120
|
+
home = homedir(),
|
|
121
|
+
exists = existsSync,
|
|
122
|
+
} = {}) {
|
|
123
|
+
const path = platform === "win32" ? winPath : posixPath;
|
|
124
|
+
const bare = [];
|
|
125
|
+
for (const candidate of claudeCliCandidates(platform, env, home)) {
|
|
126
|
+
// A full path is worth a single stat; a bare name means "ask PATH", which
|
|
127
|
+
// is the walk below. Same split quotaClaudeBin makes, for the same reason.
|
|
128
|
+
if (candidate.includes(path.sep)) { if (exists(candidate)) return true; }
|
|
129
|
+
else bare.push(candidate);
|
|
130
|
+
}
|
|
131
|
+
if (bare.length === 0) return false;
|
|
132
|
+
|
|
133
|
+
// process.env is case-insensitive on Windows, but an injected plain object in
|
|
134
|
+
// a test is not, and %Path% is how the variable is actually spelled there.
|
|
135
|
+
const rawPath = env.PATH ?? env.Path ?? env.path ?? "";
|
|
136
|
+
// The empty extension stays in the list: a Git-Bash or WSL-style shim on
|
|
137
|
+
// Windows can be an extensionless file, and on POSIX it is the only entry.
|
|
138
|
+
const exts = platform === "win32"
|
|
139
|
+
? ["", ...(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map(e => e.trim()).filter(Boolean)]
|
|
140
|
+
: [""];
|
|
141
|
+
|
|
142
|
+
for (const entry of String(rawPath).split(path.delimiter)) {
|
|
143
|
+
// Windows PATH entries are routinely quoted, and an empty entry means the
|
|
144
|
+
// current directory — which is not a place to go looking for a CLI.
|
|
145
|
+
const dir = entry.trim().replace(/^"+|"+$/g, "");
|
|
146
|
+
if (dir === "") continue;
|
|
147
|
+
for (const name of bare) {
|
|
148
|
+
for (const ext of exts) if (exists(path.join(dir, name + ext))) return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
17
153
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Whether this machine has Claude Code at all — the mirror of hasCodexInstalled().
|
|
156
|
+
*
|
|
157
|
+
* README offers "Claude Code CLI or OpenAI Codex CLI (or both)", and until now
|
|
158
|
+
* nothing anywhere asked which. A Codex-only machine got a Python
|
|
159
|
+
* account-switcher installed for a CLI it does not have, an accounts panel open
|
|
160
|
+
* on first run, and a banner telling it to sign into that CLI (#402).
|
|
161
|
+
*
|
|
162
|
+
* WHY TWO KINDS OF EVIDENCE, OR'd. Neither half is sufficient on its own:
|
|
163
|
+
*
|
|
164
|
+
* - The binary alone misses the user whose `claude` lives somewhere no list
|
|
165
|
+
* knows (nvm, mise, volta, a corporate wrapper) and whose deck was launched
|
|
166
|
+
* from a desktop shortcut with a PATH that never sourced their shell rc.
|
|
167
|
+
* That user has run Claude Code for months; the config dir proves it.
|
|
168
|
+
* - The config dir alone misses the machine where Claude Code is installed and
|
|
169
|
+
* has never been launched, which is exactly the moment somebody installs
|
|
170
|
+
* both CLIs and starts the deck first. It also cannot be the test on its own
|
|
171
|
+
* for the opposite reason: THE DECK CREATES THAT DIRECTORY ITSELF. Hence
|
|
172
|
+
* CLAUDE_USE_MARKERS above, which is a list of things only Claude Code puts
|
|
173
|
+
* there.
|
|
174
|
+
*
|
|
175
|
+
* WHY NOT CREDENTIALS. A credentials file is not a presence test on macOS at
|
|
176
|
+
* all — the token is in the Keychain and no file exists (#360) — so a machine
|
|
177
|
+
* with Claude Code, signed in and in daily use, would read as Codex-only.
|
|
178
|
+
*
|
|
179
|
+
* WHY NOT "a Claude session was seen". It is the most direct evidence there is,
|
|
180
|
+
* and it arrives far too late: the decision this answers is made at boot, before
|
|
181
|
+
* the server is listening, and a machine whose first session has not started yet
|
|
182
|
+
* would install nothing and show no panel until a restart.
|
|
183
|
+
*
|
|
184
|
+
* The bias is deliberate and one-way. A false yes leaves a Claude-only surface
|
|
185
|
+
* on a Codex machine — today's bug, visible, and the user can pass --no-claude.
|
|
186
|
+
* A false no takes the hooks away from somebody who has Claude Code, which is a
|
|
187
|
+
* deck that stays empty forever. So every check here is generous, the banner
|
|
188
|
+
* says out loud which way it went, and --claude overrides it.
|
|
189
|
+
*/
|
|
190
|
+
export function hasClaudeInstalled({
|
|
191
|
+
platform = process.platform,
|
|
192
|
+
env = process.env,
|
|
193
|
+
home = homedir(),
|
|
194
|
+
configDir = claudeConfigDir(env, home),
|
|
195
|
+
exists = existsSync,
|
|
196
|
+
} = {}) {
|
|
197
|
+
if (claudeCliOnDisk({ platform, env, home, exists })) return true;
|
|
198
|
+
const path = platform === "win32" ? winPath : posixPath;
|
|
199
|
+
for (const marker of CLAUDE_USE_MARKERS) {
|
|
200
|
+
if (exists(path.join(configDir, marker))) return true;
|
|
201
|
+
}
|
|
202
|
+
// ~/.claude.json is Claude Code's own state file on every install that never
|
|
203
|
+
// moved the config dir, and it sits BESIDE the home directory rather than
|
|
204
|
+
// inside ~/.claude — so the loop above cannot reach it there.
|
|
205
|
+
return exists(path.join(home, ".claude.json"));
|
|
22
206
|
}
|
package/src/server/index.mjs
CHANGED
|
@@ -1918,6 +1918,21 @@ async function handleSoundHookSet(req, res) {
|
|
|
1918
1918
|
// can read before a single event has arrived.
|
|
1919
1919
|
let _workspace = "";
|
|
1920
1920
|
|
|
1921
|
+
// Which CLIs this deck is actually watching. Decided in bin/deck.js — from
|
|
1922
|
+
// whether each one is on the machine, and from --claude/--no-claude and
|
|
1923
|
+
// --codex/--no-codex — and passed in here, because the browser had no way to
|
|
1924
|
+
// learn it and so drew both sides of the UI on every machine.
|
|
1925
|
+
//
|
|
1926
|
+
// That is the whole of #402 and its mirror. A Codex-only machine got the
|
|
1927
|
+
// accounts panel open on first run, telling it to sign into a CLI it does not
|
|
1928
|
+
// have; a Claude-only machine permanently carried "Quota unavailable. / Run
|
|
1929
|
+
// codex login to authenticate." Same missing fact, two directions.
|
|
1930
|
+
//
|
|
1931
|
+
// Defaults are both true, which is what an older deck effectively reported by
|
|
1932
|
+
// saying nothing — and the browser reads a missing field as "could not say" and
|
|
1933
|
+
// shows both, so the two agree.
|
|
1934
|
+
let _providers = { claude: true, codex: true };
|
|
1935
|
+
|
|
1921
1936
|
/**
|
|
1922
1937
|
* The one spelling of `--workspace` everything downstream compares against.
|
|
1923
1938
|
* Empty — including a value that is nothing but spaces — stays empty, which is
|
|
@@ -1957,6 +1972,7 @@ function handleHealth(_req, res) {
|
|
|
1957
1972
|
clients: sseClients.size,
|
|
1958
1973
|
uptimeMs: Math.round(process.uptime() * 1000),
|
|
1959
1974
|
workspace: _workspace,
|
|
1975
|
+
providers: _providers,
|
|
1960
1976
|
});
|
|
1961
1977
|
}
|
|
1962
1978
|
|
|
@@ -2343,10 +2359,13 @@ let _onRestart = null;
|
|
|
2343
2359
|
// ask; the second ask must not re-enter the shutdown.
|
|
2344
2360
|
let _restarting = false;
|
|
2345
2361
|
|
|
2346
|
-
export async function startServer({ port = 4317, host = "127.0.0.1", persist = null, portRange = [4318, 4400], workspace = "", codex = true, onRestart = null } = {}) {
|
|
2362
|
+
export async function startServer({ port = 4317, host = "127.0.0.1", persist = null, portRange = [4318, 4400], workspace = "", codex = true, claude = true, onRestart = null } = {}) {
|
|
2347
2363
|
_onRestart = typeof onRestart === "function" ? onRestart : null;
|
|
2348
2364
|
_canRestart = _onRestart != null && persist != null;
|
|
2349
2365
|
_workspace = typeof workspace === "string" ? workspace : "";
|
|
2366
|
+
// `!== false` rather than a cast: a caller that omits the field means "yes",
|
|
2367
|
+
// which is how every embedder that predates this option keeps working.
|
|
2368
|
+
_providers = { claude: claude !== false, codex: codex !== false };
|
|
2350
2369
|
const removed = await sweepStaleDiscovery();
|
|
2351
2370
|
if (removed > 0) console.log(` swept ${removed} stale discovery file(s)`);
|
|
2352
2371
|
if (persist) {
|
package/src/server/quota.mjs
CHANGED
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
// 2 and 3 are rate-floored (SELF_POLL_MS) and gated behind the same 429
|
|
29
29
|
// cooldown; 1 is not, because it is a local file read.
|
|
30
30
|
import { activeAccountUsage, requestCollection } from "./claude-accounts.mjs";
|
|
31
|
-
import { claudeConfigDir } from "./claude-dir.mjs";
|
|
31
|
+
import { claudeCliCandidates, claudeConfigDir } from "./claude-dir.mjs";
|
|
32
32
|
import { run } from "./exec.mjs";
|
|
33
33
|
import { existsSync } from "node:fs";
|
|
34
34
|
import { readFile } from "node:fs/promises";
|
|
35
|
-
import { join
|
|
35
|
+
import { join } from "node:path";
|
|
36
36
|
import { homedir } from "node:os";
|
|
37
37
|
import { PRODUCT } from "./brand.mjs";
|
|
38
38
|
|
|
@@ -334,42 +334,10 @@ function parseUsageText(raw) {
|
|
|
334
334
|
return Object.keys(result).length > 0 ? result : null;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
* Windows list can be checked from a Mac — which is the only way this list stays
|
|
342
|
-
* right, since it exists entirely for machines the author is not sitting at.
|
|
343
|
-
*/
|
|
344
|
-
export function quotaClaudeCandidates(platform = process.platform, env = process.env, home = homedir()) {
|
|
345
|
-
// The path flavour follows the PLATFORM ARGUMENT, not the host: node's `join`
|
|
346
|
-
// would emit forward slashes when the Windows list is built on a Mac.
|
|
347
|
-
const { join } = platform === "win32" ? winPath : posixPath;
|
|
348
|
-
if (platform !== "win32") {
|
|
349
|
-
return [
|
|
350
|
-
"claude",
|
|
351
|
-
join(home, ".local", "bin", "claude"),
|
|
352
|
-
"/usr/local/bin/claude",
|
|
353
|
-
"/opt/homebrew/bin/claude",
|
|
354
|
-
];
|
|
355
|
-
}
|
|
356
|
-
return [
|
|
357
|
-
// The native installer, which ships a bare claude.exe and NO .cmd shim. It
|
|
358
|
-
// was the one install this branch could not reach: the npm path below does
|
|
359
|
-
// not exist on such a machine, and the bare-name fallback used to be spelled
|
|
360
|
-
// `claude.cmd`, which cmd.exe cannot resolve to an .exe — PATHEXT supplies a
|
|
361
|
-
// missing extension, it never substitutes one that is already there.
|
|
362
|
-
join(home, ".local", "bin", "claude.exe"),
|
|
363
|
-
// `npm i -g @anthropic-ai/claude-code`. npm's global prefix is %APPDATA%\npm
|
|
364
|
-
// — Roaming rather than Local, deliberately, since it follows the user
|
|
365
|
-
// between machines — and APPDATA is read from the environment because a
|
|
366
|
-
// roaming profile puts it on a network share, not under the home directory.
|
|
367
|
-
join(env.APPDATA || join(home, "AppData", "Roaming"), "npm", "claude.cmd"),
|
|
368
|
-
// Last resort: the bare name, which cmd.exe resolves through PATH + PATHEXT
|
|
369
|
-
// and so finds claude.exe and claude.cmd alike.
|
|
370
|
-
"claude",
|
|
371
|
-
];
|
|
372
|
-
}
|
|
337
|
+
// Where the `claude` CLI can be. The list moved to claude-dir.mjs, which is the
|
|
338
|
+
// module that owns every "where does Claude Code live" answer the deck has —
|
|
339
|
+
// the config dir was already there, and the boot-time presence check that reads
|
|
340
|
+
// this same list had no business importing a quota poller to get at it.
|
|
373
341
|
|
|
374
342
|
/** Which `claude` to run for `--print /usage`: the first candidate that exists.
|
|
375
343
|
*
|
|
@@ -396,7 +364,7 @@ export function quotaClaudeBin(platform = process.platform, env = process.env,
|
|
|
396
364
|
// A bare name is left to spawn's own PATH lookup (and, on Windows, to the
|
|
397
365
|
// PATHEXT walk exec.mjs does by hand); a full path is only worth naming when
|
|
398
366
|
// it is actually there.
|
|
399
|
-
return
|
|
367
|
+
return claudeCliCandidates(platform, env, home)
|
|
400
368
|
.find(c => !c.includes(sep) || exists(c)) ?? "claude";
|
|
401
369
|
}
|
|
402
370
|
|