@floh-solutions/pharos-cli 0.29.0 → 0.31.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.
Files changed (45) hide show
  1. package/README.md +33 -0
  2. package/dist/capabilities.d.ts +7 -0
  3. package/dist/capabilities.d.ts.map +1 -1
  4. package/dist/capabilities.js +25 -0
  5. package/dist/capabilities.js.map +1 -1
  6. package/dist/cli.d.ts +18 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +193 -86
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/delegate.d.ts +248 -0
  11. package/dist/commands/delegate.d.ts.map +1 -0
  12. package/dist/commands/delegate.js +582 -0
  13. package/dist/commands/delegate.js.map +1 -0
  14. package/dist/commands/doctor.d.ts +8 -0
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +179 -4
  17. package/dist/commands/doctor.js.map +1 -1
  18. package/dist/commands/setup.d.ts.map +1 -1
  19. package/dist/commands/setup.js +7 -3
  20. package/dist/commands/setup.js.map +1 -1
  21. package/dist/delegate/hosts.d.ts +83 -0
  22. package/dist/delegate/hosts.d.ts.map +1 -0
  23. package/dist/delegate/hosts.js +154 -0
  24. package/dist/delegate/hosts.js.map +1 -0
  25. package/dist/delegate/quote.d.ts +78 -0
  26. package/dist/delegate/quote.d.ts.map +1 -0
  27. package/dist/delegate/quote.js +134 -0
  28. package/dist/delegate/quote.js.map +1 -0
  29. package/dist/delegate/sessions.d.ts +121 -0
  30. package/dist/delegate/sessions.d.ts.map +1 -0
  31. package/dist/delegate/sessions.js +229 -0
  32. package/dist/delegate/sessions.js.map +1 -0
  33. package/dist/delegate/vsix.d.ts +122 -0
  34. package/dist/delegate/vsix.d.ts.map +1 -0
  35. package/dist/delegate/vsix.js +329 -0
  36. package/dist/delegate/vsix.js.map +1 -0
  37. package/package.json +5 -4
  38. package/skill/SKILL.md +47 -0
  39. package/vscode/README.md +30 -0
  40. package/vscode/pharos-bridge.json +10 -0
  41. package/vscode/pharos-bridge.vsix +0 -0
  42. package/dist/commands/pr.d.ts +0 -56
  43. package/dist/commands/pr.d.ts.map +0 -1
  44. package/dist/commands/pr.js +0 -202
  45. package/dist/commands/pr.js.map +0 -1
@@ -0,0 +1,154 @@
1
+ import { execFile } from "node:child_process";
2
+ import { access, constants } from "node:fs/promises";
3
+ import { homedir, platform } from "node:os";
4
+ import { join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ const run = promisify(execFile);
7
+ export const HOST_IDS = ["terminal", "vscode", "vscode-insiders", "navarch"];
8
+ export const AGENT_IDS = ["claude", "codex"];
9
+ export const MODES = ["terminal", "extension"];
10
+ export const HOSTS = {
11
+ terminal: {
12
+ id: "terminal",
13
+ name: "Terminal",
14
+ bundleId: "com.apple.Terminal",
15
+ candidates: [
16
+ "/System/Applications/Utilities/Terminal.app",
17
+ "/Applications/Utilities/Terminal.app",
18
+ ],
19
+ },
20
+ vscode: {
21
+ id: "vscode",
22
+ name: "Visual Studio Code",
23
+ bundleId: "com.microsoft.VSCode",
24
+ candidates: ["/Applications/Visual Studio Code.app", "~/Applications/Visual Studio Code.app"],
25
+ cask: "visual-studio-code",
26
+ vscode: {
27
+ extensionsDir: ".vscode/extensions",
28
+ scheme: "vscode",
29
+ helper: ["Visual Studio Code.app", "Code Helper"],
30
+ },
31
+ },
32
+ "vscode-insiders": {
33
+ id: "vscode-insiders",
34
+ name: "Visual Studio Code - Insiders",
35
+ bundleId: "com.microsoft.VSCodeInsiders",
36
+ candidates: [
37
+ "/Applications/Visual Studio Code - Insiders.app",
38
+ "~/Applications/Visual Studio Code - Insiders.app",
39
+ ],
40
+ cask: "visual-studio-code@insiders",
41
+ vscode: {
42
+ extensionsDir: ".vscode-insiders/extensions",
43
+ scheme: "vscode-insiders",
44
+ helper: ["Visual Studio Code - Insiders.app", "Code - Insiders Helper"],
45
+ },
46
+ },
47
+ navarch: {
48
+ id: "navarch",
49
+ name: "Navarch",
50
+ bundleId: "com.argus.app",
51
+ candidates: ["/Applications/Navarch.app", "~/Applications/Navarch.app"],
52
+ },
53
+ };
54
+ export function isHostId(value) {
55
+ return HOST_IDS.includes(value);
56
+ }
57
+ export function isAgentId(value) {
58
+ return AGENT_IDS.includes(value);
59
+ }
60
+ export function isMode(value) {
61
+ return MODES.includes(value);
62
+ }
63
+ /**
64
+ * Is the app installed, where, and which version. Never throws — absence is an
65
+ * answer. Always `null` off macOS: every host here is a macOS application.
66
+ */
67
+ export async function findApp(host, env = process.env) {
68
+ if (platform() !== "darwin")
69
+ return null;
70
+ const spec = HOSTS[host];
71
+ const found = await spotlight(spec.bundleId);
72
+ if (found !== null)
73
+ return { path: found, version: await bundleVersion(found), via: "spotlight" };
74
+ const home = env["HOME"] ?? homedir();
75
+ for (const candidate of spec.candidates) {
76
+ const path = candidate.startsWith("~/") ? join(home, candidate.slice(2)) : candidate;
77
+ if (await exists(join(path, "Contents", "Info.plist"))) {
78
+ return { path, version: await bundleVersion(path), via: "path" };
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ /**
84
+ * Spotlight's answer, or null. Several bundles can carry one id (a copy on an
85
+ * external disk, an old version in the Trash); `/Applications` wins, then the
86
+ * first the index lists.
87
+ */
88
+ async function spotlight(bundleId) {
89
+ try {
90
+ const { stdout } = await run("/usr/bin/mdfind", [`kMDItemCFBundleIdentifier == '${bundleId}'`], { timeout: 5_000, maxBuffer: 1024 * 1024 });
91
+ const paths = stdout
92
+ .split("\n")
93
+ .map((line) => line.trim())
94
+ .filter((line) => line.endsWith(".app") && !line.includes("/.Trash/"));
95
+ if (paths.length === 0)
96
+ return null;
97
+ return paths.find((path) => path.startsWith("/Applications/") || path.startsWith("/System/")) ?? paths[0];
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ /** `CFBundleShortVersionString`, read the way scripts are meant to: `plutil -extract … raw`. */
104
+ export async function bundleVersion(appPath) {
105
+ try {
106
+ const { stdout } = await run("/usr/bin/plutil", ["-extract", "CFBundleShortVersionString", "raw", "-o", "-", join(appPath, "Contents", "Info.plist")], { timeout: 5_000 });
107
+ const version = stdout.trim();
108
+ return version === "" ? null : version;
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ /**
115
+ * The CLI inside a VS Code bundle.
116
+ *
117
+ * `code` is often NOT on PATH — it was not on the reference machine — because
118
+ * putting it there is a menu action ("Install 'code' command in PATH") that
119
+ * most people never take. The bundle always carries it, at the same relative
120
+ * path in both flavours.
121
+ */
122
+ export function bundleCli(appPath) {
123
+ return join(appPath, "Contents", "Resources", "app", "bin", "code");
124
+ }
125
+ /**
126
+ * Which host a process belongs to, from its command as `ps` prints it.
127
+ *
128
+ * Measured chains on the reference machine: a Terminal tab is
129
+ * `login ← Terminal.app/Contents/MacOS/Terminal`; a Navarch worker is
130
+ * `zsh ← argusd`; a VS Code integrated terminal's shell hangs off a
131
+ * `Code - Insiders Helper (Plugin)` process inside the bundle. Insiders is
132
+ * tested before stable because its names contain the stable ones.
133
+ */
134
+ export function hostOfCommand(comm) {
135
+ if (comm.endsWith("Terminal.app/Contents/MacOS/Terminal"))
136
+ return "terminal";
137
+ for (const id of ["vscode-insiders", "vscode"]) {
138
+ if (HOSTS[id].vscode.helper.some((marker) => comm.includes(marker)))
139
+ return id;
140
+ }
141
+ if (comm.endsWith("/argusd") || comm === "argusd" || comm.includes("Navarch.app/"))
142
+ return "navarch";
143
+ return null;
144
+ }
145
+ async function exists(path) {
146
+ try {
147
+ await access(path, constants.R_OK);
148
+ return true;
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ }
154
+ //# sourceMappingURL=hosts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hosts.js","sourceRoot":"","sources":["../../src/delegate/hosts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Getting a prompt into a terminal without a shell or AppleScript reading it.
3
+ *
4
+ * Two languages sit between `pharos delegate` and the agent, and each has its
5
+ * own idea of what a quote, a dollar or a backtick means:
6
+ *
7
+ * 1. **AppleScript**, which carries the text to Terminal.app. It is NOT
8
+ * quoted at all: the script sources below take the text as an `argv`
9
+ * item (`osascript -e '…' -- <tty> <text>`), so nothing the person typed
10
+ * is ever spliced into AppleScript source. Measured: a value holding `"`,
11
+ * `'`, `$HOME`, a backtick and a backslash comes back from `on run argv`
12
+ * byte for byte. The alternative — escaping into a string literal — is a
13
+ * second quoting scheme to get wrong, and getting it wrong runs code.
14
+ * 2. **The shell**, for the ONE route that goes through one: a new Terminal
15
+ * window runs `cd '<folder>' && '<agent>' '<text>'` in the person's login
16
+ * shell. That string is built here with single quotes, which is the only
17
+ * shell quoting that means "no interpretation whatsoever" — inside `'…'`
18
+ * nothing is special except `'` itself, spelled `'\''`.
19
+ *
20
+ * Injecting into a RUNNING session goes through neither: `do script … in tab`
21
+ * writes the string to the tab's tty as typed input, so the agent's own input
22
+ * box reads it, and shells and scripts never see it.
23
+ */
24
+ /** POSIX single-quoting: safe in `sh`, `bash` and `zsh`, and nothing is special inside. */
25
+ export declare function shellQuote(value: string): string;
26
+ /**
27
+ * The command a NEW Terminal window runs when no session was found.
28
+ *
29
+ * The agent is spawned by its resolved absolute path rather than its name: a
30
+ * fresh login shell may not have run the nvm or Homebrew init that puts
31
+ * `claude` on PATH yet, and an absolute path does not care.
32
+ */
33
+ export declare function launchCommand(folder: string, agentPath: string, text: string): string;
34
+ /**
35
+ * Bracketed paste, as the terminal would wrap a real paste.
36
+ *
37
+ * A running agent submits on Enter, so typing a multi-line prompt into it
38
+ * sends the first line and leaves the rest as a second message. Wrapped in
39
+ * `ESC[200~ … ESC[201~` the same bytes arrive as ONE paste, which the input
40
+ * box inserts whole; the return `do script` appends afterwards then submits it.
41
+ * Navarch's `WorkerManager.sendLine` does the same thing for the same reason.
42
+ */
43
+ export declare const BRACKETED_PASTE: {
44
+ readonly open: "\u001B[200~";
45
+ readonly close: "\u001B[201~";
46
+ };
47
+ /** Newlines become spaces — the fallback when bracketed paste cannot be trusted. */
48
+ export declare function foldLines(text: string): string;
49
+ /**
50
+ * What to type into a running session.
51
+ *
52
+ * Single-line text is typed as-is. Multi-line text is wrapped in bracketed
53
+ * paste, or folded to one line when the caller has reason not to trust the
54
+ * paste. Which one the Terminal route uses is a measurement recorded on
55
+ * `commands/delegate.ts`.
56
+ *
57
+ * **C0 control bytes are stripped first**, keeping only tab and newline. This
58
+ * is what wraps the text in bracketed paste, so a raw `ESC[201~` (or any stray
59
+ * escape) in the prompt could otherwise close the paste early and have the rest
60
+ * reach the TUI as keystrokes. `\n` is kept — it is what decides bracketed vs.
61
+ * single-line — and `\t` is a legitimate character in a prompt.
62
+ */
63
+ export declare function asTypedInput(text: string, multiline?: "bracketed" | "fold"): string;
64
+ /**
65
+ * AppleScript sources. **No user text is ever interpolated into these** — a
66
+ * test asserts it — everything variable arrives through `argv`.
67
+ */
68
+ /**
69
+ * Type into the tab whose tty matches, bring it forward, and say what happened.
70
+ *
71
+ * `do script … in t` writes to that tab's tty as if typed and follows it with a
72
+ * return. Answers `sent`, or `no-tab` when no window has a tab on that tty —
73
+ * the session exists, but not in a Terminal.app this instance can see.
74
+ */
75
+ export declare const SEND_TO_TAB_SCRIPT = "on run argv\n set targetTty to item 1 of argv\n set theText to item 2 of argv\n tell application \"Terminal\"\n repeat with w in windows\n repeat with t in tabs of w\n if tty of t is targetTty then\n do script theText in t\n set selected tab of w to t\n set index of w to 1\n activate\n return \"sent\"\n end if\n end repeat\n end repeat\n end tell\n return \"no-tab\"\nend run";
76
+ /** A new window running one shell command, brought to the front. */
77
+ export declare const LAUNCH_SCRIPT = "on run argv\n set theCommand to item 1 of argv\n tell application \"Terminal\"\n do script theCommand\n activate\n end tell\n return \"launched\"\nend run";
78
+ //# sourceMappingURL=quote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote.d.ts","sourceRoot":"","sources":["../../src/delegate/quote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,2FAA2F;AAC3F,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAqBD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrF;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe;;;CAA+C,CAAC;AAE5E,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,WAAW,GAAG,MAAoB,GAAG,MAAM,CAOhG;AAED;;;GAGG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,8cAiBvB,CAAC;AAET,oEAAoE;AACpE,eAAO,MAAM,aAAa,yKAOlB,CAAC"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Getting a prompt into a terminal without a shell or AppleScript reading it.
3
+ *
4
+ * Two languages sit between `pharos delegate` and the agent, and each has its
5
+ * own idea of what a quote, a dollar or a backtick means:
6
+ *
7
+ * 1. **AppleScript**, which carries the text to Terminal.app. It is NOT
8
+ * quoted at all: the script sources below take the text as an `argv`
9
+ * item (`osascript -e '…' -- <tty> <text>`), so nothing the person typed
10
+ * is ever spliced into AppleScript source. Measured: a value holding `"`,
11
+ * `'`, `$HOME`, a backtick and a backslash comes back from `on run argv`
12
+ * byte for byte. The alternative — escaping into a string literal — is a
13
+ * second quoting scheme to get wrong, and getting it wrong runs code.
14
+ * 2. **The shell**, for the ONE route that goes through one: a new Terminal
15
+ * window runs `cd '<folder>' && '<agent>' '<text>'` in the person's login
16
+ * shell. That string is built here with single quotes, which is the only
17
+ * shell quoting that means "no interpretation whatsoever" — inside `'…'`
18
+ * nothing is special except `'` itself, spelled `'\''`.
19
+ *
20
+ * Injecting into a RUNNING session goes through neither: `do script … in tab`
21
+ * writes the string to the tab's tty as typed input, so the agent's own input
22
+ * box reads it, and shells and scripts never see it.
23
+ */
24
+ /** POSIX single-quoting: safe in `sh`, `bash` and `zsh`, and nothing is special inside. */
25
+ export function shellQuote(value) {
26
+ return `'${value.replace(/'/g, `'\\''`)}'`;
27
+ }
28
+ /**
29
+ * Clear this session's Claude Code identity from the launched shell, in the
30
+ * command itself.
31
+ *
32
+ * **The Terminal route cannot rely on scrubbing the spawner's environment.** A
33
+ * new Terminal tab inherits Terminal.app's environment, not `osascript`'s, so
34
+ * when Terminal was itself launched from inside a Claude session (measured:
35
+ * #1375, and note 4792) every tab already carries `CLAUDE_CODE_*` — and a
36
+ * `claude` started there registers as a *child* of that session, invisible to
37
+ * `claude agents --json` with its transcript off. Clearing the markers in the
38
+ * shell command fixes it wherever the command runs, independent of the ambient
39
+ * environment. `CLAUDE_CODE_SSE_PORT` is kept — it points a child at a shared
40
+ * MCP server, not at the parent's identity. `env`/`sed` are in `/usr/bin`, on
41
+ * every PATH; `__v` is unset last so it does not leak into the agent.
42
+ */
43
+ const CLEAR_CLAUDE_ENV = "for __v in $(env | sed -n 's/^\\(CLAUDE_CODE_[A-Za-z0-9_]*\\)=.*/\\1/p'); "
44
+ + 'do [ "$__v" = CLAUDE_CODE_SSE_PORT ] || unset "$__v"; done; unset CLAUDECODE CLAUDE_PID __v';
45
+ /**
46
+ * The command a NEW Terminal window runs when no session was found.
47
+ *
48
+ * The agent is spawned by its resolved absolute path rather than its name: a
49
+ * fresh login shell may not have run the nvm or Homebrew init that puts
50
+ * `claude` on PATH yet, and an absolute path does not care.
51
+ */
52
+ export function launchCommand(folder, agentPath, text) {
53
+ // `--` ends the agent's own option parsing, so a prompt that begins with a
54
+ // dash is the prompt and not a misread flag. Measured: `claude -- '-foo'` and
55
+ // `codex -- '-foo'` both take the text as the prompt (claude 2.1.268, codex
56
+ // 0.154.0, clap's standard terminator).
57
+ return `${CLEAR_CLAUDE_ENV}; cd ${shellQuote(folder)} && ${shellQuote(agentPath)} -- ${shellQuote(text)}`;
58
+ }
59
+ /**
60
+ * Bracketed paste, as the terminal would wrap a real paste.
61
+ *
62
+ * A running agent submits on Enter, so typing a multi-line prompt into it
63
+ * sends the first line and leaves the rest as a second message. Wrapped in
64
+ * `ESC[200~ … ESC[201~` the same bytes arrive as ONE paste, which the input
65
+ * box inserts whole; the return `do script` appends afterwards then submits it.
66
+ * Navarch's `WorkerManager.sendLine` does the same thing for the same reason.
67
+ */
68
+ export const BRACKETED_PASTE = { open: "[200~", close: "[201~" };
69
+ /** Newlines become spaces — the fallback when bracketed paste cannot be trusted. */
70
+ export function foldLines(text) {
71
+ return text.replace(/\r?\n/g, " ").replace(/ {2,}/g, " ").trim();
72
+ }
73
+ /**
74
+ * What to type into a running session.
75
+ *
76
+ * Single-line text is typed as-is. Multi-line text is wrapped in bracketed
77
+ * paste, or folded to one line when the caller has reason not to trust the
78
+ * paste. Which one the Terminal route uses is a measurement recorded on
79
+ * `commands/delegate.ts`.
80
+ *
81
+ * **C0 control bytes are stripped first**, keeping only tab and newline. This
82
+ * is what wraps the text in bracketed paste, so a raw `ESC[201~` (or any stray
83
+ * escape) in the prompt could otherwise close the paste early and have the rest
84
+ * reach the TUI as keystrokes. `\n` is kept — it is what decides bracketed vs.
85
+ * single-line — and `\t` is a legitimate character in a prompt.
86
+ */
87
+ export function asTypedInput(text, multiline = "bracketed") {
88
+ const stripped = text.replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, "");
89
+ const trimmed = stripped.replace(/\r\n/g, "\n").replace(/\n+$/, "");
90
+ if (!trimmed.includes("\n"))
91
+ return trimmed;
92
+ return multiline === "fold"
93
+ ? foldLines(trimmed)
94
+ : `${BRACKETED_PASTE.open}${trimmed}${BRACKETED_PASTE.close}`;
95
+ }
96
+ /**
97
+ * AppleScript sources. **No user text is ever interpolated into these** — a
98
+ * test asserts it — everything variable arrives through `argv`.
99
+ */
100
+ /**
101
+ * Type into the tab whose tty matches, bring it forward, and say what happened.
102
+ *
103
+ * `do script … in t` writes to that tab's tty as if typed and follows it with a
104
+ * return. Answers `sent`, or `no-tab` when no window has a tab on that tty —
105
+ * the session exists, but not in a Terminal.app this instance can see.
106
+ */
107
+ export const SEND_TO_TAB_SCRIPT = `on run argv
108
+ set targetTty to item 1 of argv
109
+ set theText to item 2 of argv
110
+ tell application "Terminal"
111
+ repeat with w in windows
112
+ repeat with t in tabs of w
113
+ if tty of t is targetTty then
114
+ do script theText in t
115
+ set selected tab of w to t
116
+ set index of w to 1
117
+ activate
118
+ return "sent"
119
+ end if
120
+ end repeat
121
+ end repeat
122
+ end tell
123
+ return "no-tab"
124
+ end run`;
125
+ /** A new window running one shell command, brought to the front. */
126
+ export const LAUNCH_SCRIPT = `on run argv
127
+ set theCommand to item 1 of argv
128
+ tell application "Terminal"
129
+ do script theCommand
130
+ activate
131
+ end tell
132
+ return "launched"
133
+ end run`;
134
+ //# sourceMappingURL=quote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quote.js","sourceRoot":"","sources":["../../src/delegate/quote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,2FAA2F;AAC3F,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,gBAAgB,GACpB,4EAA4E;MAC1E,6FAA6F,CAAC;AAElG;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,SAAiB,EAAE,IAAY;IAC3E,2EAA2E;IAC3E,8EAA8E;IAC9E,4EAA4E;IAC5E,wCAAwC;IACxC,OAAO,GAAG,gBAAgB,QAAQ,UAAU,CAAC,MAAM,CAAC,OAAO,UAAU,CAAC,SAAS,CAAC,OAAO,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;AAC5G,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAW,CAAC;AAE5E,oFAAoF;AACpF,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,YAAkC,WAAW;IACtF,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,qCAAqC,EAAE,EAAE,CAAC,CAAC;IACzE,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACpE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC;IAC5C,OAAO,SAAS,KAAK,MAAM;QACzB,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC;QACpB,CAAC,CAAC,GAAG,eAAe,CAAC,IAAI,GAAG,OAAO,GAAG,eAAe,CAAC,KAAK,EAAE,CAAC;AAClE,CAAC;AAED;;;GAGG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;QAiB1B,CAAC;AAET,oEAAoE;AACpE,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;QAOrB,CAAC"}
@@ -0,0 +1,121 @@
1
+ import { type AgentId, type HostId } from "./hosts.js";
2
+ /**
3
+ * Which agent sessions are running, where, and inside which host.
4
+ *
5
+ * ## Two agents, two routes — measured 2026-09-10
6
+ *
7
+ * | agent | route | what it gives |
8
+ * |---|---|---|
9
+ * | Claude Code 2.1.268 | `claude agents --json` | every session, interactive ones included: `pid`, `cwd`, `sessionId`, `name`, `status` (`idle`/`busy`). No tty needed. |
10
+ * | Codex CLI 0.154.0 | `ps -axo pid,ppid,tty,comm` for `codex`, then `lsof -a -p PID -d cwd -Fn` | pid and cwd. `codex queue --thread` exists but needs the app-server daemon, which was not running, so TUI sessions are not reachable through it. |
11
+ *
12
+ * ## The host is the process tree
13
+ *
14
+ * Neither route says which application a session lives in, and that is the
15
+ * question `pharos delegate --host` asks. Walking `ppid` up does: this very
16
+ * session's chain was `zsh ← claude ← zsh ← argusd` (Navarch), a Terminal tab
17
+ * ends at `Terminal.app/Contents/MacOS/Terminal`, and a VS Code integrated
18
+ * terminal hangs off a `Code … Helper` process. One `ps` of the whole table,
19
+ * walked in memory.
20
+ *
21
+ * ## `TIOCSTI` is not an option
22
+ *
23
+ * Typing into another process's tty directly is refused by the kernel from a
24
+ * non-controlling process — `EACCES`, confirmed with a private pty. So the
25
+ * pid and tty found here are handed to the HOST (Terminal's `do script … in
26
+ * tab`, the VS Code bridge), which owns the terminal and may write to it.
27
+ */
28
+ export interface ProcessRow {
29
+ pid: number;
30
+ ppid: number;
31
+ /** `/dev/ttys003`, or null for `??`. */
32
+ tty: string | null;
33
+ /** The command as `ps` prints it — often a full path, sometimes just `claude`. */
34
+ comm: string;
35
+ }
36
+ /**
37
+ * `ps -axo pid,ppid,tty,comm`, parsed.
38
+ *
39
+ * The command column is taken as everything after the third column because
40
+ * it contains spaces — `/Applications/ChatGPT.app/…/Codex (Service)` is one
41
+ * command. The header line does not match the numeric shape and is dropped.
42
+ */
43
+ export declare function parsePs(text: string): ProcessRow[];
44
+ export interface ClaudeAgent {
45
+ pid: number;
46
+ cwd: string;
47
+ kind?: string;
48
+ sessionId?: string;
49
+ name?: string;
50
+ status?: string;
51
+ startedAt?: number;
52
+ }
53
+ /**
54
+ * `claude agents --json`, parsed tolerantly: an array of objects with a numeric
55
+ * `pid` and a string `cwd` is all that is required, so a field Claude Code
56
+ * adds or drops in a later release changes nothing here.
57
+ */
58
+ export declare function parseClaudeAgents(json: string): ClaudeAgent[];
59
+ /** `lsof -a -p PID -d cwd -Fn` prints `p<pid>`, `fcwd`, `n<path>`; the path is what we want. */
60
+ export declare function parseLsofCwd(text: string): string | null;
61
+ /** The ancestors of a pid, nearest first, stopping at launchd or a cycle. */
62
+ export declare function ancestry(pid: number, rows: readonly ProcessRow[]): ProcessRow[];
63
+ /** The first ancestor that is a known host, or null. */
64
+ export declare function hostOfPid(pid: number, rows: readonly ProcessRow[]): HostId | null;
65
+ export interface Session {
66
+ agent: AgentId;
67
+ pid: number;
68
+ cwd: string;
69
+ name: string | null;
70
+ status: "idle" | "busy" | null;
71
+ tty: string | null;
72
+ host: HostId | null;
73
+ }
74
+ /** The probes, injectable so the selection logic is testable against fixtures. */
75
+ export interface SessionProbes {
76
+ ps(): Promise<string>;
77
+ claudeAgents(): Promise<string>;
78
+ lsofCwd(pid: number): Promise<string>;
79
+ }
80
+ /**
81
+ * What detection found, and whether it could be trusted to be complete.
82
+ *
83
+ * **`incomplete` is the difference between "no session" and "could not tell".**
84
+ * An older `claude` without an `agents` subcommand, a hang, or a failed `lsof`
85
+ * all yield an empty list — and reported as "nothing running" that reads as a
86
+ * fact when it is a failure. So the caller is told, and says so in the detail
87
+ * before it launches a second session beside one it simply could not see.
88
+ */
89
+ export interface Detection {
90
+ sessions: Session[];
91
+ incomplete: boolean;
92
+ /** One clause naming what failed, for the detail. Null when nothing did. */
93
+ detail: string | null;
94
+ }
95
+ export declare function systemProbes(env: NodeJS.ProcessEnv, claudePath: string | null): SessionProbes;
96
+ /**
97
+ * Every session of `agent` whose working directory is `folder`, with its host.
98
+ *
99
+ * Working directories are compared after `realpath` on both sides, so a
100
+ * symlinked checkout and its target read as one folder. A session whose cwd
101
+ * cannot be resolved is compared as printed.
102
+ */
103
+ export declare function findSessions(agent: AgentId, folder: string, probes: SessionProbes): Promise<Detection>;
104
+ /**
105
+ * Every session in the requested host that could be delivered to, best first.
106
+ *
107
+ * Idle beats busy: a prompt typed into a busy session queues behind the turn
108
+ * in progress, which works but is a worse experience than a session that is
109
+ * waiting for exactly this. Among equals the lowest pid — the longest-lived,
110
+ * which is usually the one the person thinks of as "the" session.
111
+ *
112
+ * **This is the ONE ranking, and `--list` and a plain delegate both read it.**
113
+ * `recommended` on a listed row is not a second implementation of the choice:
114
+ * it is `[0]` of this array, which is exactly what {@link chooseSession}
115
+ * returns. Two rankings would be two answers, and the app would mark a row the
116
+ * verb does not actually use.
117
+ */
118
+ export declare function rankSessions(sessions: readonly Session[], host: HostId): Session[];
119
+ /** Which session to send to, among those in the requested host: the best-ranked one. */
120
+ export declare function chooseSession(sessions: readonly Session[], host: HostId): Session | null;
121
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/delegate/sessions.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,YAAY,CAAC;AAItE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAclD;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAwB7D;AAED,gGAAgG;AAChG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKxD;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,EAAE,CAa/E;AAED,wDAAwD;AACxD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,GAAG,IAAI,CAMjF;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC/B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC5B,EAAE,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACtB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAChC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,aAAa,CAkB7F;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,SAAS,CAAC,CA6DpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAclF;AAED,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAExF"}