@floh-solutions/pharos-cli 0.33.0 → 0.35.0
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 +65 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/delegate.d.ts +101 -3
- package/dist/commands/delegate.d.ts.map +1 -1
- package/dist/commands/delegate.js +370 -59
- package/dist/commands/delegate.js.map +1 -1
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +42 -5
- package/dist/commands/doctor.js.map +1 -1
- package/dist/delegate/hosts.d.ts +29 -0
- package/dist/delegate/hosts.d.ts.map +1 -1
- package/dist/delegate/hosts.js +23 -0
- package/dist/delegate/hosts.js.map +1 -1
- package/dist/delegate/sessions.d.ts +174 -1
- package/dist/delegate/sessions.d.ts.map +1 -1
- package/dist/delegate/sessions.js +282 -28
- package/dist/delegate/sessions.js.map +1 -1
- package/package.json +1 -1
package/dist/delegate/hosts.js
CHANGED
|
@@ -16,6 +16,7 @@ export const HOSTS = {
|
|
|
16
16
|
"/System/Applications/Utilities/Terminal.app",
|
|
17
17
|
"/Applications/Utilities/Terminal.app",
|
|
18
18
|
],
|
|
19
|
+
main: "Terminal.app/Contents/MacOS/Terminal",
|
|
19
20
|
},
|
|
20
21
|
vscode: {
|
|
21
22
|
id: "vscode",
|
|
@@ -23,6 +24,7 @@ export const HOSTS = {
|
|
|
23
24
|
bundleId: "com.microsoft.VSCode",
|
|
24
25
|
candidates: ["/Applications/Visual Studio Code.app", "~/Applications/Visual Studio Code.app"],
|
|
25
26
|
cask: "visual-studio-code",
|
|
27
|
+
main: "Visual Studio Code.app/Contents/MacOS/",
|
|
26
28
|
vscode: {
|
|
27
29
|
extensionsDir: ".vscode/extensions",
|
|
28
30
|
scheme: "vscode",
|
|
@@ -38,6 +40,7 @@ export const HOSTS = {
|
|
|
38
40
|
"~/Applications/Visual Studio Code - Insiders.app",
|
|
39
41
|
],
|
|
40
42
|
cask: "visual-studio-code@insiders",
|
|
43
|
+
main: "Visual Studio Code - Insiders.app/Contents/MacOS/",
|
|
41
44
|
vscode: {
|
|
42
45
|
extensionsDir: ".vscode-insiders/extensions",
|
|
43
46
|
scheme: "vscode-insiders",
|
|
@@ -142,6 +145,26 @@ export function hostOfCommand(comm) {
|
|
|
142
145
|
return "navarch";
|
|
143
146
|
return null;
|
|
144
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Which host a process is the MAIN process of, or null — for a helper, a
|
|
150
|
+
* shell, or anything else.
|
|
151
|
+
*
|
|
152
|
+
* Narrower than {@link hostOfCommand} on purpose: that one attributes a
|
|
153
|
+
* session to the app it hangs off, helpers included, and this one finds the
|
|
154
|
+
* single process whose environment the app's terminals inherit (measured
|
|
155
|
+
* 2026-09-11: `…/Visual Studio Code - Insiders.app/Contents/MacOS/Code -
|
|
156
|
+
* Insiders`, pid 90339, carrying the `CLAUDE_CONFIG_DIR` its launching shell
|
|
157
|
+
* had). The Squirrel updater ships inside the same bundle, under
|
|
158
|
+
* `Contents/Frameworks/`, and is therefore not matched.
|
|
159
|
+
*/
|
|
160
|
+
export function mainHostOfCommand(comm) {
|
|
161
|
+
for (const id of HOST_IDS) {
|
|
162
|
+
const marker = HOSTS[id].main;
|
|
163
|
+
if (marker !== undefined && comm.includes(marker))
|
|
164
|
+
return id;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
145
168
|
async function exists(path) {
|
|
146
169
|
try {
|
|
147
170
|
await access(path, constants.R_OK);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hosts.js","sourceRoot":"","sources":["../../src/delegate/hosts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AA8BhC,MAAM,CAAC,MAAM,QAAQ,GAAsB,CAAC,UAAU,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,CAAC,CAAC;AAChG,MAAM,CAAC,MAAM,SAAS,GAAuB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AACjE,MAAM,CAAC,MAAM,KAAK,GAAoB,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"hosts.js","sourceRoot":"","sources":["../../src/delegate/hosts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AA8BhC,MAAM,CAAC,MAAM,QAAQ,GAAsB,CAAC,UAAU,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,CAAC,CAAC;AAChG,MAAM,CAAC,MAAM,SAAS,GAAuB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AACjE,MAAM,CAAC,MAAM,KAAK,GAAoB,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AAoChE,MAAM,CAAC,MAAM,KAAK,GAA6B;IAC7C,QAAQ,EAAE;QACR,EAAE,EAAE,UAAU;QACd,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE,oBAAoB;QAC9B,UAAU,EAAE;YACV,6CAA6C;YAC7C,sCAAsC;SACvC;QACD,IAAI,EAAE,sCAAsC;KAC7C;IACD,MAAM,EAAE;QACN,EAAE,EAAE,QAAQ;QACZ,IAAI,EAAE,oBAAoB;QAC1B,QAAQ,EAAE,sBAAsB;QAChC,UAAU,EAAE,CAAC,sCAAsC,EAAE,uCAAuC,CAAC;QAC7F,IAAI,EAAE,oBAAoB;QAC1B,IAAI,EAAE,wCAAwC;QAC9C,MAAM,EAAE;YACN,aAAa,EAAE,oBAAoB;YACnC,MAAM,EAAE,QAAQ;YAChB,MAAM,EAAE,CAAC,wBAAwB,EAAE,aAAa,CAAC;SAClD;KACF;IACD,iBAAiB,EAAE;QACjB,EAAE,EAAE,iBAAiB;QACrB,IAAI,EAAE,+BAA+B;QACrC,QAAQ,EAAE,8BAA8B;QACxC,UAAU,EAAE;YACV,iDAAiD;YACjD,kDAAkD;SACnD;QACD,IAAI,EAAE,6BAA6B;QACnC,IAAI,EAAE,mDAAmD;QACzD,MAAM,EAAE;YACN,aAAa,EAAE,6BAA6B;YAC5C,MAAM,EAAE,iBAAiB;YACzB,MAAM,EAAE,CAAC,mCAAmC,EAAE,wBAAwB,CAAC;SACxE;KACF;IACD,OAAO,EAAE;QACP,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,SAAS;QACf,QAAQ,EAAE,eAAe;QACzB,UAAU,EAAE,CAAC,2BAA2B,EAAE,4BAA4B,CAAC;KACxE;CACF,CAAC;AAEF,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAQ,QAA8B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,OAAQ,SAA+B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAQ,KAA2B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACtD,CAAC;AASD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAY,EAAE,MAAyB,OAAO,CAAC,GAAG;IAC9E,IAAI,QAAQ,EAAE,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IAEzB,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC;IAElG,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;IACtC,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACxC,MAAM,IAAI,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACrF,IAAI,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC;YACvD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QACnE,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,SAAS,CAAC,QAAgB;IACvC,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAC1B,iBAAiB,EACjB,CAAC,iCAAiC,QAAQ,GAAG,CAAC,EAC9C,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,GAAG,IAAI,EAAE,CAC3C,CAAC;QACF,MAAM,KAAK,GAAG,MAAM;aACjB,KAAK,CAAC,IAAI,CAAC;aACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;aAC1B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;QACzE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACpC,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAE,CAAC;IAC7G,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAe;IACjD,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAC1B,iBAAiB,EACjB,CAAC,UAAU,EAAE,4BAA4B,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,EACrG,EAAE,OAAO,EAAE,KAAK,EAAE,CACnB,CAAC;QACF,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;QAC9B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,OAAe;IACvC,OAAO,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,IAAI,CAAC,QAAQ,CAAC,sCAAsC,CAAC;QAAE,OAAO,UAAU,CAAC;IAC7E,KAAK,MAAM,EAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAU,EAAE,CAAC;QACxD,IAAI,KAAK,CAAC,EAAE,CAAC,CAAC,MAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAAE,OAAO,EAAE,CAAC;IAClF,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO,SAAS,CAAC;IACrG,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC;QAC9B,IAAI,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,EAAE,CAAC;IAC/D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,IAAY;IAChC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -24,6 +24,44 @@ import { type AgentId, type HostId } from "./hosts.js";
|
|
|
24
24
|
* non-controlling process — `EACCES`, confirmed with a private pty. So the
|
|
25
25
|
* pid and tty found here are handed to the HOST (Terminal's `do script … in
|
|
26
26
|
* tab`, the VS Code bridge), which owns the terminal and may write to it.
|
|
27
|
+
*
|
|
28
|
+
* ## A machine has several Claude registries — measured 2026-09-11 (#1431)
|
|
29
|
+
*
|
|
30
|
+
* `claude agents --json` lists the sessions registered in ONE config
|
|
31
|
+
* directory: `<dir>/sessions/<pid>.json`, the directory being
|
|
32
|
+
* `$CLAUDE_CONFIG_DIR` or `~/.claude`. An Argus agent account is exactly such
|
|
33
|
+
* a directory (`~/.config/argus/auth/claude/<name>`), and anything started from
|
|
34
|
+
* a shell inside one of its workers inherits it — including an editor. On the
|
|
35
|
+
* reference machine VS Code Insiders (pid 90339) had been launched from a FLOH
|
|
36
|
+
* worker's shell, so every integrated terminal carried
|
|
37
|
+
* `CLAUDE_CONFIG_DIR=…/auth/claude/floh`, and a session started there
|
|
38
|
+
* (`tibata-49`) was registered under floh. Asked from the default registry it
|
|
39
|
+
* did not exist; asked from floh it was listed at once. Nothing was wrong with
|
|
40
|
+
* the session: one registry was read on a machine that has several.
|
|
41
|
+
*
|
|
42
|
+
* So Claude sessions are read from every registry this machine can have,
|
|
43
|
+
* one `claude agents --json` per distinct directory, merged and de-duplicated
|
|
44
|
+
* by pid — see {@link discoverRegistries}. Two measured facts shape it:
|
|
45
|
+
*
|
|
46
|
+
* - **Asking a registry can write into it.** `claude agents --json` against an
|
|
47
|
+
* Argus profile that had never run a session created a first-start
|
|
48
|
+
* `.claude.json` and `backups/` inside it — the fleet's credential store,
|
|
49
|
+
* which another app must not touch (#1359 is what touching it once cost). So
|
|
50
|
+
* every registry beyond the default is asked only when it already holds a
|
|
51
|
+
* `sessions/*.json`: one nobody has registered in has nothing to list, and
|
|
52
|
+
* asking would only provision it.
|
|
53
|
+
* - **The machine's own login is spelled with the variable UNSET.** It keeps
|
|
54
|
+
* `.claude.json` beside `~/.claude` rather than in it (#1356), so naming
|
|
55
|
+
* `~/.claude` in `CLAUDE_CONFIG_DIR` is a different config that happens to
|
|
56
|
+
* share the sessions directory. That registry is asked with the variable
|
|
57
|
+
* removed, never set.
|
|
58
|
+
*
|
|
59
|
+
* And one that limits it: **macOS does not show `ps -E` the environment of an
|
|
60
|
+
* Apple platform binary** (macOS 26; `/bin/sleep`, `/bin/zsh` and the running
|
|
61
|
+
* Terminal show none, `node`, VS Code and `claude` show theirs). Terminal is
|
|
62
|
+
* one, so which login a Terminal would start a session as cannot be read — it
|
|
63
|
+
* is asked all the same and simply answers nothing. Its SESSIONS are still
|
|
64
|
+
* found, because `claude` is not a platform binary and carries its own.
|
|
27
65
|
*/
|
|
28
66
|
export interface ProcessRow {
|
|
29
67
|
pid: number;
|
|
@@ -58,6 +96,27 @@ export interface ClaudeAgent {
|
|
|
58
96
|
export declare function parseClaudeAgents(json: string): ClaudeAgent[];
|
|
59
97
|
/** `lsof -a -p PID -d cwd -Fn` prints `p<pid>`, `fcwd`, `n<path>`; the path is what we want. */
|
|
60
98
|
export declare function parseLsofCwd(text: string): string | null;
|
|
99
|
+
/**
|
|
100
|
+
* One variable out of `ps -E -ww -o pid=,command= -p …`, per pid.
|
|
101
|
+
*
|
|
102
|
+
* `ps -E` prints each process's command line and then the environment it was
|
|
103
|
+
* started with, all space-separated, so a line is ambiguous in general — an
|
|
104
|
+
* argument or a value may itself contain spaces. Two rules make it good enough
|
|
105
|
+
* for the one variable read here:
|
|
106
|
+
*
|
|
107
|
+
* - **the LAST ` NAME=` on the line wins**, because the environment is printed
|
|
108
|
+
* after the arguments, and an argument that merely mentions the variable (a
|
|
109
|
+
* prompt, say) comes first;
|
|
110
|
+
* - **a value runs until the next ` IDENT=`**, so a path with a space in it
|
|
111
|
+
* survives and the variable after it is not swallowed.
|
|
112
|
+
*
|
|
113
|
+
* Only absolute values are kept: `claude` would resolve a relative one against
|
|
114
|
+
* its own working directory, which is not this process's to know.
|
|
115
|
+
*
|
|
116
|
+
* Everything else on the line — which is the process's whole environment,
|
|
117
|
+
* tokens included — is dropped here and never printed by anything.
|
|
118
|
+
*/
|
|
119
|
+
export declare function parseEnvironments(text: string, name: string): Map<number, string>;
|
|
61
120
|
/** The ancestors of a pid, nearest first, stopping at launchd or a cycle. */
|
|
62
121
|
export declare function ancestry(pid: number, rows: readonly ProcessRow[]): ProcessRow[];
|
|
63
122
|
/** The first ancestor that is a known host, or null. */
|
|
@@ -70,12 +129,70 @@ export interface Session {
|
|
|
70
129
|
status: "idle" | "busy" | null;
|
|
71
130
|
tty: string | null;
|
|
72
131
|
host: HostId | null;
|
|
132
|
+
/**
|
|
133
|
+
* The registry this Claude session was found in, when it is not the
|
|
134
|
+
* default: the directory's last component (`floh` for
|
|
135
|
+
* `~/.config/argus/auth/claude/floh`). **Absent for the default registry,
|
|
136
|
+
* and always for Codex** — so a session that runs as the machine's own login
|
|
137
|
+
* reads exactly as it did before registries were counted.
|
|
138
|
+
*/
|
|
139
|
+
profile?: string;
|
|
73
140
|
}
|
|
74
141
|
/** The probes, injectable so the selection logic is testable against fixtures. */
|
|
75
142
|
export interface SessionProbes {
|
|
76
143
|
ps(): Promise<string>;
|
|
77
|
-
|
|
144
|
+
/**
|
|
145
|
+
* `claude agents --json` against one registry. `null` asks with
|
|
146
|
+
* `CLAUDE_CONFIG_DIR` UNSET — the machine's own login, which must never be
|
|
147
|
+
* named (see the file comment); a string asks with it set to that directory.
|
|
148
|
+
*/
|
|
149
|
+
claudeAgents(configDir: string | null): Promise<string>;
|
|
78
150
|
lsofCwd(pid: number): Promise<string>;
|
|
151
|
+
/**
|
|
152
|
+
* Where else to look for Claude registries. **Optional**: without it only
|
|
153
|
+
* the default registry is read, which is exactly what detection did before
|
|
154
|
+
* #1431 — so a probe set that predates it keeps its old meaning.
|
|
155
|
+
*/
|
|
156
|
+
registries?: RegistryProbes;
|
|
157
|
+
}
|
|
158
|
+
/** The machine facts {@link discoverRegistries} reads, injectable like every other probe. */
|
|
159
|
+
export interface RegistryProbes {
|
|
160
|
+
/** The home directory, for `~/.claude` and the Argus profile root. */
|
|
161
|
+
home: string;
|
|
162
|
+
/** The caller's own `CLAUDE_CONFIG_DIR`, or null when it has none — which makes the default `~/.claude`. */
|
|
163
|
+
own: string | null;
|
|
164
|
+
/** Every directory under the Argus Claude profile root (`~/.config/argus/auth/claude/*`). Empty without Argus. */
|
|
165
|
+
argusProfiles(): Promise<string[]>;
|
|
166
|
+
/** `ps -E -ww -o pid=,command= -p …` over these pids — the environments they were started with. */
|
|
167
|
+
environments(pids: readonly number[]): Promise<string>;
|
|
168
|
+
/** Does `<dir>/sessions` hold at least one `*.json` — has any session ever registered here? */
|
|
169
|
+
hasSessions(dir: string): Promise<boolean>;
|
|
170
|
+
}
|
|
171
|
+
/** One place Claude Code registers sessions: a config directory, and how to ask it. */
|
|
172
|
+
export interface Registry {
|
|
173
|
+
/** The config directory. */
|
|
174
|
+
dir: string;
|
|
175
|
+
/** What `CLAUDE_CONFIG_DIR` is set to when asking it; null is unset (the machine's own login). */
|
|
176
|
+
configDir: string | null;
|
|
177
|
+
/** The label its rows carry; null for the default registry, whose rows carry none. */
|
|
178
|
+
profile: string | null;
|
|
179
|
+
/** How it was learned — the order they are listed in, and the order a duplicate pid resolves in. */
|
|
180
|
+
via: "default" | "ambient" | "argus" | "host" | "process";
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* A running host whose MAIN process carries a `CLAUDE_CONFIG_DIR` other than
|
|
184
|
+
* the default — so every terminal it opens inherits it, and a session STARTED
|
|
185
|
+
* there runs as that login. Said out loud in `doctor` and in the delegate
|
|
186
|
+
* detail (#1431 ▸ 3): starting one is unchanged, but it should not be a
|
|
187
|
+
* surprise.
|
|
188
|
+
*/
|
|
189
|
+
export interface CarriedProfile {
|
|
190
|
+
host: HostId;
|
|
191
|
+
pid: number;
|
|
192
|
+
/** The directory, as the process carries it. */
|
|
193
|
+
dir: string;
|
|
194
|
+
/** Its last component — the same label a session found there carries. */
|
|
195
|
+
profile: string;
|
|
79
196
|
}
|
|
80
197
|
/**
|
|
81
198
|
* What detection found, and whether it could be trusted to be complete.
|
|
@@ -91,8 +208,64 @@ export interface Detection {
|
|
|
91
208
|
incomplete: boolean;
|
|
92
209
|
/** One clause naming what failed, for the detail. Null when nothing did. */
|
|
93
210
|
detail: string | null;
|
|
211
|
+
/**
|
|
212
|
+
* The running hosts that would start a Claude session as another login —
|
|
213
|
+
* see {@link CarriedProfile}. Read for Claude only, because the variable
|
|
214
|
+
* means nothing to Codex; empty when no host carries one.
|
|
215
|
+
*/
|
|
216
|
+
carried: CarriedProfile[];
|
|
94
217
|
}
|
|
218
|
+
/** Where Argus keeps its Claude agent accounts, one config directory each, relative to the home directory. */
|
|
219
|
+
export declare const ARGUS_CLAUDE_PROFILES: string;
|
|
220
|
+
/** The caller's `CLAUDE_CONFIG_DIR`, or null when it has none — a blank value counts as none. */
|
|
221
|
+
export declare function configDirOf(env: NodeJS.ProcessEnv): string | null;
|
|
95
222
|
export declare function systemProbes(env: NodeJS.ProcessEnv, claudePath: string | null): SessionProbes;
|
|
223
|
+
/**
|
|
224
|
+
* Every Claude registry worth asking on this machine, default first — and the
|
|
225
|
+
* hosts that would start a session as another login.
|
|
226
|
+
*
|
|
227
|
+
* In order, which is also the order a pid found twice resolves in:
|
|
228
|
+
*
|
|
229
|
+
* 1. **the default** — the caller's `CLAUDE_CONFIG_DIR`, or `~/.claude`. Always
|
|
230
|
+
* asked, exactly as detection always asked it. Its rows carry no profile.
|
|
231
|
+
* 2. **`~/.claude`**, when the caller named another default: the machine's own
|
|
232
|
+
* login, asked with the variable unset.
|
|
233
|
+
* 3. **every Argus agent account** — `~/.config/argus/auth/claude/<name>`.
|
|
234
|
+
* 4. **the `CLAUDE_CONFIG_DIR` a running host carries** — both VS Codes, read
|
|
235
|
+
* from the main process's environment, because their terminals inherit it.
|
|
236
|
+
* This is the measured case. Terminal is asked too and answers nothing on
|
|
237
|
+
* macOS 26: it is a platform binary, whose environment `ps -E` does not see.
|
|
238
|
+
* 5. **the `CLAUDE_CONFIG_DIR` a running `claude` carries** — the same read, one
|
|
239
|
+
* level down. It costs nothing (the same `ps -E` call) and it catches what
|
|
240
|
+
* the host cannot show: every session in Terminal, a terminal that exported
|
|
241
|
+
* the variable itself, a VS Code whose `terminal.integrated.env` sets it.
|
|
242
|
+
*
|
|
243
|
+
* Every one past the first is de-duplicated by canonical path and asked only
|
|
244
|
+
* when it holds `sessions/*.json` (the file comment says why). A host carrying
|
|
245
|
+
* a directory nobody has registered in is still REPORTED in `carried`: the
|
|
246
|
+
* next session it starts will be the first.
|
|
247
|
+
*
|
|
248
|
+
* `failed` names the reads that could not be made. The profile root and `ps -E`
|
|
249
|
+
* failing each make the list possibly short, so the caller says so.
|
|
250
|
+
*/
|
|
251
|
+
export declare function discoverRegistries(rows: readonly ProcessRow[], probes: RegistryProbes): Promise<{
|
|
252
|
+
registries: Registry[];
|
|
253
|
+
carried: CarriedProfile[];
|
|
254
|
+
failed: string[];
|
|
255
|
+
}>;
|
|
256
|
+
/**
|
|
257
|
+
* The hosts running right now with a non-default `CLAUDE_CONFIG_DIR` — the
|
|
258
|
+
* half of {@link discoverRegistries} that `doctor` needs, without asking any
|
|
259
|
+
* registry anything.
|
|
260
|
+
*/
|
|
261
|
+
export declare function carriedProfiles(probes: SessionProbes): Promise<CarriedProfile[]>;
|
|
262
|
+
/**
|
|
263
|
+
* What a row, a doctor note and a launch say about a host that carries a
|
|
264
|
+
* profile — one sentence, written once, because three surfaces say it.
|
|
265
|
+
*/
|
|
266
|
+
export declare function carriedSentence(carried: CarriedProfile, home: string): string;
|
|
267
|
+
/** A registry's label: the directory's last component, trailing slashes ignored. */
|
|
268
|
+
export declare function profileOf(dir: string): string;
|
|
96
269
|
/**
|
|
97
270
|
* Every session of `agent` whose working directory is `folder`, with its host.
|
|
98
271
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/delegate/sessions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/delegate/sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { realpath } from "node:fs/promises";
|
|
3
|
-
import {
|
|
2
|
+
import { readdir, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, isAbsolute, join } from "node:path";
|
|
4
5
|
import { promisify } from "node:util";
|
|
5
|
-
import { hostOfCommand } from "./hosts.js";
|
|
6
|
+
import { HOSTS, hostOfCommand, mainHostOfCommand } from "./hosts.js";
|
|
6
7
|
const run = promisify(execFile);
|
|
7
8
|
/**
|
|
8
9
|
* `ps -axo pid,ppid,tty,comm`, parsed.
|
|
@@ -69,6 +70,45 @@ export function parseLsofCwd(text) {
|
|
|
69
70
|
}
|
|
70
71
|
return null;
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* One variable out of `ps -E -ww -o pid=,command= -p …`, per pid.
|
|
75
|
+
*
|
|
76
|
+
* `ps -E` prints each process's command line and then the environment it was
|
|
77
|
+
* started with, all space-separated, so a line is ambiguous in general — an
|
|
78
|
+
* argument or a value may itself contain spaces. Two rules make it good enough
|
|
79
|
+
* for the one variable read here:
|
|
80
|
+
*
|
|
81
|
+
* - **the LAST ` NAME=` on the line wins**, because the environment is printed
|
|
82
|
+
* after the arguments, and an argument that merely mentions the variable (a
|
|
83
|
+
* prompt, say) comes first;
|
|
84
|
+
* - **a value runs until the next ` IDENT=`**, so a path with a space in it
|
|
85
|
+
* survives and the variable after it is not swallowed.
|
|
86
|
+
*
|
|
87
|
+
* Only absolute values are kept: `claude` would resolve a relative one against
|
|
88
|
+
* its own working directory, which is not this process's to know.
|
|
89
|
+
*
|
|
90
|
+
* Everything else on the line — which is the process's whole environment,
|
|
91
|
+
* tokens included — is dropped here and never printed by anything.
|
|
92
|
+
*/
|
|
93
|
+
export function parseEnvironments(text, name) {
|
|
94
|
+
const found = new Map();
|
|
95
|
+
const marker = ` ${name}=`;
|
|
96
|
+
for (const line of text.split("\n")) {
|
|
97
|
+
const match = /^\s*(\d+)\s(.*)$/.exec(line);
|
|
98
|
+
if (match === null)
|
|
99
|
+
continue;
|
|
100
|
+
const rest = match[2];
|
|
101
|
+
const at = rest.lastIndexOf(marker);
|
|
102
|
+
if (at < 0)
|
|
103
|
+
continue;
|
|
104
|
+
const tail = rest.slice(at + marker.length);
|
|
105
|
+
const next = / [A-Za-z_][A-Za-z0-9_]*=/.exec(tail);
|
|
106
|
+
const value = (next === null ? tail : tail.slice(0, next.index)).trim();
|
|
107
|
+
if (value !== "" && isAbsolute(value))
|
|
108
|
+
found.set(Number(match[1]), value);
|
|
109
|
+
}
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
72
112
|
/** The ancestors of a pid, nearest first, stopping at launchd or a cycle. */
|
|
73
113
|
export function ancestry(pid, rows) {
|
|
74
114
|
const byPid = new Map(rows.map((row) => [row.pid, row]));
|
|
@@ -94,24 +134,199 @@ export function hostOfPid(pid, rows) {
|
|
|
94
134
|
}
|
|
95
135
|
return null;
|
|
96
136
|
}
|
|
137
|
+
/** Where Argus keeps its Claude agent accounts, one config directory each, relative to the home directory. */
|
|
138
|
+
export const ARGUS_CLAUDE_PROFILES = join(".config", "argus", "auth", "claude");
|
|
139
|
+
/** The caller's `CLAUDE_CONFIG_DIR`, or null when it has none — a blank value counts as none. */
|
|
140
|
+
export function configDirOf(env) {
|
|
141
|
+
const value = (env["CLAUDE_CONFIG_DIR"] ?? "").trim();
|
|
142
|
+
return value === "" ? null : value;
|
|
143
|
+
}
|
|
97
144
|
export function systemProbes(env, claudePath) {
|
|
145
|
+
const home = env["HOME"] ?? homedir();
|
|
98
146
|
return {
|
|
99
147
|
ps: async () => (await run("/bin/ps", ["-axo", "pid,ppid,tty,comm"], { timeout: 10_000, maxBuffer: 8 * 1024 * 1024 })).stdout,
|
|
100
|
-
claudeAgents: async () => {
|
|
148
|
+
claudeAgents: async (configDir) => {
|
|
101
149
|
if (claudePath === null)
|
|
102
150
|
return "[]";
|
|
103
151
|
// Bounded and given the caller's env: the same rule as every probe in
|
|
104
|
-
// `doctor` — it must ask the machine the report describes.
|
|
152
|
+
// `doctor` — it must ask the machine the report describes. The one key
|
|
153
|
+
// changed is the registry, and the default is asked with the caller's
|
|
154
|
+
// own value, so that call is the one detection always made.
|
|
105
155
|
const { stdout } = await run(claudePath, ["agents", "--json"], {
|
|
106
156
|
timeout: 20_000,
|
|
107
157
|
maxBuffer: 4 * 1024 * 1024,
|
|
108
|
-
env,
|
|
158
|
+
env: withConfigDir(env, configDir),
|
|
109
159
|
});
|
|
110
160
|
return stdout;
|
|
111
161
|
},
|
|
112
162
|
lsofCwd: async (pid) => (await run("/usr/sbin/lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], { timeout: 10_000 })).stdout,
|
|
163
|
+
registries: {
|
|
164
|
+
home,
|
|
165
|
+
own: configDirOf(env),
|
|
166
|
+
argusProfiles: async () => {
|
|
167
|
+
const root = join(home, ARGUS_CLAUDE_PROFILES);
|
|
168
|
+
let names;
|
|
169
|
+
try {
|
|
170
|
+
names = await readdir(root);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
// No Argus on this machine is an answer, not a failure.
|
|
174
|
+
if (error.code === "ENOENT")
|
|
175
|
+
return [];
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
const dirs = [];
|
|
179
|
+
for (const name of names.sort()) {
|
|
180
|
+
const dir = join(root, name);
|
|
181
|
+
if (await isDirectory(dir))
|
|
182
|
+
dirs.push(dir);
|
|
183
|
+
}
|
|
184
|
+
return dirs;
|
|
185
|
+
},
|
|
186
|
+
environments: async (pids) => {
|
|
187
|
+
if (pids.length === 0)
|
|
188
|
+
return "";
|
|
189
|
+
// `-ww` so no line is cut at a terminal width; the environment is the
|
|
190
|
+
// tail of the line, which is exactly what a cut would lose.
|
|
191
|
+
return run("/bin/ps", ["-E", "-ww", "-o", "pid=,command=", "-p", pids.join(",")], {
|
|
192
|
+
timeout: 10_000,
|
|
193
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
194
|
+
}).then((result) => result.stdout, (error) => {
|
|
195
|
+
// Measured: a pid that has gone since the table was read is left
|
|
196
|
+
// out and the call exits 0; only when EVERY listed pid has gone
|
|
197
|
+
// does it exit 1, with nothing printed — which is an answer.
|
|
198
|
+
if (error.code === 1 && typeof error.stdout === "string")
|
|
199
|
+
return error.stdout;
|
|
200
|
+
throw error;
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
hasSessions: async (dir) => {
|
|
204
|
+
try {
|
|
205
|
+
return (await readdir(join(dir, "sessions"))).some((name) => name.endsWith(".json"));
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
},
|
|
113
212
|
};
|
|
114
213
|
}
|
|
214
|
+
/** The caller's environment with the registry swapped in — or taken out, for the machine's own login. */
|
|
215
|
+
function withConfigDir(env, configDir) {
|
|
216
|
+
const next = { ...env };
|
|
217
|
+
if (configDir === null)
|
|
218
|
+
delete next["CLAUDE_CONFIG_DIR"];
|
|
219
|
+
else
|
|
220
|
+
next["CLAUDE_CONFIG_DIR"] = configDir;
|
|
221
|
+
return next;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Every Claude registry worth asking on this machine, default first — and the
|
|
225
|
+
* hosts that would start a session as another login.
|
|
226
|
+
*
|
|
227
|
+
* In order, which is also the order a pid found twice resolves in:
|
|
228
|
+
*
|
|
229
|
+
* 1. **the default** — the caller's `CLAUDE_CONFIG_DIR`, or `~/.claude`. Always
|
|
230
|
+
* asked, exactly as detection always asked it. Its rows carry no profile.
|
|
231
|
+
* 2. **`~/.claude`**, when the caller named another default: the machine's own
|
|
232
|
+
* login, asked with the variable unset.
|
|
233
|
+
* 3. **every Argus agent account** — `~/.config/argus/auth/claude/<name>`.
|
|
234
|
+
* 4. **the `CLAUDE_CONFIG_DIR` a running host carries** — both VS Codes, read
|
|
235
|
+
* from the main process's environment, because their terminals inherit it.
|
|
236
|
+
* This is the measured case. Terminal is asked too and answers nothing on
|
|
237
|
+
* macOS 26: it is a platform binary, whose environment `ps -E` does not see.
|
|
238
|
+
* 5. **the `CLAUDE_CONFIG_DIR` a running `claude` carries** — the same read, one
|
|
239
|
+
* level down. It costs nothing (the same `ps -E` call) and it catches what
|
|
240
|
+
* the host cannot show: every session in Terminal, a terminal that exported
|
|
241
|
+
* the variable itself, a VS Code whose `terminal.integrated.env` sets it.
|
|
242
|
+
*
|
|
243
|
+
* Every one past the first is de-duplicated by canonical path and asked only
|
|
244
|
+
* when it holds `sessions/*.json` (the file comment says why). A host carrying
|
|
245
|
+
* a directory nobody has registered in is still REPORTED in `carried`: the
|
|
246
|
+
* next session it starts will be the first.
|
|
247
|
+
*
|
|
248
|
+
* `failed` names the reads that could not be made. The profile root and `ps -E`
|
|
249
|
+
* failing each make the list possibly short, so the caller says so.
|
|
250
|
+
*/
|
|
251
|
+
export async function discoverRegistries(rows, probes) {
|
|
252
|
+
const ambient = join(probes.home, ".claude");
|
|
253
|
+
const first = { dir: probes.own ?? ambient, configDir: probes.own, profile: null, via: "default" };
|
|
254
|
+
const defaultKey = await canonical(first.dir);
|
|
255
|
+
const failed = [];
|
|
256
|
+
const candidates = [];
|
|
257
|
+
if (probes.own !== null)
|
|
258
|
+
candidates.push({ dir: ambient, configDir: null, via: "ambient" });
|
|
259
|
+
try {
|
|
260
|
+
for (const dir of await probes.argusProfiles())
|
|
261
|
+
candidates.push({ dir, configDir: dir, via: "argus" });
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
failed.push(`the Argus profiles under ~/${ARGUS_CLAUDE_PROFILES} could not be listed: ${messageOf(error)}`);
|
|
265
|
+
}
|
|
266
|
+
const mains = rows.filter((row) => mainHostOfCommand(row.comm) !== null);
|
|
267
|
+
const claudes = rows.filter((row) => basename(row.comm) === "claude");
|
|
268
|
+
let carriedBy = new Map();
|
|
269
|
+
try {
|
|
270
|
+
carriedBy = parseEnvironments(await probes.environments([...mains, ...claudes].map((row) => row.pid)), "CLAUDE_CONFIG_DIR");
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
failed.push(`the environment of the running hosts could not be read (\`ps -E\`): ${messageOf(error)}`);
|
|
274
|
+
}
|
|
275
|
+
const carried = [];
|
|
276
|
+
for (const row of mains) {
|
|
277
|
+
const dir = carriedBy.get(row.pid);
|
|
278
|
+
if (dir === undefined)
|
|
279
|
+
continue;
|
|
280
|
+
candidates.push({ dir, configDir: dir, via: "host" });
|
|
281
|
+
if ((await canonical(dir)) !== defaultKey) {
|
|
282
|
+
carried.push({ host: mainHostOfCommand(row.comm), pid: row.pid, dir, profile: profileOf(dir) });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const row of claudes) {
|
|
286
|
+
const dir = carriedBy.get(row.pid);
|
|
287
|
+
if (dir !== undefined)
|
|
288
|
+
candidates.push({ dir, configDir: dir, via: "process" });
|
|
289
|
+
}
|
|
290
|
+
const registries = [first];
|
|
291
|
+
const seen = new Set([defaultKey]);
|
|
292
|
+
for (const candidate of candidates) {
|
|
293
|
+
const key = await canonical(candidate.dir);
|
|
294
|
+
if (seen.has(key))
|
|
295
|
+
continue;
|
|
296
|
+
seen.add(key);
|
|
297
|
+
if (!(await probes.hasSessions(candidate.dir)))
|
|
298
|
+
continue;
|
|
299
|
+
registries.push({ ...candidate, profile: profileOf(candidate.dir) });
|
|
300
|
+
}
|
|
301
|
+
return { registries, carried, failed };
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The hosts running right now with a non-default `CLAUDE_CONFIG_DIR` — the
|
|
305
|
+
* half of {@link discoverRegistries} that `doctor` needs, without asking any
|
|
306
|
+
* registry anything.
|
|
307
|
+
*/
|
|
308
|
+
export async function carriedProfiles(probes) {
|
|
309
|
+
if (probes.registries === undefined)
|
|
310
|
+
return [];
|
|
311
|
+
const rows = parsePs(await probes.ps());
|
|
312
|
+
// Only the host rows are handed over: `carried` is about what a host would
|
|
313
|
+
// START, and reading every running session's environment would buy nothing.
|
|
314
|
+
const mains = rows.filter((row) => mainHostOfCommand(row.comm) !== null);
|
|
315
|
+
return (await discoverRegistries(mains, { ...probes.registries, argusProfiles: async () => [] })).carried;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* What a row, a doctor note and a launch say about a host that carries a
|
|
319
|
+
* profile — one sentence, written once, because three surfaces say it.
|
|
320
|
+
*/
|
|
321
|
+
export function carriedSentence(carried, home) {
|
|
322
|
+
const shown = carried.dir === home || carried.dir.startsWith(`${home}/`) ? `~${carried.dir.slice(home.length)}` : carried.dir;
|
|
323
|
+
return (`${HOSTS[carried.host].name} was started with CLAUDE_CONFIG_DIR=${shown}, so sessions it starts run as that `
|
|
324
|
+
+ `login (profile ${carried.profile}); relaunch it from the Dock for the default account.`);
|
|
325
|
+
}
|
|
326
|
+
/** A registry's label: the directory's last component, trailing slashes ignored. */
|
|
327
|
+
export function profileOf(dir) {
|
|
328
|
+
return basename(dir.replace(/\/+$/, "")) || dir;
|
|
329
|
+
}
|
|
115
330
|
/**
|
|
116
331
|
* Every session of `agent` whose working directory is `folder`, with its host.
|
|
117
332
|
*
|
|
@@ -124,29 +339,59 @@ export async function findSessions(agent, folder, probes) {
|
|
|
124
339
|
const target = await canonical(folder);
|
|
125
340
|
const sessions = [];
|
|
126
341
|
if (agent === "claude") {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
342
|
+
const { registries, carried, failed } = probes.registries === undefined
|
|
343
|
+
? { registries: [{ dir: "", configDir: null, profile: null, via: "default" }], carried: [], failed: [] }
|
|
344
|
+
: await discoverRegistries(rows, probes.registries);
|
|
345
|
+
// One `claude agents --json` per registry, all at once: each is ~0.2 s of
|
|
346
|
+
// its own process, and none depends on another.
|
|
347
|
+
const answers = await Promise.all(registries.map(async (registry) => {
|
|
348
|
+
try {
|
|
349
|
+
return { registry, agents: parseClaudeAgents(await probes.claudeAgents(registry.configDir)), failure: null };
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
// A `claude` that cannot list a registry leaves us unable to tell an
|
|
353
|
+
// idle session there from none at all — the caller says so, and
|
|
354
|
+
// keeps what the other registries answered.
|
|
355
|
+
return {
|
|
356
|
+
registry,
|
|
357
|
+
agents: [],
|
|
358
|
+
failure: registry.profile === null
|
|
359
|
+
? `\`claude agents --json\` could not be read: ${messageOf(error)}`
|
|
360
|
+
: `\`claude agents --json\` could not be read for profile ${registry.profile}: ${messageOf(error)}`,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}));
|
|
364
|
+
// Merged in registry order, and a pid listed twice keeps its FIRST
|
|
365
|
+
// registry — the default before any profile. The same process cannot be
|
|
366
|
+
// registered as two sessions; two directories that resolve to one
|
|
367
|
+
// registry, or a stale file beside a live one, can make it look so.
|
|
368
|
+
const seen = new Set();
|
|
369
|
+
for (const { registry, agents } of answers) {
|
|
370
|
+
for (const found of agents) {
|
|
371
|
+
if (seen.has(found.pid))
|
|
372
|
+
continue;
|
|
373
|
+
seen.add(found.pid);
|
|
374
|
+
if ((await canonical(found.cwd)) !== target)
|
|
375
|
+
continue;
|
|
376
|
+
sessions.push({
|
|
377
|
+
agent,
|
|
378
|
+
pid: found.pid,
|
|
379
|
+
cwd: found.cwd,
|
|
380
|
+
name: found.name ?? null,
|
|
381
|
+
status: found.status === "idle" || found.status === "busy" ? found.status : null,
|
|
382
|
+
tty: rows.find((row) => row.pid === found.pid)?.tty ?? null,
|
|
383
|
+
host: hostOfPid(found.pid, rows),
|
|
384
|
+
...(registry.profile === null ? {} : { profile: registry.profile }),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
135
387
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
name: found.name ?? null,
|
|
144
|
-
status: found.status === "idle" || found.status === "busy" ? found.status : null,
|
|
145
|
-
tty: rows.find((row) => row.pid === found.pid)?.tty ?? null,
|
|
146
|
-
host: hostOfPid(found.pid, rows),
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
return { sessions, incomplete: false, detail: null };
|
|
388
|
+
const failures = [...failed, ...answers.flatMap((answer) => (answer.failure === null ? [] : [answer.failure]))];
|
|
389
|
+
return {
|
|
390
|
+
sessions,
|
|
391
|
+
incomplete: failures.length > 0,
|
|
392
|
+
detail: failures.length === 0 ? null : failures.join("; "),
|
|
393
|
+
carried,
|
|
394
|
+
};
|
|
150
395
|
}
|
|
151
396
|
// Codex: the TUI process is `codex`; `codex-code-mode-host` and the ChatGPT
|
|
152
397
|
// app's bundled helpers are not sessions, and the host walk drops the app's
|
|
@@ -182,6 +427,7 @@ export async function findSessions(agent, folder, probes) {
|
|
|
182
427
|
sessions,
|
|
183
428
|
incomplete: lsofFailed > 0,
|
|
184
429
|
detail: lsofFailed > 0 ? `the working directory of ${lsofFailed} codex process(es) could not be read (\`lsof\`)` : null,
|
|
430
|
+
carried: [],
|
|
185
431
|
};
|
|
186
432
|
}
|
|
187
433
|
/**
|
|
@@ -226,4 +472,12 @@ async function canonical(path) {
|
|
|
226
472
|
return trimmed;
|
|
227
473
|
}
|
|
228
474
|
}
|
|
475
|
+
async function isDirectory(path) {
|
|
476
|
+
try {
|
|
477
|
+
return (await stat(path)).isDirectory();
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
229
483
|
//# sourceMappingURL=sessions.js.map
|