agent-dag 3.22.1 → 3.22.4

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.
Files changed (70) hide show
  1. package/README.md +6 -477
  2. package/package.json +14 -48
  3. package/shim.js +107 -0
  4. package/LICENSE +0 -661
  5. package/LICENSING.md +0 -82
  6. package/THIRD_PARTY_NOTICES.md +0 -395
  7. package/bin/agent-dag.js +0 -626
  8. package/bin/deck.js +0 -1805
  9. package/dist/web/assets/index-CJYsv0lr.css +0 -1
  10. package/dist/web/assets/index-Ifm23DDC.js +0 -270
  11. package/dist/web/index.html +0 -49
  12. package/hook/hook.js +0 -542
  13. package/release-notes.json +0 -398
  14. package/src/server/activity.mjs +0 -52
  15. package/src/server/agent-activity.mjs +0 -522
  16. package/src/server/args.mjs +0 -183
  17. package/src/server/auto-update.mjs +0 -79
  18. package/src/server/block-notify.mjs +0 -173
  19. package/src/server/boot-deadline.mjs +0 -127
  20. package/src/server/brand.mjs +0 -16
  21. package/src/server/browser-history.mjs +0 -497
  22. package/src/server/browser-presence.mjs +0 -211
  23. package/src/server/browser-profiles.mjs +0 -279
  24. package/src/server/browser-react.mjs +0 -284
  25. package/src/server/browser-watch-store.mjs +0 -350
  26. package/src/server/browser-watch.mjs +0 -905
  27. package/src/server/ccusage.mjs +0 -1168
  28. package/src/server/claude-accounts.mjs +0 -951
  29. package/src/server/claude-dir.mjs +0 -213
  30. package/src/server/codex-auth.mjs +0 -388
  31. package/src/server/codex-dir.mjs +0 -171
  32. package/src/server/codex-quota.mjs +0 -449
  33. package/src/server/codex-usage.mjs +0 -512
  34. package/src/server/cswap-admin.mjs +0 -1562
  35. package/src/server/cswap-auto.mjs +0 -658
  36. package/src/server/cswap-install.mjs +0 -641
  37. package/src/server/deck-home.mjs +0 -243
  38. package/src/server/deck-prefs.mjs +0 -301
  39. package/src/server/deck-probe.mjs +0 -111
  40. package/src/server/detach.mjs +0 -244
  41. package/src/server/exec.mjs +0 -996
  42. package/src/server/global-install.mjs +0 -67
  43. package/src/server/hwmonitor.mjs +0 -56
  44. package/src/server/index.mjs +0 -6043
  45. package/src/server/installer.mjs +0 -912
  46. package/src/server/invoked-as.mjs +0 -144
  47. package/src/server/lan-about.mjs +0 -119
  48. package/src/server/lan-engine.mjs +0 -952
  49. package/src/server/lan-reach.mjs +0 -256
  50. package/src/server/lan-socket.mjs +0 -682
  51. package/src/server/lan-sync.mjs +0 -941
  52. package/src/server/lhm-parse.mjs +0 -91
  53. package/src/server/log-tail.mjs +0 -139
  54. package/src/server/log-writer.mjs +0 -322
  55. package/src/server/login-service.mjs +0 -473
  56. package/src/server/macmon.mjs +0 -310
  57. package/src/server/npx.mjs +0 -264
  58. package/src/server/open-url.mjs +0 -242
  59. package/src/server/presence.mjs +0 -40
  60. package/src/server/quota.mjs +0 -792
  61. package/src/server/relay-guard.mjs +0 -507
  62. package/src/server/reset-label.mjs +0 -78
  63. package/src/server/retire-sound-hook.mjs +0 -349
  64. package/src/server/running-deck.mjs +0 -234
  65. package/src/server/self-update.mjs +0 -1380
  66. package/src/server/stop-deck.mjs +0 -171
  67. package/src/server/supervisor.mjs +0 -392
  68. package/src/server/system-metrics.mjs +0 -1825
  69. package/src/server/term.mjs +0 -686
  70. package/src/server/uv-bootstrap.mjs +0 -337
@@ -1,337 +0,0 @@
1
- // Last-resort acquisition of `uv`, so claude-swap can be installed on a machine
2
- // with no Python tooling at all.
3
- //
4
- // The alternative everyone reaches for is `curl -LsSf https://astral.sh/uv/
5
- // install.sh | sh`, which executes whatever that URL happens to serve, with the
6
- // user's privileges, and drops files into their global tool path. This does the
7
- // narrower thing instead: fetch one named release artifact from Astral's own
8
- // GitHub releases, check it against the SHA-256 that release publishes beside
9
- // it, and unpack it inside ~/.agents-deck. Nothing is executed until it has
10
- // been verified, nothing lands outside a directory the deck already owns — the
11
- // staging directory included, which is what #551 was about — and deleting that
12
- // directory undoes all of it.
13
- //
14
- // It is still a network fetch of an executable, so it only happens when there
15
- // is no other way: uv, pipx, and python -m pipx have all already been ruled out
16
- // by the caller. AGENTS_DECK_NO_INSTALL=1 disables it along with everything else.
17
- import { createHash } from "node:crypto";
18
- import { mkdir, mkdtemp, writeFile, rm, chmod, readdir, copyFile, open } from "node:fs/promises";
19
- import { existsSync } from "node:fs";
20
- import { join } from "node:path";
21
- import { homedir } from "node:os";
22
- import { run } from "./exec.mjs";
23
- // The same rename used to replace settings.json, for the same reason: on
24
- // Windows a rename over an existing file fails outright while any other process
25
- // holds the target open, and a virus scanner or the search indexer opens a
26
- // freshly written executable the instant it appears. Sharing one implementation
27
- // keeps the retryable-error list from drifting apart between the two callers.
28
- import { renameWithRetry } from "./installer.mjs";
29
-
30
- const TOOL_DIR = join(homedir(), ".agents-deck", "tools");
31
- const UV_DIR = join(TOOL_DIR, "uv");
32
-
33
- const RELEASE_API = "https://api.github.com/repos/astral-sh/uv/releases/latest";
34
- const DOWNLOAD_BASE = "https://github.com/astral-sh/uv/releases/download";
35
-
36
- // Used when the releases API cannot be reached — rate limiting is per-IP and
37
- // unauthenticated, so this is a normal outcome rather than an error. Any
38
- // reasonably recent uv can install claude-swap; it self-updates later.
39
- const FALLBACK_VERSION = "0.12.3";
40
-
41
- const DOWNLOAD_TIMEOUT_MS = 120_000;
42
-
43
- /**
44
- * Release artifact for this machine, or null if uv does not publish one.
45
- *
46
- * Rosetta is deliberately not special-cased: an x64 Node on Apple Silicon
47
- * reports x64 and gets the x64 build, which runs.
48
- */
49
- export function assetName(platform = process.platform, arch = process.arch, libc = detectLibc(platform)) {
50
- // `arm` is 32-bit ARM — a Raspberry Pi on the armhf image, which uv publishes
51
- // as armv7. Left out, that machine got `unsupported_platform` for a target
52
- // that exists.
53
- const a = { x64: "x86_64", arm64: "aarch64", ia32: "i686", arm: "armv7" }[arch];
54
- if (!a) return null;
55
- // Apple publishes no i686 build, and never will.
56
- if (platform === "darwin") return a === "i686" ? null : `uv-${a}-apple-darwin.tar.gz`;
57
- if (platform === "win32") return `uv-${a}-pc-windows-msvc.zip`;
58
- if (platform === "linux") {
59
- // MUSL IS ITS OWN TARGET, and asking for the gnu build on Alpine is not a
60
- // degraded install, it is an endless one: the glibc ELF interpreter is
61
- // absent, so the downloaded binary cannot start, `bootstrapUv` returns
62
- // `does_not_run`, and because ensureCswap runs whenever cswapVersion() is
63
- // null the 35 MB archive is fetched, hashed and extracted again on EVERY
64
- // launch — with the accounts panel permanently unavailable. `node:alpine`
65
- // is a common enough base image for this to be somebody's whole experience
66
- // of the deck.
67
- if (libc === "musl") return `uv-${a}-unknown-linux-musl.tar.gz`;
68
- return `uv-${a}-unknown-linux-gnu.tar.gz`;
69
- }
70
- return null;
71
- }
72
-
73
- /**
74
- * "glibc" or "musl" for this process.
75
- *
76
- * Node's own report names the glibc it is linked against, and a musl build has
77
- * no such field — which is the check every native-addon loader uses, and needs
78
- * no child process. Anything unexpected reads as glibc, because that is the
79
- * overwhelmingly common case and the wrong guess there is the state this code
80
- * was already in.
81
- */
82
- export function detectLibc(platform = process.platform, report = () => process.report?.getReport?.()) {
83
- // The question only exists on Linux. macOS and Windows have no glibc field
84
- // either, so asking there and reading the absence as musl would answer "musl"
85
- // for every Mac — which is why the platform comes first.
86
- if (platform !== "linux") return "glibc";
87
- try {
88
- const header = report()?.header;
89
- if (!header) return "glibc";
90
- if (typeof header.glibcVersionRuntime === "string") return "glibc";
91
- return "musl";
92
- } catch { return "glibc"; }
93
- }
94
-
95
- /** Path to a uv this function installed earlier, or null. */
96
- export function existingBootstrappedUv() {
97
- const bin = join(UV_DIR, process.platform === "win32" ? "uv.exe" : "uv");
98
- return existsSync(bin) ? bin : null;
99
- }
100
-
101
- async function latestVersion() {
102
- try {
103
- const res = await fetch(RELEASE_API, {
104
- headers: { accept: "application/vnd.github+json", "user-agent": "agents-deck" },
105
- signal: AbortSignal.timeout(8_000),
106
- });
107
- if (!res.ok) return FALLBACK_VERSION;
108
- const tag = (await res.json())?.tag_name;
109
- return typeof tag === "string" && /^\d/.test(tag) ? tag : FALLBACK_VERSION;
110
- } catch {
111
- return FALLBACK_VERSION;
112
- }
113
- }
114
-
115
- async function download(url, { asText = false } = {}) {
116
- const res = await fetch(url, {
117
- redirect: "follow",
118
- headers: { "user-agent": "agents-deck" },
119
- signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
120
- });
121
- if (!res.ok) return null;
122
- return asText ? (await res.text()) : Buffer.from(await res.arrayBuffer());
123
- }
124
-
125
- /**
126
- * A path as a PowerShell single-quoted string literal.
127
- *
128
- * Inside single quotes PowerShell expands nothing, so a doubled quote is both
129
- * the only escape it has and the only one needed. Windows filenames cannot
130
- * contain `"`, `<`, `>` or `|`, but they can contain an apostrophe — and the
131
- * archive lands under the user's own profile either way, which is where an
132
- * apostrophe comes from: C:\Users\O'Brien\.agents-deck\tools. Pasted in raw,
133
- * that apostrophe ended the string mid-path and Expand-Archive died of a syntax
134
- * error, so uv could never be bootstrapped on an account whose owner has that
135
- * name.
136
- */
137
- const psQuote = (s) => `'${String(s).replace(/'/g, "''")}'`;
138
-
139
- /**
140
- * The command that unpacks the artifact, as file + argv.
141
- *
142
- * `tar` is present on macOS, on Linux, and on Windows 10 1803 and later (as
143
- * bsdtar), but Windows ships .zip and older Windows has no tar at all — so zip
144
- * goes through PowerShell's Expand-Archive, which is always there.
145
- *
146
- * Exported, pure, and with the platform as a parameter, because the branch that
147
- * needs checking is the one that never runs on the machine running the tests.
148
- *
149
- * Only the tar branch gets to pass its paths as arguments. `powershell.exe
150
- * -Command` has no argument channel to pass them through — everything after
151
- * -Command is joined back into the script text — so the paths are part of a
152
- * PowerShell program either way, and quoting them for PowerShell is the whole
153
- * job. Nothing else is quoting them on the way there: powershell.exe is an .exe,
154
- * so exec.mjs spawns it directly with shell:false and the -Command string
155
- * arrives as one intact argv entry, never seeing cmd.exe.
156
- */
157
- export function extractCommand(archivePath, destDir, platform = process.platform) {
158
- if (platform === "win32" && archivePath.endsWith(".zip")) {
159
- return {
160
- file: "powershell.exe",
161
- args: [
162
- "-NoProfile", "-NonInteractive", "-Command",
163
- `Expand-Archive -LiteralPath ${psQuote(archivePath)} ` +
164
- `-DestinationPath ${psQuote(destDir)} -Force`,
165
- ],
166
- };
167
- }
168
- return { file: "tar", args: ["-xzf", archivePath, "-C", destDir] };
169
- }
170
-
171
- /** Unpack the artifact. */
172
- async function extract(archivePath, destDir) {
173
- const { file, args } = extractCommand(archivePath, destDir);
174
- const r = await run(file, args, { timeout: 120_000 });
175
- return r.ok;
176
- }
177
-
178
- /**
179
- * Find the uv executable somewhere under `dir`.
180
- *
181
- * The archives put it in a versioned subdirectory whose name has changed
182
- * between releases, so it is located rather than assumed.
183
- */
184
- async function findUv(dir, depth = 0) {
185
- const wanted = process.platform === "win32" ? "uv.exe" : "uv";
186
- let entries;
187
- try { entries = await readdir(dir, { withFileTypes: true }); } catch { return null; }
188
- for (const e of entries) {
189
- if (e.isFile() && e.name === wanted) return join(dir, e.name);
190
- }
191
- if (depth >= 2) return null;
192
- for (const e of entries) {
193
- if (!e.isDirectory()) continue;
194
- const hit = await findUv(join(dir, e.name), depth + 1);
195
- if (hit) return hit;
196
- }
197
- return null;
198
- }
199
-
200
- /**
201
- * Download and verify uv into ~/.agents-deck/tools/uv.
202
- *
203
- * Returns { ok: true, bin, version } or { ok: false, reason }. Never throws:
204
- * every caller treats a missing uv as an expected state.
205
- */
206
- export async function bootstrapUv() {
207
- // A uv from an earlier run is reused, but only once it has proved it still
208
- // runs. Existing-and-therefore-good was a one-way door: the caller has no
209
- // other installer left by the time it gets here, and nothing anywhere ever
210
- // deletes or re-checks this path, so a uv that had been damaged — by an
211
- // interrupted copy from a deck older than this fix, a half-finished disk
212
- // restore, a truncating backup tool — made every boot from then on fail with
213
- // `install_failed` until the user found and deleted the directory by hand.
214
- // One `--version` costs milliseconds and turns that into a re-download.
215
- const already = existingBootstrappedUv();
216
- if (already && (await run(already, ["--version"], { timeout: 15_000 })).ok) {
217
- return { ok: true, bin: already, version: "existing" };
218
- }
219
- // A broken one is deliberately left where it is rather than deleted: the
220
- // download below renames its replacement over it, and if the download cannot
221
- // happen the file is harmless — every caller probes `--version` before using
222
- // it, so a uv that does not answer is already treated as absent.
223
-
224
- // Downloading an executable is a bigger step than installing a package with a
225
- // tool the user already chose, so it gets its own off switch as well as the
226
- // blanket one — someone may want the managed installs and not this.
227
- if (process.env.AGENTS_DECK_NO_DOWNLOAD === "1") return { ok: false, reason: "download_disabled" };
228
-
229
- const asset = assetName();
230
- if (!asset) return { ok: false, reason: "unsupported_platform" };
231
-
232
- const version = await latestVersion();
233
- const base = `${DOWNLOAD_BASE}/${version}/${asset}`;
234
-
235
- // Created inside the try, because it is created rather than merely named:
236
- // see the mkdtemp below for why that distinction is the whole fix.
237
- let staging = null;
238
- // Set while a half-finished binary exists under a name of its own, cleared the
239
- // moment it is renamed into place; the finally below removes whatever is left.
240
- let partial = null;
241
- try {
242
- const [archive, sumText] = await Promise.all([
243
- download(base),
244
- download(`${base}.sha256`, { asText: true }),
245
- ]);
246
- if (!archive) return { ok: false, reason: "download_failed" };
247
-
248
- // No published checksum means no way to know what was downloaded, and an
249
- // unverified binary is not something to run on someone's machine.
250
- const expected = sumText?.trim().split(/\s+/)[0]?.toLowerCase();
251
- if (!expected || !/^[0-9a-f]{64}$/.test(expected)) {
252
- return { ok: false, reason: "no_checksum" };
253
- }
254
- const actual = createHash("sha256").update(archive).digest("hex");
255
- if (actual !== expected) return { ok: false, reason: "checksum_mismatch" };
256
-
257
- // ── where the archive is unpacked, and why it is not the system temp dir ──
258
- //
259
- // This used to be `join(tmpdir(), `agents-deck-uv-${process.pid}`)`, created
260
- // with `mkdir(..., { recursive: true })` — which does not fail on a
261
- // directory that is already there.
262
- //
263
- // On macOS tmpdir() is a per-user `/var/folders/…/T` and on Windows it is
264
- // inside the profile, so the header's promise held on both. On Linux it is
265
- // `/tmp`, mode 1777, writable by every account on the box. The comment two
266
- // hundred lines up reasons carefully about tmpdir() being inside the user's
267
- // profile ON WINDOWS and never asks the Linux question.
268
- //
269
- // Three things then line up. The name is derived from a pid, so it is
270
- // cheap to blanket the whole plausible range in advance. `findUv` returns a
271
- // depth-0 file before it recurses, while the genuine uv always sits one
272
- // level down inside the archive's versioned directory — so a planted
273
- // `/tmp/agents-deck-uv-<pid>/uv` wins deterministically rather than by
274
- // racing. And the SHA-256 above is computed on the downloaded buffer, never
275
- // on the extracted file, so it does not cover the thing that is executed.
276
- // What followed was a copy, a chmod 0755, a `--version` run, a rename to
277
- // the permanent name and reuse of that name forever.
278
- //
279
- // `mkdtemp` under TOOL_DIR fixes all three at once: the directory is inside
280
- // a tree the user already owns, its name is unpredictable, and mkdtemp
281
- // CREATES — it cannot be handed a directory somebody else made. findUv's
282
- // ordering is left alone deliberately; it is not wrong on its own, and it
283
- // stops being reachable once nobody else can write here.
284
- await mkdir(TOOL_DIR, { recursive: true });
285
- staging = await mkdtemp(join(TOOL_DIR, "uv-staging-"));
286
- const archivePath = join(staging, asset);
287
- await writeFile(archivePath, archive);
288
- if (!(await extract(archivePath, staging))) return { ok: false, reason: "extract_failed" };
289
-
290
- const found = await findUv(staging);
291
- if (!found) return { ok: false, reason: "not_in_archive" };
292
-
293
- await mkdir(UV_DIR, { recursive: true });
294
- const dest = join(UV_DIR, process.platform === "win32" ? "uv.exe" : "uv");
295
-
296
- // Copy rather than rename out of staging: the staging dir is in the system
297
- // temp directory, which is often a different filesystem, and rename across
298
- // one fails. But the copy goes to a name of its own INSIDE UV_DIR — same
299
- // filesystem as the destination, so THAT rename is the atomic kind — and
300
- // only becomes `uv` once it is whole, flushed, executable and has answered
301
- // `--version`. Copying 40MB straight to the final name is many writes long,
302
- // and a Ctrl-C in the middle of them (the boot install runs before the
303
- // signal handlers are up) left a truncated file under the name every later
304
- // run trusts. Interrupt this instead and all that is left is an inert temp
305
- // file: `uv` either does not exist or is a binary that ran once already.
306
- // The pid keeps two decks starting at once off each other's copy, and the
307
- // .exe keeps the file executable on Windows, where the version check below
308
- // would otherwise have nothing to spawn.
309
- const suffix = process.platform === "win32" ? ".exe" : "";
310
- partial = join(UV_DIR, `.uv-${process.pid}-${Date.now().toString(36)}${suffix}`);
311
- await copyFile(found, partial);
312
- // Flushed before the rename, so a crash or power cut just after a
313
- // successful bootstrap cannot leave the name pointing at blocks that were
314
- // never written — the same guarantee writeFileAtomic gives settings.json.
315
- const handle = await open(partial, "r+");
316
- try { await handle.sync(); } finally { await handle.close(); }
317
- if (process.platform !== "win32") await chmod(partial, 0o755);
318
-
319
- const check = await run(partial, ["--version"], { timeout: 15_000 });
320
- if (!check.ok) return { ok: false, reason: "does_not_run" };
321
-
322
- await renameWithRetry(partial, dest);
323
- partial = null;
324
-
325
- return { ok: true, bin: dest, version };
326
- } catch (err) {
327
- return { ok: false, reason: "error", detail: String(err?.message ?? err).slice(0, 200) };
328
- } finally {
329
- // maxRetries for the reason the ccusage repair gives: the archive was just
330
- // unpacked here and `partial` was just run out of it, and on Windows a file
331
- // a handle has not finished releasing cannot be deleted — the directory
332
- // then refuses to go with ENOTEMPTY. Litter rather than a failure, since
333
- // this is swallowed, but litter in the user's home that nothing else sweeps.
334
- if (staging) await rm(staging, { recursive: true, force: true, maxRetries: 10, retryDelay: 25 }).catch(() => {});
335
- if (partial) await rm(partial, { force: true }).catch(() => {});
336
- }
337
- }