@parlor/agent-host 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"adapter-launch.d.ts","sourceRoot":"","sources":["../src/adapter-launch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAKpE,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAoBA;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACpC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAQjC"}
1
+ {"version":3,"file":"adapter-launch.d.ts","sourceRoot":"","sources":["../src/adapter-launch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAKjF,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY,GAAG;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAoBA;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACpC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAOjC"}
@@ -1,4 +1,4 @@
1
- import { ADAPTER_BINARY_ENV } from "./agents.js";
1
+ import { ADAPTER_BINARY_ENV, ADAPTER_ENV } from "./agents.js";
2
2
  import { adapterKey, embeddedAdapters } from "./embedded.js";
3
3
  import { selfInvocation } from "./self.js";
4
4
  import { resolveBinary } from "./shell-path.js";
@@ -24,12 +24,12 @@ export function commandOf(profile) {
24
24
  };
25
25
  }
26
26
  export async function launchEnvironment(profile, env) {
27
- const variable = profile.launch.kind === "adapter"
28
- ? ADAPTER_BINARY_ENV[profile.launch.package]
29
- : undefined;
27
+ const adapter = profile.launch.kind === "adapter" ? profile.launch.package : undefined;
28
+ const own = { ...env, ...(adapter ? ADAPTER_ENV[adapter] : undefined) };
29
+ const variable = adapter ? ADAPTER_BINARY_ENV[adapter] : undefined;
30
30
  if (variable === undefined || env[variable] !== undefined)
31
- return { ...env };
31
+ return own;
32
32
  const found = await resolveBinary(profile.binary, env.PATH ?? "");
33
- return found === undefined ? { ...env } : { ...env, [variable]: found };
33
+ return found === undefined ? own : { ...own, [variable]: found };
34
34
  }
35
35
  //# sourceMappingURL=adapter-launch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter-launch.js","sourceRoot":"","sources":["../src/adapter-launch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAqB,MAAM,aAAa,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,MAAM,UAAU,SAAS,CAAC,OAAqB;IAI7C,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACxE,CAAC;IACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAClE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,cAAc,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;QACnD,OAAO;YACL,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;SACrD,CAAC;IACJ,CAAC;IACD,OAAO;QACL,OAAO,EAAE,KAAK;QACd,IAAI,EAAE;YACJ,OAAO;YACP,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE;YACrD,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;SAC/B;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAqB,EACrB,GAAqC;IAErC,MAAM,QAAQ,GACZ,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS;QAC/B,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC;QAC5C,CAAC,CAAC,SAAS,CAAC;IAChB,IAAI,QAAQ,KAAK,SAAS,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,SAAS;QAAE,OAAO,EAAE,GAAG,GAAG,EAAE,CAAC;IAC7E,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAClE,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;AAC1E,CAAC","sourcesContent":["import { ADAPTER_BINARY_ENV, type AgentProfile } from \"./agents.js\";\nimport { adapterKey, embeddedAdapters } from \"./embedded.js\";\nimport { selfInvocation } from \"./self.js\";\nimport { resolveBinary } from \"./shell-path.js\";\n\nexport function commandOf(profile: AgentProfile): {\n readonly command: string;\n readonly args: readonly string[];\n} {\n if (profile.launch.kind === \"native\") {\n return { command: profile.launch.command, args: profile.launch.args };\n }\n const embedded = embeddedAdapters.get(adapterKey(profile.launch));\n if (embedded !== undefined) {\n const self = selfInvocation([\"adapter\", embedded]);\n return {\n command: self.command,\n args: [...self.args, ...(profile.launch.args ?? [])],\n };\n }\n return {\n command: \"npx\",\n args: [\n \"--yes\",\n `${profile.launch.package}@${profile.launch.version}`,\n ...(profile.launch.args ?? []),\n ],\n };\n}\n\nexport async function launchEnvironment(\n profile: AgentProfile,\n env: Readonly<Record<string, string>>,\n): Promise<Record<string, string>> {\n const variable =\n profile.launch.kind === \"adapter\"\n ? ADAPTER_BINARY_ENV[profile.launch.package]\n : undefined;\n if (variable === undefined || env[variable] !== undefined) return { ...env };\n const found = await resolveBinary(profile.binary, env.PATH ?? \"\");\n return found === undefined ? { ...env } : { ...env, [variable]: found };\n}\n"]}
1
+ {"version":3,"file":"adapter-launch.js","sourceRoot":"","sources":["../src/adapter-launch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAqB,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,MAAM,UAAU,SAAS,CAAC,OAAqB;IAI7C,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACxE,CAAC;IACD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAClE,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,cAAc,CAAC,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAC;QACnD,OAAO;YACL,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;SACrD,CAAC;IACJ,CAAC;IACD,OAAO;QACL,OAAO,EAAE,KAAK;QACd,IAAI,EAAE;YACJ,OAAO;YACP,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE;YACrD,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;SAC/B;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAqB,EACrB,GAAqC;IAErC,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IACvF,MAAM,GAAG,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC;IACxE,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACnE,IAAI,QAAQ,KAAK,SAAS,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,SAAS;QAAE,OAAO,GAAG,CAAC;IACtE,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAClE,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;AACnE,CAAC","sourcesContent":["import { ADAPTER_BINARY_ENV, ADAPTER_ENV, type AgentProfile } from \"./agents.js\";\nimport { adapterKey, embeddedAdapters } from \"./embedded.js\";\nimport { selfInvocation } from \"./self.js\";\nimport { resolveBinary } from \"./shell-path.js\";\n\nexport function commandOf(profile: AgentProfile): {\n readonly command: string;\n readonly args: readonly string[];\n} {\n if (profile.launch.kind === \"native\") {\n return { command: profile.launch.command, args: profile.launch.args };\n }\n const embedded = embeddedAdapters.get(adapterKey(profile.launch));\n if (embedded !== undefined) {\n const self = selfInvocation([\"adapter\", embedded]);\n return {\n command: self.command,\n args: [...self.args, ...(profile.launch.args ?? [])],\n };\n }\n return {\n command: \"npx\",\n args: [\n \"--yes\",\n `${profile.launch.package}@${profile.launch.version}`,\n ...(profile.launch.args ?? []),\n ],\n };\n}\n\nexport async function launchEnvironment(\n profile: AgentProfile,\n env: Readonly<Record<string, string>>,\n): Promise<Record<string, string>> {\n const adapter = profile.launch.kind === \"adapter\" ? profile.launch.package : undefined;\n const own = { ...env, ...(adapter ? ADAPTER_ENV[adapter] : undefined) };\n const variable = adapter ? ADAPTER_BINARY_ENV[adapter] : undefined;\n if (variable === undefined || env[variable] !== undefined) return own;\n const found = await resolveBinary(profile.binary, env.PATH ?? \"\");\n return found === undefined ? own : { ...own, [variable]: found };\n}\n"]}
@@ -0,0 +1,15 @@
1
+ /** What a skill says about the app it points the agent's MCP tools at. */
2
+ export interface SkillApp {
3
+ /** The MCP server name, which is also the skill's name and the word the user asks with. */
4
+ readonly name: string;
5
+ readonly appName: string;
6
+ readonly appOrigin: string;
7
+ /** Whose account the tools act as: an email, or a name. */
8
+ readonly account: string;
9
+ }
10
+ export declare function skillOf(app: SkillApp): string;
11
+ /** Writes or refreshes the app's skill for each agent here; a skill of the same name the user wrote stays. */
12
+ export declare function writeAgentSkills(agents: readonly string[], app: SkillApp, env: Record<string, string>): Promise<void>;
13
+ /** Removes the skills Parlor Host wrote for the app; one the user wrote under the name stays. */
14
+ export declare function removeAgentSkills(agents: readonly string[], name: string, env: Record<string, string>): Promise<void>;
15
+ //# sourceMappingURL=agent-skills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-skills.d.ts","sourceRoot":"","sources":["../src/agent-skills.ts"],"names":[],"mappings":"AAMA,0EAA0E;AAC1E,MAAM,WAAW,QAAQ;IACvB,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAiBD,wBAAgB,OAAO,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAiB7C;AA4BD,8GAA8G;AAC9G,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,GAAG,EAAE,QAAQ,EACb,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC1B,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,iGAAiG;AACjG,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC1B,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -0,0 +1,92 @@
1
+ import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { logEvent } from "./log.js";
5
+ /**
6
+ * Where each agent reads user-wide skills. Coding agents load MCP tools on demand and list
7
+ * skills up front, so a skill named after the app is what the agent sees when the user
8
+ * names it, instead of a browser skill or a guess.
9
+ */
10
+ const SKILL_ROOTS = {
11
+ claude: (env) => join(env.CLAUDE_CONFIG_DIR || join(homeOf(env), ".claude"), "skills"),
12
+ codex: (env) => join(env.CODEX_HOME || join(homeOf(env), ".codex"), "skills"),
13
+ };
14
+ /** Marks a skill directory as Parlor Host's, so a skill the user wrote is never replaced or removed. */
15
+ const OWNER_FILE = ".parlor-host";
16
+ const homeOf = (env) => env.HOME || homedir();
17
+ export function skillOf(app) {
18
+ const description = `Use whenever the user names ${app.appName} or "${app.name}", to read or change anything in ${app.appName} (${app.appOrigin}). Use the ${app.name} MCP tools for it, not a browser.`;
19
+ return `---
20
+ name: ${app.name}
21
+ description: ${JSON.stringify(description)}
22
+ ---
23
+
24
+ # ${app.appName}
25
+
26
+ The \`${app.name}\` MCP server acts on the live ${app.appName} app at ${app.appOrigin} as ${app.account}. Its tools read and change the same data the app shows, directly.
27
+
28
+ - For anything the user asks of ${app.appName}, use these tools instead of opening the app in a browser.
29
+ - The tools may load on demand: if none from \`${app.name}\` are listed, search your tools for "${app.name}" first.
30
+ - Reading is free. Before the first change in a conversation, the user is asked to allow changes.
31
+
32
+ Parlor Host wrote this skill when this computer was connected to ${app.appName}, and removes it when it is disconnected.
33
+ `;
34
+ }
35
+ async function ownedBy(dir) {
36
+ return readFile(join(dir, OWNER_FILE), "utf8").then((text) => text.trim(), () => undefined);
37
+ }
38
+ /**
39
+ * Whether the user keeps the agent's settings elsewhere and links them in, as dotfiles and
40
+ * home-manager do: a skill written there would land in their own repository or store.
41
+ */
42
+ async function linkedIn(root) {
43
+ for (const path of [root, dirname(root)]) {
44
+ const stat = await lstat(path).catch(() => undefined);
45
+ if (stat?.isSymbolicLink())
46
+ return true;
47
+ }
48
+ return false;
49
+ }
50
+ async function exists(path) {
51
+ return readFile(join(path, "SKILL.md")).then(() => true, () => false);
52
+ }
53
+ /** Writes or refreshes the app's skill for each agent here; a skill of the same name the user wrote stays. */
54
+ export async function writeAgentSkills(agents, app, env) {
55
+ for (const agent of agents) {
56
+ const root = SKILL_ROOTS[agent];
57
+ if (!root)
58
+ continue;
59
+ const dir = join(root(env), app.name);
60
+ try {
61
+ if (await linkedIn(root(env))) {
62
+ logEvent("agent_skill_kept", { agent, name: app.name, reason: "linked" });
63
+ continue;
64
+ }
65
+ if ((await exists(dir)) && (await ownedBy(dir)) === undefined) {
66
+ logEvent("agent_skill_kept", { agent, name: app.name });
67
+ continue;
68
+ }
69
+ await mkdir(dir, { recursive: true });
70
+ await writeFile(join(dir, "SKILL.md"), skillOf(app));
71
+ await writeFile(join(dir, OWNER_FILE), `${app.appOrigin}\n`);
72
+ }
73
+ catch (error) {
74
+ logEvent("agent_skill_failed", { agent, name: app.name, error: String(error) });
75
+ }
76
+ }
77
+ }
78
+ /** Removes the skills Parlor Host wrote for the app; one the user wrote under the name stays. */
79
+ export async function removeAgentSkills(agents, name, env) {
80
+ for (const agent of agents) {
81
+ const root = SKILL_ROOTS[agent];
82
+ if (!root)
83
+ continue;
84
+ const dir = join(root(env), name);
85
+ if ((await ownedBy(dir)) === undefined)
86
+ continue;
87
+ await rm(dir, { recursive: true, force: true }).catch((error) => {
88
+ logEvent("agent_skill_failed", { agent, name, error: String(error) });
89
+ });
90
+ }
91
+ }
92
+ //# sourceMappingURL=agent-skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-skills.js","sourceRoot":"","sources":["../src/agent-skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAYpC;;;;GAIG;AACH,MAAM,WAAW,GAAsE;IACrF,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,iBAAiB,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,SAAS,CAAC,EAAE,QAAQ,CAAC;IACtF,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC;CAC9E,CAAC;AAEF,wGAAwG;AACxG,MAAM,UAAU,GAAG,cAAc,CAAC;AAElC,MAAM,MAAM,GAAG,CAAC,GAA2B,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;AAEtE,MAAM,UAAU,OAAO,CAAC,GAAa;IACnC,MAAM,WAAW,GAAG,+BAA+B,GAAG,CAAC,OAAO,QAAQ,GAAG,CAAC,IAAI,oCAAoC,GAAG,CAAC,OAAO,KAAK,GAAG,CAAC,SAAS,cAAc,GAAG,CAAC,IAAI,mCAAmC,CAAC;IACzM,OAAO;QACD,GAAG,CAAC,IAAI;eACD,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC;;;IAGtC,GAAG,CAAC,OAAO;;QAEP,GAAG,CAAC,IAAI,kCAAkC,GAAG,CAAC,OAAO,WAAW,GAAG,CAAC,SAAS,OAAO,GAAG,CAAC,OAAO;;kCAErE,GAAG,CAAC,OAAO;iDACI,GAAG,CAAC,IAAI,yCAAyC,GAAG,CAAC,IAAI;;;mEAGvC,GAAG,CAAC,OAAO;CAC7E,CAAC;AACF,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,GAAW;IAChC,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,CACjD,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,EACrB,GAAG,EAAE,CAAC,SAAS,CAChB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,QAAQ,CAAC,IAAY;IAClC,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACtD,IAAI,IAAI,EAAE,cAAc,EAAE;YAAE,OAAO,IAAI,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,IAAY;IAChC,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAC1C,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACZ,CAAC;AACJ,CAAC;AAED,8GAA8G;AAC9G,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAyB,EACzB,GAAa,EACb,GAA2B;IAE3B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC;YACH,IAAI,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBAC9B,QAAQ,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAC1E,SAAS;YACX,CAAC;YACD,IAAI,CAAC,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;gBAC9D,QAAQ,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;gBACxD,SAAS;YACX,CAAC;YACD,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACtC,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YACrD,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,GAAG,GAAG,CAAC,SAAS,IAAI,CAAC,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,QAAQ,CAAC,oBAAoB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;AACH,CAAC;AAED,iGAAiG;AACjG,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAyB,EACzB,IAAY,EACZ,GAA2B;IAE3B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS;YAAE,SAAS;QACjD,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACvE,QAAQ,CAAC,oBAAoB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACxE,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC","sourcesContent":["import { lstat, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nimport { logEvent } from \"./log.js\";\n\n/** What a skill says about the app it points the agent's MCP tools at. */\nexport interface SkillApp {\n /** The MCP server name, which is also the skill's name and the word the user asks with. */\n readonly name: string;\n readonly appName: string;\n readonly appOrigin: string;\n /** Whose account the tools act as: an email, or a name. */\n readonly account: string;\n}\n\n/**\n * Where each agent reads user-wide skills. Coding agents load MCP tools on demand and list\n * skills up front, so a skill named after the app is what the agent sees when the user\n * names it, instead of a browser skill or a guess.\n */\nconst SKILL_ROOTS: Readonly<Record<string, (env: Record<string, string>) => string>> = {\n claude: (env) => join(env.CLAUDE_CONFIG_DIR || join(homeOf(env), \".claude\"), \"skills\"),\n codex: (env) => join(env.CODEX_HOME || join(homeOf(env), \".codex\"), \"skills\"),\n};\n\n/** Marks a skill directory as Parlor Host's, so a skill the user wrote is never replaced or removed. */\nconst OWNER_FILE = \".parlor-host\";\n\nconst homeOf = (env: Record<string, string>) => env.HOME || homedir();\n\nexport function skillOf(app: SkillApp): string {\n const description = `Use whenever the user names ${app.appName} or \"${app.name}\", to read or change anything in ${app.appName} (${app.appOrigin}). Use the ${app.name} MCP tools for it, not a browser.`;\n return `---\nname: ${app.name}\ndescription: ${JSON.stringify(description)}\n---\n\n# ${app.appName}\n\nThe \\`${app.name}\\` MCP server acts on the live ${app.appName} app at ${app.appOrigin} as ${app.account}. Its tools read and change the same data the app shows, directly.\n\n- For anything the user asks of ${app.appName}, use these tools instead of opening the app in a browser.\n- The tools may load on demand: if none from \\`${app.name}\\` are listed, search your tools for \"${app.name}\" first.\n- Reading is free. Before the first change in a conversation, the user is asked to allow changes.\n\nParlor Host wrote this skill when this computer was connected to ${app.appName}, and removes it when it is disconnected.\n`;\n}\n\nasync function ownedBy(dir: string): Promise<string | undefined> {\n return readFile(join(dir, OWNER_FILE), \"utf8\").then(\n (text) => text.trim(),\n () => undefined,\n );\n}\n\n/**\n * Whether the user keeps the agent's settings elsewhere and links them in, as dotfiles and\n * home-manager do: a skill written there would land in their own repository or store.\n */\nasync function linkedIn(root: string): Promise<boolean> {\n for (const path of [root, dirname(root)]) {\n const stat = await lstat(path).catch(() => undefined);\n if (stat?.isSymbolicLink()) return true;\n }\n return false;\n}\n\nasync function exists(path: string): Promise<boolean> {\n return readFile(join(path, \"SKILL.md\")).then(\n () => true,\n () => false,\n );\n}\n\n/** Writes or refreshes the app's skill for each agent here; a skill of the same name the user wrote stays. */\nexport async function writeAgentSkills(\n agents: readonly string[],\n app: SkillApp,\n env: Record<string, string>,\n): Promise<void> {\n for (const agent of agents) {\n const root = SKILL_ROOTS[agent];\n if (!root) continue;\n const dir = join(root(env), app.name);\n try {\n if (await linkedIn(root(env))) {\n logEvent(\"agent_skill_kept\", { agent, name: app.name, reason: \"linked\" });\n continue;\n }\n if ((await exists(dir)) && (await ownedBy(dir)) === undefined) {\n logEvent(\"agent_skill_kept\", { agent, name: app.name });\n continue;\n }\n await mkdir(dir, { recursive: true });\n await writeFile(join(dir, \"SKILL.md\"), skillOf(app));\n await writeFile(join(dir, OWNER_FILE), `${app.appOrigin}\\n`);\n } catch (error) {\n logEvent(\"agent_skill_failed\", { agent, name: app.name, error: String(error) });\n }\n }\n}\n\n/** Removes the skills Parlor Host wrote for the app; one the user wrote under the name stays. */\nexport async function removeAgentSkills(\n agents: readonly string[],\n name: string,\n env: Record<string, string>,\n): Promise<void> {\n for (const agent of agents) {\n const root = SKILL_ROOTS[agent];\n if (!root) continue;\n const dir = join(root(env), name);\n if ((await ownedBy(dir)) === undefined) continue;\n await rm(dir, { recursive: true, force: true }).catch((error: unknown) => {\n logEvent(\"agent_skill_failed\", { agent, name, error: String(error) });\n });\n }\n}\n"]}
package/dist/agents.d.ts CHANGED
@@ -6,6 +6,11 @@ export interface AgentProfile {
6
6
  readonly launch: AgentLaunch;
7
7
  /** The CLI behind it, for the installed-and-logged-in check. */
8
8
  readonly binary: string;
9
+ /**
10
+ * Where a desktop app ships the same CLI, for a computer with the app and no CLI on its
11
+ * PATH: the app reads the CLI's own settings, so the agent behaves the same either way.
12
+ */
13
+ readonly apps?: readonly string[] | undefined;
9
14
  }
10
15
  /**
11
16
  * `adapter`: an npm ACP adapter at an exact version, run from the copy this build embeds
@@ -24,9 +29,25 @@ export type AgentLaunch = {
24
29
  export declare const AGENT_PROFILES: readonly AgentProfile[];
25
30
  /** Where each adapter looks for the agent CLI it drives; its own search does not survive a single executable. */
26
31
  export declare const ADAPTER_BINARY_ENV: Readonly<Record<string, string>>;
32
+ /**
33
+ * What each adapter needs set to serve an app's turn. Codex's adapter skips a requested MCP
34
+ * server whose name the user's own config already uses, so a turn would reach the app through
35
+ * the registration for agents outside the sidebar instead, which asks before the first change.
36
+ */
37
+ export declare const ADAPTER_ENV: Readonly<Record<string, Readonly<Record<string, string>>>>;
27
38
  export declare const DETECT_TIMEOUT_MS = 3000;
28
39
  /** Built-ins plus the user's own `agents.json`, which only this computer's user edits. */
29
40
  export declare function loadProfiles(file: string): Promise<readonly AgentProfile[]>;
41
+ /** The agents installed here, each with its profile's `binary` set to the CLI that answered. */
42
+ export interface FoundAgents {
43
+ readonly agents: readonly AgentInfo[];
44
+ readonly profiles: readonly AgentProfile[];
45
+ }
46
+ /**
47
+ * Looks for each agent's CLI on the login-shell PATH, then inside the desktop apps that ship
48
+ * it. An agent found nowhere is left out; the profiles it returns run what was found.
49
+ */
50
+ export declare function findAgents(profiles: readonly AgentProfile[]): Promise<FoundAgents>;
30
51
  /** What `hostReport` sends: the installed agents; one that is not installed is left out. */
31
52
  export declare function detectAgents(profiles: readonly AgentProfile[]): Promise<AgentInfo[]>;
32
53
  //# sourceMappingURL=agents.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAI9C,mGAAmG;AACnG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GACnB;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC/C,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAAC;AAEN,eAAO,MAAM,cAAc,EAAE,SAAS,YAAY,EA2BjD,CAAC;AAEF,iHAAiH;AACjH,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAG/D,CAAC;AAEF,eAAO,MAAM,iBAAiB,OAAQ,CAAC;AA4CvC,0FAA0F;AAC1F,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,CAkBjF;AAkBD,4FAA4F;AAC5F,wBAAsB,YAAY,CAChC,QAAQ,EAAE,SAAS,YAAY,EAAE,GAChC,OAAO,CAAC,SAAS,EAAE,CAAC,CAwBtB"}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAI9C,mGAAmG;AACnG,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC/C;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GACnB;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC/C,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAAC;AAEN,eAAO,MAAM,cAAc,EAAE,SAAS,YAAY,EA4BjD,CAAC;AAEF,iHAAiH;AACjH,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAG/D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAElF,CAAC;AAEF,eAAO,MAAM,iBAAiB,OAAQ,CAAC;AA6CvC,0FAA0F;AAC1F,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,CAkBjF;AAkBD,gGAAgG;AAChG,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;CAC5C;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,SAAS,YAAY,EAAE,GAChC,OAAO,CAAC,WAAW,CAAC,CAwCtB;AAED,4FAA4F;AAC5F,wBAAsB,YAAY,CAChC,QAAQ,EAAE,SAAS,YAAY,EAAE,GAChC,OAAO,CAAC,SAAS,EAAE,CAAC,CAEtB"}
package/dist/agents.js CHANGED
@@ -21,6 +21,7 @@ export const AGENT_PROFILES = [
21
21
  version: "2.2.2",
22
22
  },
23
23
  binary: "codex",
24
+ apps: ["/Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex"],
24
25
  },
25
26
  {
26
27
  id: "gemini",
@@ -34,6 +35,14 @@ export const ADAPTER_BINARY_ENV = {
34
35
  "@agentclientprotocol/claude-agent-acp": "CLAUDE_CODE_EXECUTABLE",
35
36
  "@agentclientprotocol/codex-acp": "CODEX_PATH",
36
37
  };
38
+ /**
39
+ * What each adapter needs set to serve an app's turn. Codex's adapter skips a requested MCP
40
+ * server whose name the user's own config already uses, so a turn would reach the app through
41
+ * the registration for agents outside the sidebar instead, which asks before the first change.
42
+ */
43
+ export const ADAPTER_ENV = {
44
+ "@agentclientprotocol/codex-acp": { DISABLE_MCP_CONFIG_FILTERING: "true" },
45
+ };
37
46
  export const DETECT_TIMEOUT_MS = 3_000;
38
47
  const isFields = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
39
48
  const isStrings = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
@@ -62,12 +71,14 @@ function profileOf(value) {
62
71
  if (!isFields(value))
63
72
  return undefined;
64
73
  const launch = launchOf(value.launch);
65
- const { id, name, binary } = value;
74
+ const { id, name, binary, apps } = value;
66
75
  if (!launch || typeof id !== "string" || typeof name !== "string")
67
76
  return undefined;
68
77
  if (typeof binary !== "string" || id === "")
69
78
  return undefined;
70
- return { id, name, launch, binary };
79
+ if (apps !== undefined && !isStrings(apps))
80
+ return undefined;
81
+ return { id, name, launch, binary, ...(apps ? { apps } : {}) };
71
82
  }
72
83
  /** Built-ins plus the user's own `agents.json`, which only this computer's user edits. */
73
84
  export async function loadProfiles(file) {
@@ -104,29 +115,50 @@ function versionOf(binary, path) {
104
115
  });
105
116
  });
106
117
  }
107
- /** What `hostReport` sends: the installed agents; one that is not installed is left out. */
108
- export async function detectAgents(profiles) {
118
+ /**
119
+ * Looks for each agent's CLI on the login-shell PATH, then inside the desktop apps that ship
120
+ * it. An agent found nowhere is left out; the profiles it returns run what was found.
121
+ */
122
+ export async function findAgents(profiles) {
109
123
  const path = await agentPath();
110
124
  const found = await Promise.all(profiles.map(async (profile) => {
111
- const output = await versionOf(profile.binary, path);
125
+ let binary = profile.binary;
126
+ let output = await versionOf(binary, path);
127
+ for (const app of profile.apps ?? []) {
128
+ if (output !== "missing")
129
+ break;
130
+ binary = app;
131
+ output = await versionOf(app, path);
132
+ }
112
133
  if (output === "missing")
113
134
  return undefined;
135
+ const resolved = binary === profile.binary ? profile : { ...profile, binary };
114
136
  if (output === "broken") {
115
- return {
137
+ const info = {
116
138
  id: profile.id,
117
139
  name: profile.name,
118
140
  ready: false,
119
- problem: `\`${profile.binary} --version\` failed: check that ${profile.name} runs in a terminal.`,
141
+ problem: `\`${binary} --version\` failed: check that ${profile.name} runs in a terminal.`,
120
142
  };
143
+ return { info, profile: resolved };
121
144
  }
122
145
  const version = /\d+\.\d+\.\d+[\w.-]*/.exec(output)?.[0];
123
- return {
146
+ const info = {
124
147
  id: profile.id,
125
148
  name: profile.name,
126
149
  ready: true,
127
150
  ...(version ? { version } : {}),
128
151
  };
152
+ return { info, profile: resolved };
129
153
  }));
130
- return found.filter((agent) => agent !== undefined);
154
+ const present = found.filter((entry) => entry !== undefined);
155
+ return {
156
+ agents: present.map((entry) => entry.info),
157
+ profiles: profiles.map((profile) => present.find((entry) => entry.profile.id === profile.id)?.profile ?? profile),
158
+ };
159
+ }
160
+ /** What `hostReport` sends: the installed agents; one that is not installed is left out. */
161
+ export async function detectAgents(profiles) {
162
+ return [...(await findAgents(profiles)).agents];
131
163
  }
132
164
  //# sourceMappingURL=agents.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agents.js","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\n\nimport type { AgentInfo } from \"@parlor/chat\";\n\nimport { agentPath } from \"./shell-path.js\";\n\n/** The app picks an agent by `id` only, never by command, so no remote party chooses what runs. */\nexport interface AgentProfile {\n readonly id: string;\n readonly name: string;\n readonly launch: AgentLaunch;\n /** The CLI behind it, for the installed-and-logged-in check. */\n readonly binary: string;\n}\n\n/**\n * `adapter`: an npm ACP adapter at an exact version, run from the copy this build embeds\n * and else through `npx`. `native`: an agent CLI that speaks ACP itself, found on the login-shell PATH.\n */\nexport type AgentLaunch =\n | {\n readonly kind: \"adapter\";\n readonly package: string;\n readonly version: string;\n readonly args?: readonly string[] | undefined;\n }\n | {\n readonly kind: \"native\";\n readonly command: string;\n readonly args: readonly string[];\n };\n\nexport const AGENT_PROFILES: readonly AgentProfile[] = [\n {\n id: \"claude\",\n name: \"Claude Code\",\n launch: {\n kind: \"adapter\",\n package: \"@agentclientprotocol/claude-agent-acp\",\n version: \"0.93.0\",\n },\n binary: \"claude\",\n },\n {\n id: \"codex\",\n name: \"Codex\",\n launch: {\n kind: \"adapter\",\n package: \"@agentclientprotocol/codex-acp\",\n version: \"2.2.2\",\n },\n binary: \"codex\",\n },\n {\n id: \"gemini\",\n name: \"Gemini CLI\",\n launch: { kind: \"native\", command: \"gemini\", args: [\"--acp\"] },\n binary: \"gemini\",\n },\n];\n\n/** Where each adapter looks for the agent CLI it drives; its own search does not survive a single executable. */\nexport const ADAPTER_BINARY_ENV: Readonly<Record<string, string>> = {\n \"@agentclientprotocol/claude-agent-acp\": \"CLAUDE_CODE_EXECUTABLE\",\n \"@agentclientprotocol/codex-acp\": \"CODEX_PATH\",\n};\n\nexport const DETECT_TIMEOUT_MS = 3_000;\n\ntype Fields = Readonly<Record<string, unknown>>;\n\nconst isFields = (value: unknown): value is Fields =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isStrings = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every((entry) => typeof entry === \"string\");\n\nfunction launchOf(value: unknown): AgentLaunch | undefined {\n if (!isFields(value)) return undefined;\n if (\n value.kind === \"native\" &&\n typeof value.command === \"string\" &&\n isStrings(value.args)\n ) {\n return { kind: \"native\", command: value.command, args: value.args };\n }\n if (\n value.kind === \"adapter\" &&\n typeof value.package === \"string\" &&\n typeof value.version === \"string\" &&\n (value.args === undefined || isStrings(value.args))\n ) {\n return {\n kind: \"adapter\",\n package: value.package,\n version: value.version,\n args: value.args,\n };\n }\n return undefined;\n}\n\nfunction profileOf(value: unknown): AgentProfile | undefined {\n if (!isFields(value)) return undefined;\n const launch = launchOf(value.launch);\n const { id, name, binary } = value;\n if (!launch || typeof id !== \"string\" || typeof name !== \"string\") return undefined;\n if (typeof binary !== \"string\" || id === \"\") return undefined;\n return { id, name, launch, binary };\n}\n\n/** Built-ins plus the user's own `agents.json`, which only this computer's user edits. */\nexport async function loadProfiles(file: string): Promise<readonly AgentProfile[]> {\n let text: string;\n try {\n text = await readFile(file, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return AGENT_PROFILES;\n throw error;\n }\n const listed: unknown = JSON.parse(text);\n if (!Array.isArray(listed))\n throw new Error(`${file} must hold a JSON array of agent profiles.`);\n const own = listed.map((entry, at) => {\n const profile = profileOf(entry);\n if (!profile) throw new Error(`${file}: entry ${at + 1} is not an agent profile.`);\n return profile;\n });\n const ids = new Set(own.map((profile) => profile.id));\n return [...AGENT_PROFILES.filter((profile) => !ids.has(profile.id)), ...own];\n}\n\nfunction versionOf(binary: string, path: string): Promise<string | \"missing\" | \"broken\"> {\n return new Promise((resolve) => {\n execFile(\n binary,\n [\"--version\"],\n { timeout: DETECT_TIMEOUT_MS, env: { ...process.env, PATH: path } },\n (error, stdout) => {\n if ((error as NodeJS.ErrnoException | null)?.code === \"ENOENT\")\n resolve(\"missing\");\n else if (error) resolve(\"broken\");\n else resolve(stdout.trim());\n },\n );\n });\n}\n\n/** What `hostReport` sends: the installed agents; one that is not installed is left out. */\nexport async function detectAgents(\n profiles: readonly AgentProfile[],\n): Promise<AgentInfo[]> {\n const path = await agentPath();\n const found = await Promise.all(\n profiles.map(async (profile): Promise<AgentInfo | undefined> => {\n const output = await versionOf(profile.binary, path);\n if (output === \"missing\") return undefined;\n if (output === \"broken\") {\n return {\n id: profile.id,\n name: profile.name,\n ready: false,\n problem: `\\`${profile.binary} --version\\` failed: check that ${profile.name} runs in a terminal.`,\n };\n }\n const version = /\\d+\\.\\d+\\.\\d+[\\w.-]*/.exec(output)?.[0];\n return {\n id: profile.id,\n name: profile.name,\n ready: true,\n ...(version ? { version } : {}),\n };\n }),\n );\n return found.filter((agent): agent is AgentInfo => agent !== undefined);\n}\n"]}
1
+ {"version":3,"file":"agents.js","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\n\nimport type { AgentInfo } from \"@parlor/chat\";\n\nimport { agentPath } from \"./shell-path.js\";\n\n/** The app picks an agent by `id` only, never by command, so no remote party chooses what runs. */\nexport interface AgentProfile {\n readonly id: string;\n readonly name: string;\n readonly launch: AgentLaunch;\n /** The CLI behind it, for the installed-and-logged-in check. */\n readonly binary: string;\n /**\n * Where a desktop app ships the same CLI, for a computer with the app and no CLI on its\n * PATH: the app reads the CLI's own settings, so the agent behaves the same either way.\n */\n readonly apps?: readonly string[] | undefined;\n}\n\n/**\n * `adapter`: an npm ACP adapter at an exact version, run from the copy this build embeds\n * and else through `npx`. `native`: an agent CLI that speaks ACP itself, found on the login-shell PATH.\n */\nexport type AgentLaunch =\n | {\n readonly kind: \"adapter\";\n readonly package: string;\n readonly version: string;\n readonly args?: readonly string[] | undefined;\n }\n | {\n readonly kind: \"native\";\n readonly command: string;\n readonly args: readonly string[];\n };\n\nexport const AGENT_PROFILES: readonly AgentProfile[] = [\n {\n id: \"claude\",\n name: \"Claude Code\",\n launch: {\n kind: \"adapter\",\n package: \"@agentclientprotocol/claude-agent-acp\",\n version: \"0.93.0\",\n },\n binary: \"claude\",\n },\n {\n id: \"codex\",\n name: \"Codex\",\n launch: {\n kind: \"adapter\",\n package: \"@agentclientprotocol/codex-acp\",\n version: \"2.2.2\",\n },\n binary: \"codex\",\n apps: [\"/Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex\"],\n },\n {\n id: \"gemini\",\n name: \"Gemini CLI\",\n launch: { kind: \"native\", command: \"gemini\", args: [\"--acp\"] },\n binary: \"gemini\",\n },\n];\n\n/** Where each adapter looks for the agent CLI it drives; its own search does not survive a single executable. */\nexport const ADAPTER_BINARY_ENV: Readonly<Record<string, string>> = {\n \"@agentclientprotocol/claude-agent-acp\": \"CLAUDE_CODE_EXECUTABLE\",\n \"@agentclientprotocol/codex-acp\": \"CODEX_PATH\",\n};\n\n/**\n * What each adapter needs set to serve an app's turn. Codex's adapter skips a requested MCP\n * server whose name the user's own config already uses, so a turn would reach the app through\n * the registration for agents outside the sidebar instead, which asks before the first change.\n */\nexport const ADAPTER_ENV: Readonly<Record<string, Readonly<Record<string, string>>>> = {\n \"@agentclientprotocol/codex-acp\": { DISABLE_MCP_CONFIG_FILTERING: \"true\" },\n};\n\nexport const DETECT_TIMEOUT_MS = 3_000;\n\ntype Fields = Readonly<Record<string, unknown>>;\n\nconst isFields = (value: unknown): value is Fields =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst isStrings = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every((entry) => typeof entry === \"string\");\n\nfunction launchOf(value: unknown): AgentLaunch | undefined {\n if (!isFields(value)) return undefined;\n if (\n value.kind === \"native\" &&\n typeof value.command === \"string\" &&\n isStrings(value.args)\n ) {\n return { kind: \"native\", command: value.command, args: value.args };\n }\n if (\n value.kind === \"adapter\" &&\n typeof value.package === \"string\" &&\n typeof value.version === \"string\" &&\n (value.args === undefined || isStrings(value.args))\n ) {\n return {\n kind: \"adapter\",\n package: value.package,\n version: value.version,\n args: value.args,\n };\n }\n return undefined;\n}\n\nfunction profileOf(value: unknown): AgentProfile | undefined {\n if (!isFields(value)) return undefined;\n const launch = launchOf(value.launch);\n const { id, name, binary, apps } = value;\n if (!launch || typeof id !== \"string\" || typeof name !== \"string\") return undefined;\n if (typeof binary !== \"string\" || id === \"\") return undefined;\n if (apps !== undefined && !isStrings(apps)) return undefined;\n return { id, name, launch, binary, ...(apps ? { apps } : {}) };\n}\n\n/** Built-ins plus the user's own `agents.json`, which only this computer's user edits. */\nexport async function loadProfiles(file: string): Promise<readonly AgentProfile[]> {\n let text: string;\n try {\n text = await readFile(file, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return AGENT_PROFILES;\n throw error;\n }\n const listed: unknown = JSON.parse(text);\n if (!Array.isArray(listed))\n throw new Error(`${file} must hold a JSON array of agent profiles.`);\n const own = listed.map((entry, at) => {\n const profile = profileOf(entry);\n if (!profile) throw new Error(`${file}: entry ${at + 1} is not an agent profile.`);\n return profile;\n });\n const ids = new Set(own.map((profile) => profile.id));\n return [...AGENT_PROFILES.filter((profile) => !ids.has(profile.id)), ...own];\n}\n\nfunction versionOf(binary: string, path: string): Promise<string | \"missing\" | \"broken\"> {\n return new Promise((resolve) => {\n execFile(\n binary,\n [\"--version\"],\n { timeout: DETECT_TIMEOUT_MS, env: { ...process.env, PATH: path } },\n (error, stdout) => {\n if ((error as NodeJS.ErrnoException | null)?.code === \"ENOENT\")\n resolve(\"missing\");\n else if (error) resolve(\"broken\");\n else resolve(stdout.trim());\n },\n );\n });\n}\n\n/** The agents installed here, each with its profile's `binary` set to the CLI that answered. */\nexport interface FoundAgents {\n readonly agents: readonly AgentInfo[];\n readonly profiles: readonly AgentProfile[];\n}\n\n/**\n * Looks for each agent's CLI on the login-shell PATH, then inside the desktop apps that ship\n * it. An agent found nowhere is left out; the profiles it returns run what was found.\n */\nexport async function findAgents(\n profiles: readonly AgentProfile[],\n): Promise<FoundAgents> {\n const path = await agentPath();\n const found = await Promise.all(\n profiles.map(async (profile) => {\n let binary = profile.binary;\n let output = await versionOf(binary, path);\n for (const app of profile.apps ?? []) {\n if (output !== \"missing\") break;\n binary = app;\n output = await versionOf(app, path);\n }\n if (output === \"missing\") return undefined;\n const resolved = binary === profile.binary ? profile : { ...profile, binary };\n if (output === \"broken\") {\n const info: AgentInfo = {\n id: profile.id,\n name: profile.name,\n ready: false,\n problem: `\\`${binary} --version\\` failed: check that ${profile.name} runs in a terminal.`,\n };\n return { info, profile: resolved };\n }\n const version = /\\d+\\.\\d+\\.\\d+[\\w.-]*/.exec(output)?.[0];\n const info: AgentInfo = {\n id: profile.id,\n name: profile.name,\n ready: true,\n ...(version ? { version } : {}),\n };\n return { info, profile: resolved };\n }),\n );\n const present = found.filter((entry) => entry !== undefined);\n return {\n agents: present.map((entry) => entry.info),\n profiles: profiles.map(\n (profile) =>\n present.find((entry) => entry.profile.id === profile.id)?.profile ?? profile,\n ),\n };\n}\n\n/** What `hostReport` sends: the installed agents; one that is not installed is left out. */\nexport async function detectAgents(\n profiles: readonly AgentProfile[],\n): Promise<AgentInfo[]> {\n return [...(await findAgents(profiles)).agents];\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAW9C,OAAO,EAA8B,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAMzF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAQ/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,eAAe,CAAC;AAI9D,OAAO,EAAE,KAAK,WAAW,EAAoC,MAAM,cAAc,CAAC;AAElF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,6GAA6G;IAC7G,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,kFAAkF;IAClF,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAChE,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACnE,kFAAkF;IAClF,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAChF,4EAA4E;IAC5E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAiBD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAM,GAAG,SAAuB,GACtC,OAAO,CAAC,IAAI,CAAC,CAqBf;AAWD,6FAA6F;AAC7F,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,SAAkB,IAAI,wBAAwB;IAC9C,cAEC;CACF;AAKD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa,CAssBhE"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAY9C,OAAO,EAA8B,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAMzF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAQ/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,eAAe,CAAC;AAI9D,OAAO,EAAE,KAAK,WAAW,EAAoC,MAAM,cAAc,CAAC;AAElF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,6GAA6G;IAC7G,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,kFAAkF;IAClF,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAChE,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACnE,kFAAkF;IAClF,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAChF,4EAA4E;IAC5E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,sFAAsF;AACtF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAiBD;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAM,GAAG,SAAuB,GACtC,OAAO,CAAC,IAAI,CAAC,CAqBf;AAWD,6FAA6F;AAC7F,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,SAAkB,IAAI,wBAAwB;IAC9C,cAEC;CACF;AAKD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa,CA0zBhE"}
package/dist/daemon.js CHANGED
@@ -3,9 +3,10 @@ import { createServer } from "node:net";
3
3
  import { basename, delimiter, dirname, join } from "node:path";
4
4
  import { HOST_PROTOCOL, } from "@parlor/chat";
5
5
  import { ActionError, asId, DEV_USER_ID } from "@parlor/core";
6
+ import { removeAgentSkills, writeAgentSkills } from "./agent-skills.js";
6
7
  import { readUsage, surveyOptions } from "./agent-survey.js";
7
8
  import { agentNames, registerAgentTools, unregisterAgentTools, } from "./agent-tools.js";
8
- import { detectAgents, loadProfiles } from "./agents.js";
9
+ import { findAgents, loadProfiles } from "./agents.js";
9
10
  import { connectControl } from "./control.js";
10
11
  import { requestDevHostToken } from "./dev-host.js";
11
12
  import { devHostKeys } from "./grants.js";
@@ -22,7 +23,7 @@ import { compareVersions, RELEASE_KEY, signedRelease } from "./release.js";
22
23
  import { revokeGrant } from "./revoke.js";
23
24
  import { selfEntry } from "./self.js";
24
25
  import { installLoginItem, installRelease } from "./service.js";
25
- import { agentEnvironment } from "./shell-path.js";
26
+ import { agentEnvironment, rereadAgentPath } from "./shell-path.js";
26
27
  import { runTurn } from "./turn.js";
27
28
  const CONNECT_TIMEOUT_MS = 15_000;
28
29
  /** An agent token is asked for again this long before it runs out. */
@@ -72,6 +73,8 @@ export function createHostDaemon(deps) {
72
73
  const devTokens = new Map();
73
74
  const name = computerName();
74
75
  const platform = platformName();
76
+ /** As `agents.json` lists them; `profiles` are these with each `binary` set to the CLI found. */
77
+ let listedProfiles = [];
75
78
  let profiles = [];
76
79
  let agents = [];
77
80
  const learned = learnedStore(paths.learned);
@@ -246,21 +249,112 @@ export function createHostDaemon(deps) {
246
249
  agents: agentNames(profiles, { name: tools.name, agents: tools.agents }),
247
250
  };
248
251
  };
252
+ /** The skill that sends the agents to the app's tools when the user names it, kept current. */
253
+ const writeSkills = async (grant) => {
254
+ const tools = grant.agentTools;
255
+ if (grant.dev || !tools?.on || !tools.name || !tools.agents?.length)
256
+ return;
257
+ await writeAgentSkills(tools.agents, {
258
+ name: tools.name,
259
+ appName: grant.appName,
260
+ appOrigin: grant.appOrigin,
261
+ account: grant.account.email ?? grant.account.name,
262
+ }, await agentEnvironment());
263
+ };
264
+ let toolsWork = Promise.resolve();
265
+ /** Registering and removing the app with the agents runs one at a time, on the grant as saved by then. */
266
+ const serially = (work) => {
267
+ const next = toolsWork.then(work, work);
268
+ toolsWork = next.catch(() => undefined);
269
+ return next;
270
+ };
249
271
  /** Registers the app with the agents here once the user chose it and it is not yet. */
250
- const ensureAgentTools = async (grant) => {
251
- if (!grant.agentTools?.on || grant.dev || grant.agentTools.agents)
272
+ const ensureAgentTools = (grant) => serially(() => ensureAgentToolsNow(grant));
273
+ const ensureAgentToolsNow = async (grant) => {
274
+ if (!grant.agentTools?.on || grant.dev)
252
275
  return grant;
276
+ if (grant.agentTools.agents) {
277
+ await writeSkills(grant);
278
+ return grant;
279
+ }
253
280
  const registered = await registerAgentTools(toolsDeps(), grant);
254
281
  const next = { ...grant, agentTools: { on: true, ...registered } };
255
282
  await deps.grants.save(next);
283
+ await writeSkills(next);
256
284
  return next;
257
285
  };
258
- const removeAgentTools = async (grant) => {
286
+ /** Hands the app to agents installed since it was registered with the ones here then. */
287
+ const handToNewAgents = (origin) => serially(async () => {
288
+ const grant = await deps.grants.get(origin);
289
+ if (grant)
290
+ await handToNewAgentsNow(grant);
291
+ });
292
+ const handToNewAgentsNow = async (grant) => {
293
+ const tools = grant.agentTools;
294
+ if (grant.declined || grant.dev || !tools?.on || !tools.agents)
295
+ return;
296
+ const have = new Set(tools.agents);
297
+ const fresh = toolsDeps().profiles.filter((profile) => !have.has(profile.id));
298
+ if (fresh.length === 0) {
299
+ await writeSkills(grant);
300
+ return;
301
+ }
302
+ const registered = await registerAgentTools({ ...toolsDeps(), profiles: fresh }, grant);
303
+ if (registered.agents.length === 0) {
304
+ await writeSkills(grant);
305
+ return;
306
+ }
307
+ const next = {
308
+ ...grant,
309
+ agentTools: {
310
+ on: true,
311
+ name: registered.name,
312
+ agents: [...tools.agents, ...registered.agents],
313
+ },
314
+ };
315
+ await deps.grants.save(next);
316
+ latest.set(grant.appOrigin, next);
317
+ await writeSkills(next);
318
+ };
319
+ let checking;
320
+ let checkAgain = false;
321
+ /**
322
+ * Looks for the agents again because a tab asked: one installed since this computer
323
+ * started shows in every app's sidebar, and gets the apps the user let its agents use.
324
+ */
325
+ const checkAgents = () => {
326
+ if (checking) {
327
+ checkAgain = true;
328
+ return checking;
329
+ }
330
+ checking = (async () => {
331
+ do {
332
+ checkAgain = false;
333
+ await rereadAgentPath();
334
+ const found = await findAgents(listedProfiles);
335
+ profiles = found.profiles;
336
+ agents = [...found.agents];
337
+ for (const grant of await deps.grants.list()) {
338
+ await handToNewAgents(grant.appOrigin).catch((error) => logEvent("agent_tools_failed", {
339
+ appOrigin: grant.appOrigin,
340
+ error: String(error),
341
+ }));
342
+ }
343
+ await Promise.all([...links.values()].map((state) => report(state, true).catch(() => undefined)));
344
+ logEvent("agents_checked", { agents: agents.map((agent) => agent.id) });
345
+ } while (checkAgain);
346
+ })().finally(() => {
347
+ checking = undefined;
348
+ });
349
+ return checking;
350
+ };
351
+ const removeAgentTools = (grant) => serially(async () => {
259
352
  const tools = grant?.agentTools;
260
353
  if (!tools?.name || !tools.agents?.length)
261
354
  return;
262
355
  await unregisterAgentTools(toolsDeps(), { name: tools.name, agents: tools.agents });
263
- };
356
+ await removeAgentSkills(tools.agents, tools.name, await agentEnvironment());
357
+ });
264
358
  const agentTokens = new Map();
265
359
  /** The grant as last saved, which a link opened before a change still holds the old copy of. */
266
360
  const latest = new Map();
@@ -323,6 +417,7 @@ export function createHostDaemon(deps) {
323
417
  platform,
324
418
  fetch: (input, init) => fetch(input, init),
325
419
  onWake: () => wake(state),
420
+ onAgentsCheck: () => void checkAgents(),
326
421
  onPushed: (_link, turn) => {
327
422
  // Tells the app this computer has the turn, while it already starts on it.
328
423
  void link.api.calls
@@ -384,11 +479,13 @@ export function createHostDaemon(deps) {
384
479
  ? { ...agent, outsideName: tools.name }
385
480
  : agent);
386
481
  };
387
- const report = async (state) => {
482
+ /** `agentsChecked` when the agents were just looked for, which a tab waiting on a check reads. */
483
+ const report = async (state, agentsChecked = false) => {
388
484
  const reported = state.link.api.calls.call("hostReport", {
389
485
  online: true,
390
486
  agents: describeFor(state.link.grant.appOrigin),
391
487
  ...(await pinnedDevices(state)),
488
+ ...(agentsChecked ? { agentsChecked: true } : {}),
392
489
  });
393
490
  const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("The app did not answer in time.")), CONNECT_TIMEOUT_MS));
394
491
  try {
@@ -416,7 +513,7 @@ export function createHostDaemon(deps) {
416
513
  const attach = async (origin, hostToken) => {
417
514
  await closeLink(origin, "replaced");
418
515
  devTokens.set(origin, hostToken);
419
- await report(open(await devGrant(origin), devHostKeys(hostToken)));
516
+ await report(open(await devGrant(origin), devHostKeys(hostToken)), true);
420
517
  };
421
518
  const reattach = async (grant) => {
422
519
  await closeLink(grant.appOrigin, "dev_server_restarted");
@@ -471,8 +568,10 @@ export function createHostDaemon(deps) {
471
568
  openHostLog(join(paths.root, "logs", "daemon.log"));
472
569
  await putHostCommandFirstOnPath(paths.root);
473
570
  await deps.proxy.listen(paths.proxy);
474
- profiles = await loadProfiles(paths.agents);
475
- agents = await detectAgents(profiles);
571
+ listedProfiles = await loadProfiles(paths.agents);
572
+ const found = await findAgents(listedProfiles);
573
+ profiles = found.profiles;
574
+ agents = [...found.agents];
476
575
  await learned.load();
477
576
  finishStartup();
478
577
  }
@@ -487,7 +586,8 @@ export function createHostDaemon(deps) {
487
586
  for (const grant of await deps.grants.list()) {
488
587
  if (grant.declined)
489
588
  continue;
490
- void report(open(grant, deps.keys)).catch((error) => logEvent("link_failed", { appOrigin: grant.appOrigin, error: String(error) }));
589
+ void handToNewAgents(grant.appOrigin).catch(() => undefined);
590
+ void report(open(grant, deps.keys), true).catch((error) => logEvent("link_failed", { appOrigin: grant.appOrigin, error: String(error) }));
491
591
  }
492
592
  },
493
593
  async connect(appOrigin) {
@@ -499,14 +599,14 @@ export function createHostDaemon(deps) {
499
599
  const current = links.get(appOrigin);
500
600
  if (current?.link.grant.grantId === grant.grantId) {
501
601
  latest.set(appOrigin, grant);
502
- await report(current);
602
+ await report(current, true);
503
603
  return toolsOf(grant);
504
604
  }
505
605
  await closeLink(appOrigin, "replaced");
506
606
  if (current)
507
607
  await deps.pool.forget(current.link.grant);
508
608
  latest.set(appOrigin, grant);
509
- await report(open(grant, deps.keys));
609
+ await report(open(grant, deps.keys), true);
510
610
  return toolsOf(grant);
511
611
  },
512
612
  async agentTools(appOrigin, on) {