@floh-solutions/pharos-cli 0.29.0 → 0.30.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 +33 -0
- package/dist/capabilities.d.ts +7 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +25 -0
- package/dist/capabilities.js.map +1 -1
- package/dist/cli.d.ts +18 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +178 -86
- package/dist/cli.js.map +1 -1
- package/dist/commands/delegate.d.ts +143 -0
- package/dist/commands/delegate.d.ts.map +1 -0
- package/dist/commands/delegate.js +364 -0
- package/dist/commands/delegate.js.map +1 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +179 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +7 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/delegate/hosts.d.ts +83 -0
- package/dist/delegate/hosts.d.ts.map +1 -0
- package/dist/delegate/hosts.js +154 -0
- package/dist/delegate/hosts.js.map +1 -0
- package/dist/delegate/quote.d.ts +78 -0
- package/dist/delegate/quote.d.ts.map +1 -0
- package/dist/delegate/quote.js +134 -0
- package/dist/delegate/quote.js.map +1 -0
- package/dist/delegate/sessions.d.ts +113 -0
- package/dist/delegate/sessions.d.ts.map +1 -0
- package/dist/delegate/sessions.js +220 -0
- package/dist/delegate/sessions.js.map +1 -0
- package/dist/delegate/vsix.d.ts +122 -0
- package/dist/delegate/vsix.d.ts.map +1 -0
- package/dist/delegate/vsix.js +329 -0
- package/dist/delegate/vsix.js.map +1 -0
- package/package.json +3 -2
- package/skill/SKILL.md +41 -0
- package/vscode/README.md +30 -0
- package/vscode/pharos-bridge.json +10 -0
- package/vscode/pharos-bridge.vsix +0 -0
- package/dist/commands/pr.d.ts +0 -56
- package/dist/commands/pr.d.ts.map +0 -1
- package/dist/commands/pr.js +0 -202
- package/dist/commands/pr.js.map +0 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four places a coding agent can be running, and how to find each.
|
|
3
|
+
*
|
|
4
|
+
* ## Looked up by BUNDLE ID, not by name
|
|
5
|
+
*
|
|
6
|
+
* `osascript -e 'id of app "Visual Studio Code"'` is the obvious probe and it
|
|
7
|
+
* is wrong twice over: it launches nothing but it *is* an Apple Event, so it
|
|
8
|
+
* can trip an Automation prompt, and its failure text is localised — on the
|
|
9
|
+
* reference machine (Dutch locale) a missing app answered `kan niet worden
|
|
10
|
+
* opgevraagd (-1728)`. Spotlight answers the same question as data:
|
|
11
|
+
* `mdfind "kMDItemCFBundleIdentifier == '…'"` prints the bundle's path or
|
|
12
|
+
* nothing, in 70 ms, in any language. When Spotlight is off for a volume the
|
|
13
|
+
* well-known paths are tried, and the check says which route answered so a
|
|
14
|
+
* wrong answer is one line to diagnose.
|
|
15
|
+
*
|
|
16
|
+
* ## System binaries by ABSOLUTE PATH
|
|
17
|
+
*
|
|
18
|
+
* `mdfind`, `plutil`, `osascript`, `open`, `ps` and `lsof` are spawned from
|
|
19
|
+
* `/usr/bin` and `/bin` directly, never by name. Detection must not depend on
|
|
20
|
+
* PATH: a GUI-launched app inherits roughly `/usr/bin:/bin`, and the only
|
|
21
|
+
* things that legitimately depend on the person's PATH are the agent CLIs
|
|
22
|
+
* themselves.
|
|
23
|
+
*/
|
|
24
|
+
export type HostId = "terminal" | "vscode" | "vscode-insiders" | "navarch";
|
|
25
|
+
export type AgentId = "claude" | "codex";
|
|
26
|
+
export type Mode = "terminal" | "extension";
|
|
27
|
+
export declare const HOST_IDS: readonly HostId[];
|
|
28
|
+
export declare const AGENT_IDS: readonly AgentId[];
|
|
29
|
+
export declare const MODES: readonly Mode[];
|
|
30
|
+
export interface HostSpec {
|
|
31
|
+
id: HostId;
|
|
32
|
+
name: string;
|
|
33
|
+
bundleId: string;
|
|
34
|
+
/** Where the bundle lives when Spotlight cannot say. `~` means the home directory. */
|
|
35
|
+
candidates: readonly string[];
|
|
36
|
+
/** Homebrew cask, for the doctor's install line. */
|
|
37
|
+
cask?: string;
|
|
38
|
+
/** The VS Code flavours only. */
|
|
39
|
+
vscode?: {
|
|
40
|
+
/** Relative to the home directory. */
|
|
41
|
+
extensionsDir: string;
|
|
42
|
+
scheme: "vscode" | "vscode-insiders";
|
|
43
|
+
/** Substrings of a process command that mark it as this flavour's. */
|
|
44
|
+
helper: readonly string[];
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export declare const HOSTS: Record<HostId, HostSpec>;
|
|
48
|
+
export declare function isHostId(value: string): value is HostId;
|
|
49
|
+
export declare function isAgentId(value: string): value is AgentId;
|
|
50
|
+
export declare function isMode(value: string): value is Mode;
|
|
51
|
+
export interface InstalledApp {
|
|
52
|
+
path: string;
|
|
53
|
+
version: string | null;
|
|
54
|
+
/** Which lookup answered — the fact that makes a wrong answer diagnosable. */
|
|
55
|
+
via: "spotlight" | "path";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Is the app installed, where, and which version. Never throws — absence is an
|
|
59
|
+
* answer. Always `null` off macOS: every host here is a macOS application.
|
|
60
|
+
*/
|
|
61
|
+
export declare function findApp(host: HostId, env?: NodeJS.ProcessEnv): Promise<InstalledApp | null>;
|
|
62
|
+
/** `CFBundleShortVersionString`, read the way scripts are meant to: `plutil -extract … raw`. */
|
|
63
|
+
export declare function bundleVersion(appPath: string): Promise<string | null>;
|
|
64
|
+
/**
|
|
65
|
+
* The CLI inside a VS Code bundle.
|
|
66
|
+
*
|
|
67
|
+
* `code` is often NOT on PATH — it was not on the reference machine — because
|
|
68
|
+
* putting it there is a menu action ("Install 'code' command in PATH") that
|
|
69
|
+
* most people never take. The bundle always carries it, at the same relative
|
|
70
|
+
* path in both flavours.
|
|
71
|
+
*/
|
|
72
|
+
export declare function bundleCli(appPath: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Which host a process belongs to, from its command as `ps` prints it.
|
|
75
|
+
*
|
|
76
|
+
* Measured chains on the reference machine: a Terminal tab is
|
|
77
|
+
* `login ← Terminal.app/Contents/MacOS/Terminal`; a Navarch worker is
|
|
78
|
+
* `zsh ← argusd`; a VS Code integrated terminal's shell hangs off a
|
|
79
|
+
* `Code - Insiders Helper (Plugin)` process inside the bundle. Insiders is
|
|
80
|
+
* tested before stable because its names contain the stable ones.
|
|
81
|
+
*/
|
|
82
|
+
export declare function hostOfCommand(comm: string): HostId | null;
|
|
83
|
+
//# sourceMappingURL=hosts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosts.d.ts","sourceRoot":"","sources":["../../src/delegate/hosts.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,iBAAiB,GAAG,SAAS,CAAC;AAC3E,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AACzC,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG,WAAW,CAAC;AAE5C,eAAO,MAAM,QAAQ,EAAE,SAAS,MAAM,EAAyD,CAAC;AAChG,eAAO,MAAM,SAAS,EAAE,SAAS,OAAO,EAAwB,CAAC;AACjE,eAAO,MAAM,KAAK,EAAE,SAAS,IAAI,EAA8B,CAAC;AAEhE,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,MAAM,CAAC,EAAE;QACP,sCAAsC;QACtC,aAAa,EAAE,MAAM,CAAC;QACtB,MAAM,EAAE,QAAQ,GAAG,iBAAiB,CAAC;QACrC,sEAAsE;QACtE,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;KAC3B,CAAC;CACH;AAED,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CA2C1C,CAAC;AAEF,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,MAAM,CAEvD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,OAAO,CAEzD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,IAAI,CAEnD;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,8EAA8E;IAC9E,GAAG,EAAE,WAAW,GAAG,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAe9G;AAyBD,gGAAgG;AAChG,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAY3E;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOzD"}
|
|
@@ -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,113 @@
|
|
|
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
|
+
* Which session to send to, among those in the requested host.
|
|
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
|
+
export declare function chooseSession(sessions: readonly Session[], host: HostId): Session | null;
|
|
113
|
+
//# 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;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAcxF"}
|