agent-dag 3.22.0 → 3.22.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.
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-3FWd7g_W.css +0 -1
  10. package/dist/web/assets/index-BOwtoP02.js +0 -266
  11. package/dist/web/index.html +0 -49
  12. package/hook/hook.js +0 -542
  13. package/release-notes.json +0 -392
  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,211 +0,0 @@
1
- // Which browsers are on this machine, which are running, and what can honestly
2
- // be said about whether any of them is talking to the relay.
3
- //
4
- // THE ANSWER IS MOSTLY "I CANNOT TELL", AND SAYING SO IS THE FEATURE. Two facts
5
- // measured on the machine this was written on decide the whole shape of this
6
- // file:
7
- //
8
- // dig +short bridge.claudeusercontent.com -> 160.79.104.10
9
- // dig +short api.anthropic.com -> 160.79.104.10
10
- //
11
- // The relay shares an address with the API and with claude.ai. An established
12
- // connection to it is therefore NOT evidence of a relay session — an open
13
- // claude.ai tab is indistinguishable — and blocking by address would sever
14
- // Claude Code itself, which is why the killswitch blocks the NAME.
15
- //
16
- // lsof -nP -i TCP -a -c "Brave Browser" -> 14 lines
17
- // lsof -nP -i TCP -a -c "Google Chrome" -> 0 lines
18
- //
19
- // And lsof cannot see some browsers' sockets at all. Zero lines for a browser
20
- // that is plainly running is a blind probe, not a quiet one, so absence is not
21
- // evidence either.
22
- //
23
- // Both directions therefore fail, and the honest report has three states rather
24
- // than two: `live` (with the caveat attached), `none-seen` (the probe worked and
25
- // found nothing) and `unknown` (the probe could not see, or does not exist on
26
- // this platform). There is deliberately no state that means "definitely not
27
- // connected", because nothing here can establish that.
28
- import { existsSync } from "node:fs";
29
- import { basename } from "node:path";
30
- import { browserRoots, hasExtension, profileDirs } from "./browser-profiles.mjs";
31
- import { run } from "./exec.mjs";
32
-
33
- /**
34
- * The name each browser's processes carry — PER PLATFORM, because they do not
35
- * agree.
36
- *
37
- * This used to be one table of macOS bundle display names, sent to all three
38
- * probes. `tasklist /FI "IMAGENAME eq Google Chrome.exe"` matches nothing and
39
- * exits **0** printing `INFO: No tasks are running…`, so the probe read
40
- * `ok: true` and returned a confident `false`; `pgrep -x "Google Chrome"`
41
- * matches against `comm`, which on Linux is `chrome`. Every browser on Windows
42
- * and Linux therefore reported "not running", `relayLink` was never called, and
43
- * the relay half of this panel was dead on two of the three platforms — while
44
- * the module's own header forbids exactly that: a state that means "definitely
45
- * not connected" when nothing established it.
46
- *
47
- * Arc is macOS-only and has no entry elsewhere, which is the honest answer: a
48
- * root with no name here is reported as installed and never as running.
49
- */
50
- const APP_NAME = {
51
- darwin: {
52
- chrome: "Google Chrome",
53
- "chrome-beta": "Google Chrome Beta",
54
- "chrome-canary": "Google Chrome Canary",
55
- chromium: "Chromium",
56
- brave: "Brave Browser",
57
- edge: "Microsoft Edge",
58
- vivaldi: "Vivaldi",
59
- arc: "Arc",
60
- },
61
- // Image names, which is what tasklist's IMAGENAME filter compares against.
62
- // The probe appends `.exe`, so these are spelled without it, exactly as the
63
- // POSIX ones are.
64
- win32: {
65
- chrome: "chrome",
66
- "chrome-beta": "chrome",
67
- "chrome-canary": "chrome",
68
- chromium: "chrome",
69
- brave: "brave",
70
- edge: "msedge",
71
- vivaldi: "vivaldi",
72
- },
73
- // `comm`, which is what `pgrep -x` matches and what the packages install as.
74
- linux: {
75
- chrome: "chrome",
76
- "chrome-beta": "chrome",
77
- "chrome-canary": "chrome",
78
- chromium: "chromium",
79
- // The two roots `browserRoots` emits and this table used to miss (#794).
80
- // The snap path's own comment there says it "is the only Chromium root a
81
- // default Ubuntu install has" — so on stock Ubuntu the panel reported
82
- // Chromium as not running while it was running, and the quit reaction,
83
- // which `available("linux")` does offer, answered `unknown_browser` on
84
- // every finding from either of them. darwin covers all 8 of its roots and
85
- // win32 all 7 of its own; only this table was short.
86
- "chromium-snap": "chromium",
87
- "brave-flatpak": "brave",
88
- brave: "brave",
89
- edge: "msedge",
90
- vivaldi: "vivaldi-bin",
91
- },
92
- };
93
-
94
- /** The process name for a browser on a platform, or null when this platform
95
- * has no name for it — which is a different answer from "not running". */
96
- export function processName(key, platform = process.platform) {
97
- const table = APP_NAME[platform] ?? APP_NAME.linux;
98
- return table[key] ?? null;
99
- }
100
-
101
- /** Every address the relay currently resolves to.
102
- *
103
- * Empty is not an error — a machine with no `dig`, or one where the name is
104
- * already blocked in /etc/hosts, both land here — and an empty list makes
105
- * every connection probe answer `unknown`, which is the correct answer when
106
- * there is nothing to compare against. */
107
- export async function relayAddresses(host, deps = {}) {
108
- const exec = deps.run ?? run;
109
- const out = await exec("dig", ["+short", host]).catch(() => null);
110
- if (!out?.ok) return [];
111
- return String(out.stdout ?? "").split("\n")
112
- .map(l => l.trim())
113
- .filter(l => /^[0-9.]+$/.test(l) || /^[0-9a-f:]+$/i.test(l) && l.includes(":"));
114
- }
115
-
116
- /** Whether a named application has any process at all. */
117
- export async function isRunning(app, platform = process.platform, deps = {}) {
118
- const exec = deps.run ?? run;
119
- if (platform === "win32") {
120
- const out = await exec("tasklist", ["/FI", `IMAGENAME eq ${app}.exe`, "/NH"]).catch(() => null);
121
- if (!out?.ok) return null;
122
- return String(out.stdout ?? "").toLowerCase().includes(`${app.toLowerCase()}.exe`);
123
- }
124
- const out = await exec("pgrep", ["-x", app]).catch(() => null);
125
- // pgrep exits 1 when it matched nothing, which `run` reports as not-ok — the
126
- // same shape as "pgrep is missing". Distinguished by whether it said anything
127
- // on stderr, because only one of the two is a failure to ask.
128
- if (out === null) return null;
129
- if (out.ok) return true;
130
- return String(out.stderr ?? "").trim() === "" ? false : null;
131
- }
132
-
133
- /**
134
- * What can be said about this browser's connections to the relay.
135
- *
136
- * -> { state: "live" | "none-seen" | "unknown", count, why }
137
- *
138
- * `why` is not decoration. Every one of these three answers is qualified, and a
139
- * panel that showed the state without the qualification would be making a claim
140
- * this module has already established it cannot make.
141
- */
142
- export async function relayLink(app, addresses, platform = process.platform, deps = {}) {
143
- const exec = deps.run ?? run;
144
- if (platform === "win32") {
145
- return { state: "unknown", count: 0, why: "this check needs lsof, which Windows does not have" };
146
- }
147
- if (addresses.length === 0) {
148
- return { state: "unknown", count: 0, why: "the relay name did not resolve, so there is nothing to match against" };
149
- }
150
- const out = await exec("lsof", ["-nP", "-i", "TCP", "-a", "-c", app]).catch(() => null);
151
- if (out === null) return { state: "unknown", count: 0, why: "lsof is not available here" };
152
-
153
- const lines = String(out.stdout ?? "").split("\n").filter(l => l.trim() !== "");
154
- // A browser with no visible sockets at all is a BLIND probe, not a quiet one.
155
- // Measured: lsof sees fourteen TCP lines for Brave and zero for Google Chrome
156
- // while both are running. Reporting "none seen" here would turn "I cannot
157
- // look" into "I looked and it was clear".
158
- if (lines.length <= 1) {
159
- return { state: "unknown", count: 0, why: "lsof cannot see this browser's sockets, so absence proves nothing" };
160
- }
161
- const hit = lines.filter(l => l.includes("ESTABLISHED") && addresses.some(a => l.includes(a)));
162
- if (hit.length === 0) {
163
- return { state: "none-seen", count: 0, why: "no TCP connection to that address; QUIC would not appear here" };
164
- }
165
- return {
166
- state: "live",
167
- count: hit.length,
168
- why: "the relay shares an address with api.anthropic.com and claude.ai, so an open claude.ai tab looks the same",
169
- };
170
- }
171
-
172
- /**
173
- * Every browser this deck knows how to look at, whether or not it is here.
174
- *
175
- * Installed browsers with no profile are included on purpose. "Chrome is
176
- * installed and has never been opened" and "Chrome is not installed" are
177
- * different answers, and a panel that lists only what it found makes them
178
- * indistinguishable — which is the same failure browser-profiles.mjs guards
179
- * against one level down.
180
- */
181
- export async function browserSurvey({
182
- relayHost,
183
- platform = process.platform,
184
- env = process.env,
185
- home,
186
- deps = {},
187
- } = {}) {
188
- const exists = deps.existsSync ?? existsSync;
189
- const roots = browserRoots(platform, env, home);
190
- const addresses = await relayAddresses(relayHost, deps);
191
-
192
- const out = [];
193
- for (const root of roots) {
194
- const installed = exists(root.root);
195
- const profiles = installed ? profileDirs(root.root, deps.fs) : [];
196
- const app = processName(root.key, platform);
197
- const running = installed && app ? await isRunning(app, platform, deps) : false;
198
- out.push({
199
- key: root.key,
200
- name: root.name,
201
- installed,
202
- profiles: profiles.length,
203
- withExtension: profiles.filter(d => hasExtension(d, undefined, deps.fs)).map(d => basename(d)),
204
- running,
205
- relay: installed && running && app
206
- ? await relayLink(app, addresses, platform, deps)
207
- : { state: "unknown", count: 0, why: running ? "no process name known for this browser" : "not running" },
208
- });
209
- }
210
- return out;
211
- }
@@ -1,279 +0,0 @@
1
- // Which Chromium-family browsers are on this machine, which of their profiles
2
- // exist, and which of those carry the Claude in Chrome extension.
3
- //
4
- // This is the floor of Browser Watch: every other part of the feature opens a
5
- // profile's History or its Secure Preferences, and neither is findable until
6
- // something has answered "where are the profiles". The whole module is a claim
7
- // about somebody else's directory layout, which is why nothing here reaches the
8
- // disk except through an injected `deps` — the Windows layout and the two Linux
9
- // sandbox layouts have to be exercisable from a Mac, exactly as
10
- // claudeCliCandidates() and cswapCandidates() are, or the list rots on every
11
- // platform its author does not sit at.
12
- //
13
- // THE ASYMMETRY THAT SILENTLY BREAKS IT. Chromium's user-data directory is the
14
- // parent of `Default` and `Profile N`, and macOS spells it differently from
15
- // everywhere else:
16
- //
17
- // macOS ~/Library/Application Support/Google/Chrome/Default
18
- // Windows %LOCALAPPDATA%\Google\Chrome\User Data\Default
19
- // Linux ~/.config/google-chrome/Default
20
- //
21
- // There is NO `User Data` level on macOS — the app-support directory is itself
22
- // the user-data directory — and the mistake is invisible in both directions.
23
- // Insert the level on macOS and every root misses by one; drop it on Windows
24
- // and every root misses by one. Both produce an empty list, and an empty list
25
- // is also the honest answer for a machine that has no Chrome. So a bug here
26
- // never looks like a bug. It looks like a user who does not use Chrome, on a
27
- // panel whose entire job is to say which browsers are watched.
28
- //
29
- // Arc is the exception that proves the rule: it keeps `User Data` under its own
30
- // app-support directory on macOS, because Arc is Chromium shipped by people who
31
- // did not inherit Chrome's macOS habit. It is the reason the macOS list is a
32
- // table of paths rather than a directory name plus a formula.
33
- import { existsSync as fsExistsSync, readdirSync as fsReaddirSync, statSync as fsStatSync } from "node:fs";
34
- import { homedir } from "node:os";
35
- import { posix as posixPath, win32 as winPath } from "node:path";
36
- // ONE DECLARATION OF THE EXTENSION ID (#798). It was written out here and again
37
- // in relay-guard.mjs, with the same literal, in two modules that never imported
38
- // each other — so republishing the extension under a new id and updating one
39
- // would leave the other detector answering "not installed" forever, with
40
- // nothing going red anywhere. It comes from relay-guard because that direction
41
- // is the safe one: relay-guard imports node:path and nothing else, a property
42
- // its own header states and relay-guard.test.ts pins by reading the source, and
43
- // importing this module (node:fs) into it would break that.
44
- import { CLAUDE_EXT_ID } from "./relay-guard.mjs";
45
-
46
- /**
47
- * Claude in Chrome's extension id, which is the same 32 characters in every
48
- * Chromium-family browser that can install it.
49
- *
50
- * A Chrome extension id is derived from the packing key, not from the store or
51
- * the browser, so Brave, Edge and Vivaldi all hold it under this exact name.
52
- * That is what makes a directory test sufficient here and what lets one
53
- * constant serve every row this module returns.
54
- *
55
- * Re-exported rather than moved, so the callers that read it from here keep
56
- * working and the name still documents itself where it is used.
57
- */
58
- export { CLAUDE_EXT_ID };
59
-
60
- /**
61
- * The directories inside a user-data root that are browsing profiles.
62
- *
63
- * `Default` and `Profile N` only. Chromium also keeps `Guest Profile` and
64
- * `System Profile` beside them on every install — the second exists before the
65
- * user has ever opened a window — and both carry a History file that never gets
66
- * a row. Reporting them would put two permanently empty entries on the panel of
67
- * every machine in the world and make "no browsing here yet" indistinguishable
68
- * from "this is not a profile at all".
69
- */
70
- const PROFILE_DIR = /^(?:Default|Profile \d+)$/;
71
-
72
- /**
73
- * The path rules a root is written in, read off the root itself.
74
- *
75
- * `profileDirs` and `hasExtension` take a path and no platform — they are handed
76
- * a root `browserRoots` already spelled — so the flavour has to come out of the
77
- * string. It matters more than it looks. POSIX `join` would answer
78
- * `C:\…\User Data/Default`, a mixed-separator path that Windows itself would
79
- * happily open, so nothing on a real machine would ever complain; but
80
- * `posixPath.basename` of that string is the WHOLE string, so `profile` would
81
- * come back as an absolute path instead of `Default` and every caller keying on
82
- * the profile name would miss. The failure only ever appears on the leg that
83
- * checks Windows from a Mac, which is every leg that exists for this module.
84
- *
85
- * Anchored at the front rather than sniffing for a backslash anywhere: a POSIX
86
- * directory may legally be named `Brave\Browser`, and every root built here is
87
- * absolute, so a drive letter or a UNC prefix is the entire test.
88
- */
89
- const WINDOWS_ROOT = /^(?:[A-Za-z]:[\\/]|\\\\)/;
90
- const flavourOf = p => (WINDOWS_ROOT.test(String(p)) ? winPath : posixPath);
91
-
92
- /**
93
- * Whether `p` is a directory, answering false for everything else a real disk
94
- * can put there.
95
- *
96
- * Never throws, and that is the point rather than tidiness. This runs once per
97
- * candidate profile across every browser on the machine: one profile on a
98
- * disconnected network home, one `Default` that a sync tool left as a file, one
99
- * broken symlink into an unmounted volume would otherwise abort the whole walk
100
- * and take every other browser's profiles down with it.
101
- */
102
- function isDirectory(p, statSync) {
103
- try { return statSync(p).isDirectory(); } catch { return false; }
104
- }
105
-
106
- /**
107
- * Every known Chromium-family user-data root for this platform, whether or not
108
- * it exists.
109
- *
110
- * Pure, and platform, environment and home are parameters, so the Windows and
111
- * Linux answers are checkable from a Mac — the same reason claudeCliCandidates()
112
- * takes them. Deliberately does NOT stat anything: "which browsers could be
113
- * here" and "which are" are separate questions, and keeping the first one pure
114
- * is what lets a test assert the whole table without a disk of any kind.
115
- *
116
- * `key` is the stable identifier the rest of Browser Watch keys rows on; `name`
117
- * is the string a human reads. They are separate because Brave's directory is
118
- * `Brave-Browser` on one platform and `com.brave.Browser` on another, and no
119
- * panel should ever show either.
120
- */
121
- export function browserRoots(platform = process.platform, env = process.env, home = homedir()) {
122
- // The path flavour follows the PLATFORM ARGUMENT, not the host: node's `join`
123
- // emits forward slashes when the Windows table is built on a Mac, which is
124
- // both wrong for the caller and invisible in a test that only compares
125
- // against a string it built the same wrong way.
126
- const path = platform === "win32" ? winPath : posixPath;
127
-
128
- if (platform === "darwin") {
129
- // No `User Data` anywhere in here except Arc. See the module header.
130
- const support = path.join(home, "Library", "Application Support");
131
- return [
132
- { key: "chrome", name: "Google Chrome", root: path.join(support, "Google", "Chrome") },
133
- { key: "chrome-beta", name: "Google Chrome Beta", root: path.join(support, "Google", "Chrome Beta") },
134
- { key: "chrome-canary", name: "Google Chrome Canary", root: path.join(support, "Google", "Chrome Canary") },
135
- { key: "chromium", name: "Chromium", root: path.join(support, "Chromium") },
136
- { key: "brave", name: "Brave", root: path.join(support, "BraveSoftware", "Brave-Browser") },
137
- { key: "edge", name: "Microsoft Edge", root: path.join(support, "Microsoft Edge") },
138
- { key: "vivaldi", name: "Vivaldi", root: path.join(support, "Vivaldi") },
139
- { key: "arc", name: "Arc", root: path.join(support, "Arc", "User Data") },
140
- ];
141
- }
142
-
143
- if (platform === "win32") {
144
- // %LOCALAPPDATA% rather than a path under the home directory, because a
145
- // roaming profile puts AppData\Roaming on a network share while Local stays
146
- // on the machine — the same reason claudeCliCandidates() reads %APPDATA%.
147
- // An empty value is what a broken login script leaves behind, not a
148
- // relative path anybody means, so it falls back rather than joining onto "".
149
- const local = env.LOCALAPPDATA?.trim() || path.join(home, "AppData", "Local");
150
- return [
151
- { key: "chrome", name: "Google Chrome", root: path.join(local, "Google", "Chrome", "User Data") },
152
- { key: "chrome-beta", name: "Google Chrome Beta", root: path.join(local, "Google", "Chrome Beta", "User Data") },
153
- // Canary is `Chrome SxS` here and `Chrome Canary` on macOS — the same
154
- // channel under two names, because the Windows installer has always used
155
- // the side-by-side codename. Spelling it the macOS way finds nothing, and
156
- // finding nothing is this module's indistinguishable failure. Linux has
157
- // no Canary channel at all, which is why the third table has no such row.
158
- { key: "chrome-canary", name: "Google Chrome Canary", root: path.join(local, "Google", "Chrome SxS", "User Data") },
159
- { key: "chromium", name: "Chromium", root: path.join(local, "Chromium", "User Data") },
160
- { key: "brave", name: "Brave", root: path.join(local, "BraveSoftware", "Brave-Browser", "User Data") },
161
- { key: "edge", name: "Microsoft Edge", root: path.join(local, "Microsoft", "Edge", "User Data") },
162
- { key: "vivaldi", name: "Vivaldi", root: path.join(local, "Vivaldi", "User Data") },
163
- ];
164
- }
165
-
166
- // Linux, and every other POSIX that is not macOS. Falling through rather than
167
- // testing for "linux" is deliberate: a deck on FreeBSD gets the XDG layout,
168
- // which is the one its Chromium package actually uses, instead of nothing.
169
- //
170
- // XDG_CONFIG_HOME is honoured because Chromium honours it — it is where the
171
- // browser itself puts the directory, not a preference of ours — and trimmed
172
- // and truthiness-tested for the reason codexHome() spells out: an empty
173
- // variable is a shell accident, and joining onto it yields a CWD-relative
174
- // path that would have the deck reading profiles out of wherever it was
175
- // started from.
176
- const config = env.XDG_CONFIG_HOME?.trim() || path.join(home, ".config");
177
- return [
178
- { key: "chrome", name: "Google Chrome", root: path.join(config, "google-chrome") },
179
- { key: "chrome-beta", name: "Google Chrome Beta", root: path.join(config, "google-chrome-beta") },
180
- { key: "chromium", name: "Chromium", root: path.join(config, "chromium") },
181
- { key: "brave", name: "Brave", root: path.join(config, "BraveSoftware", "Brave-Browser") },
182
- { key: "edge", name: "Microsoft Edge", root: path.join(config, "microsoft-edge") },
183
- { key: "vivaldi", name: "Vivaldi", root: path.join(config, "vivaldi") },
184
- // Snap and Flatpak confine the browser to a private filesystem, so their
185
- // profiles are NOT under XDG_CONFIG_HOME and never appear in the six above.
186
- // On Ubuntu this is not an edge case: `chromium` from the archive has been
187
- // a snap-only transitional package for years, so the snap path is the only
188
- // Chromium root a default Ubuntu install has.
189
- { key: "chromium-snap", name: "Chromium (snap)", root: path.join(home, "snap", "chromium", "common", "chromium") },
190
- { key: "brave-flatpak", name: "Brave (Flatpak)", root: path.join(home, ".var", "app", "com.brave.Browser", "config", "BraveSoftware", "Brave-Browser") },
191
- ];
192
- }
193
-
194
- /** Where a profile name sits in the browser's own numbering; `Default` is first. */
195
- const profileIndex = name => (name === "Default" ? -1 : Number(name.slice("Profile ".length)));
196
-
197
- /**
198
- * The profile directories that actually exist inside `root`, `Default` first
199
- * and then `Profile N` in numeric order.
200
- *
201
- * A root that is absent, unreadable, or a file is not an error — it is the
202
- * ordinary state of five of the eight roots on any real machine — so it answers
203
- * with an empty list and the caller moves on.
204
- *
205
- * THE SORT IS NUMERIC ON PURPOSE. readdir returns whatever order the filesystem
206
- * feels like, and the obvious repair — a plain `.sort()` — puts `Profile 10`
207
- * between `Profile 1` and `Profile 2`. Nothing breaks visibly; the panel just
208
- * lists a user's profiles in an order that changes meaning as soon as they
209
- * create their tenth, which is the sort of wrongness nobody reports and
210
- * everybody notices.
211
- */
212
- export function profileDirs(root, { readdirSync = fsReaddirSync, statSync = fsStatSync } = {}) {
213
- const path = flavourOf(root);
214
- let names;
215
- try { names = readdirSync(root); } catch { return []; }
216
- return names
217
- .map(String)
218
- .filter(name => PROFILE_DIR.test(name))
219
- // `Default` is profile zero in everything but its name, and it sorts ahead
220
- // of `Profile 1` here rather than by luck of the alphabet.
221
- .sort((a, b) => profileIndex(a) - profileIndex(b))
222
- .map(name => path.join(root, name))
223
- .filter(dir => isDirectory(dir, statSync));
224
- }
225
-
226
- /**
227
- * Whether this profile has the extension's payload on disk.
228
- *
229
- * Chromium unpacks an extension to `Extensions/<id>/<version>/`, one directory
230
- * per profile, so the id directory is the cheap and complete answer to "is it
231
- * installed here" — one stat per profile, at boot, across every browser found.
232
- *
233
- * It deliberately does NOT answer "is it enabled". That state lives in Secure
234
- * Preferences, which is signed against the profile and is another part of this
235
- * feature's job; conflating the two here would mean parsing a several-megabyte
236
- * JSON file per profile to answer a question this function is not being asked.
237
- */
238
- export function hasExtension(profileDir, extId = CLAUDE_EXT_ID, { existsSync = fsExistsSync } = {}) {
239
- const path = flavourOf(profileDir);
240
- return existsSync(path.join(profileDir, "Extensions", extId));
241
- }
242
-
243
- /**
244
- * Every browsing profile on this machine, with the two files Browser Watch
245
- * reads and whether Claude in Chrome is installed in it.
246
- *
247
- * A PROFILE WITHOUT THE EXTENSION IS REPORTED, NOT DROPPED. "You have Brave
248
- * here and the extension is not in it" is the one sentence on this panel that
249
- * gets somebody unstuck, and it can only be said by a row that exists. Filtering
250
- * to extension-carrying profiles would make an un-extended browser look exactly
251
- * like an absent one — the same indistinguishable-from-nothing failure the
252
- * `User Data` trap produces, arrived at on purpose.
253
- *
254
- * A ROOT WITH NO PROFILES CONTRIBUTES NOTHING, which is not the same thing. On
255
- * the machine this was written against, Edge, Vivaldi, Chromium and Arc all have
256
- * a root and no profile inside it — installed, or merely left behind by an
257
- * uninstall — and a row for one of them would point the History reader at a
258
- * path that will never exist.
259
- */
260
- export function discoverProfiles(platform = process.platform, env = process.env, home = homedir(), deps = {}) {
261
- const profiles = [];
262
- for (const { key, name, root } of browserRoots(platform, env, home)) {
263
- // Read off the root rather than off `platform`, so the two can never
264
- // disagree about which separator this row's paths are spelled with.
265
- const path = flavourOf(root);
266
- for (const dir of profileDirs(root, deps)) {
267
- profiles.push({
268
- browser: key,
269
- name,
270
- profile: path.basename(dir),
271
- dir,
272
- historyPath: path.join(dir, "History"),
273
- securePrefsPath: path.join(dir, "Secure Preferences"),
274
- hasClaudeExt: hasExtension(dir, CLAUDE_EXT_ID, deps),
275
- });
276
- }
277
- }
278
- return profiles;
279
- }