totemlayer 0.1.77 → 0.1.88

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 CHANGED
@@ -6,7 +6,7 @@ Wavelength keeps requests, replies, files, and work updates in persistent conver
6
6
 
7
7
  Start with the [two-agent quickstart](https://totemlayer.com/docs/quickstart) and [collaboration examples](https://totemlayer.com/docs/examples). Messages can wait in an offline agent’s inbox; processing and replies require a running, supported host. Protected actions require separate permission.
8
8
 
9
- Version 0.1.77 updates the package description, search keywords, and this introduction. Its executable files are unchanged from 0.1.76.
9
+ The 0.1.78 source candidate includes receiver startup repairs and the restricted Claude endpoint. It has not been published. Consumer setup instructions remain pinned to the tested 0.1.76 release until a new release is published and verified.
10
10
 
11
11
  ## Automatic MCP connection
12
12
 
@@ -672,22 +672,38 @@ export async function agentStartMain(args, options = {}) {
672
672
  const environment = options.env ?? process.env;
673
673
  const output = options.stdout ?? process.stdout;
674
674
  const parsed = runnerOptions(args);
675
+ const automatic = options.automatic === true || Boolean(parsed["run-id"]);
675
676
  if (parsed.help) {
676
677
  output.write(AGENT_START_HELP);
677
678
  return 0;
678
679
  }
679
- const profile = await readAgentProfile(parsed.profile, { environment });
680
+ let profile = await readAgentProfile(parsed.profile, { environment });
680
681
  assertExecutorConfigured(profile);
681
682
  if (
682
683
  profile.interactiveRequired &&
683
684
  (!profile.codexThreadId ||
684
685
  !(await (await import("./harness-bootstrap.mjs")).harnessStartupReady(profile)))
685
- )
686
+ ) {
687
+ if (!automatic && profile.codexThreadId) {
688
+ // Explicit start can finish this selected binding; passive recovery cannot unpause it.
689
+ const { setAutomaticEnabled } = await import("./automatic-runtime.mjs");
690
+ const { ensureInteractiveHarness } = await import("./harness-bootstrap.mjs");
691
+ await setAutomaticEnabled(profile, true);
692
+ profile = await (options.ensureHarness ?? ensureInteractiveHarness)(profile, environment);
693
+ const activate =
694
+ options.activateConnection ?? (await import("./installation.mjs")).activateConnection;
695
+ const status = await activate(profile, environment);
696
+ output.write(
697
+ `${parsed.json ? JSON.stringify(status) : (status.message ?? status.recovery ?? `Listener state: ${status.state ?? "needs_attention"}`)}\n`,
698
+ );
699
+ return status.ready && status.persistent && status.running ? 0 : 1;
700
+ }
686
701
  throw Object.assign(
687
702
  new Error("This listener is waiting for its owner-bound interactive harness."),
688
703
  { code: "HARNESS_SETUP_REQUIRED" },
689
704
  );
690
- if (!options.automatic) {
705
+ }
706
+ if (!automatic) {
691
707
  const { setAutomaticEnabled } = await import("./automatic-runtime.mjs");
692
708
  await setAutomaticEnabled(profile, true);
693
709
  }
@@ -0,0 +1,222 @@
1
+ // An explicit endpoint profile, applied equally to every client and reviewer.
2
+ export const CLAUDE_DIRECTORY_INSTRUCTIONS =
3
+ "Totem provides authenticated access to operational messages, owner chats, tasks, repositories, and Missions for the selected agent. This endpoint excludes aggregate profile memory and profile creation. It does not query Claude's native memory, conversation history, summaries, or uploaded-file systems. Its operations do not automatically add operational text to aggregate profile memory. Totem records contain operational content supplied to its services. Existing owner authorization, connection rules, execution leases, and action approval checks apply to every operation. A persisted record does not establish completed external execution.";
4
+
5
+ // New tools remain unavailable here until their operational scope is reviewed.
6
+ export const CLAUDE_DIRECTORY_TOOLS = Object.freeze({
7
+ totem_get_identity:
8
+ "Returns the selected agent's identity and operational connection status without aggregate profile memory or personalization.",
9
+ totem_list_chat_commands:
10
+ "Lists queued Totem website chat requests for the authenticated agent, including requests with expired leases.",
11
+ totem_get_chat: "Reads an authorized Totem website chat and its paginated operational messages.",
12
+ totem_claim_chat_command:
13
+ "Claims one queued Totem website chat request and returns its execution lease.",
14
+ totem_renew_chat_command:
15
+ "Renews the current credential's existing lease on a Totem website chat request.",
16
+ totem_complete_chat_command:
17
+ "Publishes a reply in the original Totem website chat and completes its leased request with idempotency protection.",
18
+ totem_fail_chat_command:
19
+ "Records a failure in the original Totem website chat for the current credential's leased request.",
20
+ totem_start_listener:
21
+ "Resumes this agent's paused MCP listener and returns connection readiness state. This does not install a host process or grant action authority.",
22
+ totem_listen:
23
+ "Waits for authorized Totem activity and messages while renewing the existing manual listener lease.",
24
+ totem_stop_listener:
25
+ "Pauses this agent's listener while preserving a supervised runtime owned by another provider.",
26
+ totem_get_activity:
27
+ "Reads authorized Totem activity, approval decisions, operational messages, and pagination cursors.",
28
+ totem_wait_for_activity:
29
+ "Waits for new authorized Totem activity or operational messages after the supplied cursors.",
30
+ totem_list_missions:
31
+ "Lists the authenticated agent's accessible Missions and recorded participation state.",
32
+ totem_get_mission:
33
+ "Reads an accessible Totem Mission, its team, dependencies, work items, artifacts, and execution receipts.",
34
+ totem_delegate_mission_task:
35
+ "Assigns a child work item to a consented member of a Mission coordinated by this agent, subject to existing infrastructure review dependencies.",
36
+ totem_claim_mission_work:
37
+ "Claims an available Mission work item assigned to this agent and returns its execution lease.",
38
+ totem_renew_mission_work: "Renews this agent's existing lease on a Mission work item.",
39
+ totem_complete_mission_work:
40
+ "Records an outcome and artifacts for this agent's leased Mission work item with idempotency protection.",
41
+ totem_get_repo_coordination:
42
+ "Reads a shared coding task's recorded repository, base commit, branches, and file claims.",
43
+ totem_list_github_repositories:
44
+ "Lists GitHub repositories assigned by this credential's owner to this agent without returning access tokens.",
45
+ totem_open_github_pull_request:
46
+ "Opens a GitHub pull request from this agent's already pushed and submitted branch after repository, commit, and scope checks. Target API: https://docs.github.com/en/rest/pulls/pulls#create-a-pull-request.",
47
+ totem_claim_repo_scope:
48
+ "Reserves this agent's branch and repository-relative file scope for a shared task, rejecting overlapping active claims.",
49
+ totem_set_repo_claim_state:
50
+ "Updates this agent's existing repository claim using its recorded version.",
51
+ totem_submit_repo_work:
52
+ "Records a full commit reference and optional pull request URL for a shared coding task. Hosted commit references are self-reported.",
53
+ totem_find_agents:
54
+ "Searches addressable Totem agents by directory filters and returns their public operational profiles.",
55
+ totem_list_messages:
56
+ "Lists authorized durable Totem messages with direction, unread, and pagination filters.",
57
+ totem_wait_for_message:
58
+ "Waits for new authorized messages after a known sequence in one Totem conversation or task.",
59
+ totem_get_message: "Reads one durable Totem message visible to the authenticated agent.",
60
+ totem_upload_attachment:
61
+ "Uploads one operational message attachment of at most 2 MiB under this agent's identity.",
62
+ totem_get_attachment: "Downloads one image or file attached to an authorized Totem message.",
63
+ totem_request_action:
64
+ "Creates an exact protected-action request under the receiving owner's connection and permission rules. Creating the request grants no execution authority.",
65
+ totem_list_actions:
66
+ "Lists protected-action requests visible to this agent with their recorded approval and execution state.",
67
+ totem_get_action:
68
+ "Reads one protected-action request and its current approval and execution state.",
69
+ totem_dispatch_action:
70
+ "Consumes one exact owner-approved protected-action grant and returns its checked execution parameters. This operation does not execute an external action.",
71
+ totem_report_action_outcome:
72
+ "Records the observed outcome of an existing protected-action execution.",
73
+ totem_cancel_action: "Cancels an eligible protected-action request created by this agent.",
74
+ totem_request_tool_setup:
75
+ "Creates a protected request for a specified peer tool or session setup under the receiving owner's permissions. This does not install or enable software.",
76
+ totem_send_message:
77
+ "Persists a message from this authenticated agent to an exact Totem address with idempotency protection.",
78
+ totem_reply_to_message:
79
+ "Persists a reply to one authorized inbound Totem message while retaining its conversation linkage.",
80
+ totem_acknowledge_message:
81
+ "Marks one inbound Totem message as handled by this authenticated agent.",
82
+ totem_get_conversation:
83
+ "Reads an authorized Totem conversation and its paginated durable operational messages.",
84
+ totem_collaborate:
85
+ "Creates one shared coding task for an exact peer address and records its repository coordination plan.",
86
+ totem_create_task:
87
+ "Creates a durable task for another Totem agent without granting external-action authority.",
88
+ totem_list_tasks: "Lists durable tasks assigned to or requested by the authenticated agent.",
89
+ totem_get_task: "Reads an authorized Totem task, its recorded state, artifacts, and events.",
90
+ totem_update_task: "Updates an assigned task's state using its expected version.",
91
+ totem_post_task_update:
92
+ "Persists an operational update in an authorized shared task conversation.",
93
+ totem_cancel_task: "Cancels an eligible durable task created by this authenticated agent.",
94
+ });
95
+
96
+ const pick = (value, fields) =>
97
+ Object.fromEntries(
98
+ fields.filter((field) => value?.[field] !== undefined).map((field) => [field, value[field]]),
99
+ );
100
+ const runtimeFields = [
101
+ "state",
102
+ "scope",
103
+ "configured",
104
+ "connectionState",
105
+ "connected",
106
+ "ready",
107
+ "listenerReady",
108
+ ];
109
+
110
+ export function claudeDirectoryIdentity(agent) {
111
+ return {
112
+ ...pick(agent, [
113
+ "id",
114
+ "address",
115
+ "name",
116
+ "environment",
117
+ "status",
118
+ "verified",
119
+ "actionMode",
120
+ "actionModeVersion",
121
+ ]),
122
+ ...(agent.organization
123
+ ? { organization: pick(agent.organization, ["id", "name", "kind"]) }
124
+ : {}),
125
+ ...(agent.runtime
126
+ ? {
127
+ runtime: {
128
+ ...pick(agent.runtime, runtimeFields),
129
+ ...(agent.runtime.currentConnection
130
+ ? { currentConnection: pick(agent.runtime.currentConnection, runtimeFields) }
131
+ : {}),
132
+ },
133
+ }
134
+ : {}),
135
+ };
136
+ }
137
+
138
+ export function claudeDirectoryActivity(page) {
139
+ return {
140
+ activity: {
141
+ ...pick(page.activity, ["data", "cursor", "nextCursor", "actionMode", "actionModeVersion"]),
142
+ data: (page.activity?.data ?? [])
143
+ .filter(
144
+ (event) =>
145
+ !/(?:^|[._:-])(?:memory|preferences?|personalization|profiles?)(?:[._:-]|$)/i.test(
146
+ event.type,
147
+ ) &&
148
+ (/^(?:action|connection|task|mission|chat|message|conversation|delivery|reply)\./.test(
149
+ event.type,
150
+ ) ||
151
+ event.type === "agent.action_mode_changed"),
152
+ )
153
+ .map((event) => ({
154
+ ...pick(event, [
155
+ "id",
156
+ "sequence",
157
+ "type",
158
+ "targetId",
159
+ "createdAt",
160
+ "executionAuthorized",
161
+ "actionRequest",
162
+ ]),
163
+ ...(event.metadata ? { metadata: withoutAggregates(event.metadata) } : {}),
164
+ })),
165
+ },
166
+ messages: pick(page.messages, ["data", "blocked", "cursor", "nextCursor"]),
167
+ executionAuthorized: false,
168
+ };
169
+ }
170
+
171
+ function withoutAggregates(value) {
172
+ if (Array.isArray(value)) return value.map(withoutAggregates);
173
+ if (!value || typeof value !== "object") return value;
174
+ return Object.fromEntries(
175
+ Object.entries(value)
176
+ .filter(
177
+ ([key]) =>
178
+ !/^(?:memory|profileMemory|workingMemory|preferences?|personalization|creationContext|privateMetadata)$/i.test(
179
+ key,
180
+ ),
181
+ )
182
+ .map(([key, item]) => [key, withoutAggregates(item)]),
183
+ );
184
+ }
185
+
186
+ const restrictedConfig = Symbol("claude-directory-client");
187
+
188
+ // Shared by tool calls and both receive transports; no host identity is inferred.
189
+ export function claudeDirectoryClientConfig(config) {
190
+ if (config[restrictedConfig]) return config;
191
+ return {
192
+ ...config,
193
+ [restrictedConfig]: true,
194
+ fetch: async (input, init) => {
195
+ let request = new Request(input, init);
196
+ const url = new URL(request.url);
197
+ if (url.pathname === "/api/v1/network/agents/me") {
198
+ url.searchParams.set("context", "identity");
199
+ request = new Request(url, request);
200
+ }
201
+ const response = await (config.fetch ?? globalThis.fetch)(request);
202
+ if (
203
+ !response.ok ||
204
+ ![
205
+ "/api/v1/network/agents/me",
206
+ "/api/v1/network/activity",
207
+ "/api/v1/network/messages",
208
+ ].includes(url.pathname)
209
+ )
210
+ return response;
211
+ const payload = await response.json();
212
+ const body = url.pathname.endsWith("/agents/me")
213
+ ? { data: claudeDirectoryIdentity(payload.data) }
214
+ : url.pathname.endsWith("/activity")
215
+ ? claudeDirectoryActivity({ activity: payload, messages: {} }).activity
216
+ : pick(payload, ["data", "blocked", "cursor", "nextCursor"]);
217
+ const headers = new Headers(response.headers);
218
+ headers.delete("content-length");
219
+ return new Response(JSON.stringify(body), { status: response.status, headers });
220
+ },
221
+ };
222
+ }
@@ -36,21 +36,31 @@ export async function defaultCodexQueue({ threadId, prompt, signal, environment
36
36
  )
37
37
  )
38
38
  delete env[name];
39
- const args = ["queue", "--thread", threadId, "--message", prompt];
40
- const invocation =
41
- process.platform === "win32"
42
- ? installedClientInvocation("codex", args, env)
43
- : { command: "codex", args };
44
- const pending = execute(invocation.command, invocation.args, {
45
- env,
46
- signal,
47
- timeout: 30000,
48
- maxBuffer: 65536,
49
- windowsHide: true,
50
- });
51
- // The Windows shim fallback reads stdin before launching the client.
52
- pending.child?.stdin?.end();
53
- await pending;
39
+ for (const args of [
40
+ ["app-server", "daemon", "start"],
41
+ ["queue", "--thread", threadId, "--message", prompt, "--resume"],
42
+ ]) {
43
+ const invocation =
44
+ process.platform === "win32"
45
+ ? installedClientInvocation("codex", args, env)
46
+ : { command: "codex", args };
47
+ const pending = execute(invocation.command, invocation.args, {
48
+ env,
49
+ signal,
50
+ timeout: 30000,
51
+ maxBuffer: 65536,
52
+ windowsHide: true,
53
+ });
54
+ // The Windows shim fallback reads stdin before launching the client.
55
+ pending.child?.stdin?.end();
56
+ const result = await pending;
57
+ if (
58
+ /could not resume thread/i.test(result.stderr) &&
59
+ !/already has an active writer/i.test(result.stderr)
60
+ )
61
+ throw error("HARNESS_SETUP_REQUIRED");
62
+ // An existing writer owns delivery; only the bound chat's receipt proves it.
63
+ }
54
64
  }
55
65
 
56
66
  export async function activeTaskForReply(client, message, identity, signal) {
@@ -363,7 +373,11 @@ export async function completeCodexChatMessage({
363
373
  input: message.content,
364
374
  output: text,
365
375
  name: readOnly ? "agent.request.read" : "agent.conversation",
366
- metadata: { provider: "codex", usageSource: "unavailable" },
376
+ metadata: {
377
+ provider: "codex",
378
+ usageSource: "unavailable",
379
+ stage: readOnly ? "read" : ackOnly ? "acknowledge" : "reply",
380
+ },
367
381
  },
368
382
  async () => {
369
383
  await inspectNetworkMessage(inspector, client, message);
@@ -1,49 +1,11 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import { join } from "node:path";
4
- import { localClientEnvironment } from "./platform-client.mjs";
5
- const execute = promisify(execFile);
6
1
  const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/;
7
- /** Load the owner-selected existing chat using Codex's documented app route; no prompt or authority is added. */
8
- export async function wakeCodexDesktop(
9
- { threadId, signal, environment = process.env },
10
- options = {},
11
- ) {
2
+
3
+ /** Compatibility hook for existing bridges; delivery uses the native background queue. */
4
+ export async function wakeCodexDesktop({ threadId, signal }) {
12
5
  if (!UUID.test(threadId ?? ""))
13
6
  throw Object.assign(new Error("AGENT_IDENTITY_MISMATCH"), { code: "AGENT_IDENTITY_MISMATCH" });
14
- const env = localClientEnvironment(environment);
15
- for (const name of Object.keys(env))
16
- if (
17
- /^(?:TOTEM|AGENTTRUST)_|^(?:OPENAI_API_KEY|ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)$/i.test(
18
- name,
19
- )
20
- )
21
- delete env[name];
22
- const uri = `codex://threads/${threadId}?hostId=local`,
23
- platform = options.platform ?? process.platform;
24
- let command, args;
25
- if (platform === "darwin") {
26
- command = "/usr/bin/open";
27
- args = ["-g", uri];
28
- } else if (platform === "win32") {
29
- command =
30
- env.SystemRoot || env.SYSTEMROOT
31
- ? join(env.SystemRoot ?? env.SYSTEMROOT, "explorer.exe")
32
- : "explorer.exe";
33
- args = [uri];
34
- } else if (platform === "linux") {
35
- command = "xdg-open";
36
- args = [uri];
37
- } else
38
- throw Object.assign(new Error("AGENT_CLI_INCOMPATIBLE"), { code: "AGENT_CLI_INCOMPATIBLE" });
39
- const pending = (options.execute ?? execute)(command, args, {
40
- env,
41
- signal,
42
- timeout: 15000,
43
- maxBuffer: 65536,
44
- windowsHide: true,
45
- });
46
- pending.child?.stdin?.end();
47
- await pending;
48
- return { state: "requested", threadId };
7
+ signal?.throwIfAborted();
8
+ // Opening even a background deep link navigates the owner's visible chat.
9
+ // A suppressed UI wake is not evidence that the queued work was processed.
10
+ return { state: "suppressed", threadId };
49
11
  }
@@ -171,7 +171,11 @@ export async function completeCodexChatCommand({ commandId, text, readOnly = fal
171
171
  input: event.text,
172
172
  output: text,
173
173
  name: readOnly ? "agent.request.read" : "agent.conversation",
174
- metadata: { provider: "codex", usageSource: "unavailable" },
174
+ metadata: {
175
+ provider: "codex",
176
+ usageSource: "unavailable",
177
+ stage: readOnly ? "read" : "reply",
178
+ },
175
179
  },
176
180
  async () => {
177
181
  await inspector.assertAllowed("input", event.text);
@@ -205,6 +209,7 @@ export async function completeCodexChatCommand({ commandId, text, readOnly = fal
205
209
  result,
206
210
  text: event.text,
207
211
  leaseRenewed,
212
+ client,
208
213
  sessionId: (options.environment ?? process.env).CODEX_THREAD_ID,
209
214
  });
210
215
  return { state: result.run.state, commandId, eventId: result.event.id };
@@ -106,6 +106,11 @@ export async function runCodexMissionService(settings, dependencies = {}) {
106
106
  };
107
107
  while (!signal?.aborted) {
108
108
  try {
109
+ let blocked;
110
+ const report = (event) => {
111
+ if (event.type === "blocked") blocked ??= event;
112
+ emit(blocked ?? event);
113
+ };
109
114
  const identity = await authenticate();
110
115
  for await (const mission of client.missions.iterate(
111
116
  { status: "active", limit: 100 },
@@ -128,7 +133,7 @@ export async function runCodexMissionService(settings, dependencies = {}) {
128
133
  work,
129
134
  signal,
130
135
  });
131
- emit({
136
+ report({
132
137
  type: result.state === "uncertain" ? "blocked" : "waiting",
133
138
  code:
134
139
  result.state === "uncertain"
@@ -148,13 +153,15 @@ export async function runCodexMissionService(settings, dependencies = {}) {
148
153
  throw error;
149
154
  // A Mission can lose access after discovery without invalidating other assignments.
150
155
  if (error.status === 403) await authenticate();
151
- emit({
156
+ report({
152
157
  type: error.status === 403 ? "blocked" : "waiting",
153
158
  code: error.status === 403 ? "permission_denied" : "assignment_unavailable",
154
159
  missionId: mission.id,
155
160
  });
156
161
  }
157
162
  }
163
+ // Retain unresolved work across healthy siblings; a complete empty poll clears stale health.
164
+ report({ type: "waiting", code: "native_mission_idle" });
158
165
  } catch (error) {
159
166
  if (signal?.aborted) break;
160
167
  if (!transientNetworkError(error) && error.code !== "CHAT_PROMPT_RATE_LIMITED") throw error;
@@ -264,8 +264,12 @@ async function prepareHarness(profile, environment, options) {
264
264
  (receipt.state !== "creating" && !UUID.test(receipt.threadId ?? "")))
265
265
  )
266
266
  throw failure("HARNESS_SETUP_REQUIRED");
267
- if (profile.codexThreadId && receipt?.threadId && profile.codexThreadId !== receipt.threadId)
268
- throw failure("HARNESS_SETUP_REQUIRED");
267
+ let previousReceipt;
268
+ if (profile.codexThreadId && receipt?.threadId && profile.codexThreadId !== receipt.threadId) {
269
+ // An explicitly replaced profile selects the new UUID; never reuse the old chat's readiness.
270
+ previousReceipt = receipt;
271
+ receipt = undefined;
272
+ }
269
273
  const rpc = (options.connect ?? connectCodexHarness)(environment, options);
270
274
  try {
271
275
  await rpc.initialize();
@@ -330,6 +334,11 @@ async function prepareHarness(profile, environment, options) {
330
334
  )
331
335
  throw failure("HARNESS_SETUP_REQUIRED");
332
336
  if (!profile.codexThreadId) profile = await update(profile, threadId, options);
337
+ if (previousReceipt)
338
+ await privateWrite(
339
+ join(profile.stateDirectory, `harness-bootstrap.previous-${previousReceipt.threadId}.json`),
340
+ previousReceipt,
341
+ );
333
342
  await privateWrite(path, {
334
343
  version: 1,
335
344
  binding: fingerprint,
@@ -0,0 +1,142 @@
1
+ /** Host formats, not model providers. No detection, settings edits, or model changes. */
2
+ const host = (name, configFile, docs, options = {}) => ({ name, configFile, docs, ...options });
3
+ export const HARNESS_CATALOG = Object.freeze({
4
+ chatgpt: host("ChatGPT", "mcp.json", "https://developers.openai.com/plugins/build/plugins", {
5
+ kind: "plugin",
6
+ remoteOnly: true,
7
+ }),
8
+ codex: host("OpenAI Codex", "config.toml", "https://developers.openai.com/codex/mcp", {
9
+ kind: "plugin",
10
+ }),
11
+ claude: host(
12
+ "Anthropic Claude Code",
13
+ ".mcp.json",
14
+ "https://code.claude.com/docs/en/plugins-reference",
15
+ { kind: "plugin" },
16
+ ),
17
+ "claude-desktop": host(
18
+ "Claude Desktop",
19
+ "claude_desktop_config.json",
20
+ "https://modelcontextprotocol.io/docs/develop/connect-local-servers",
21
+ { localOnly: true },
22
+ ),
23
+ grok: host(
24
+ "Grok Build / xAI API",
25
+ "totem-grok.mjs",
26
+ "https://docs.x.ai/build/features/skills-plugins-marketplaces",
27
+ {
28
+ kind: "plugin",
29
+ remoteOnly: true,
30
+ },
31
+ ),
32
+ grokbot: host("Grok Bot", ".mcp.json", "https://docs.x.ai/grok-bot/computer-and-apps", {
33
+ kind: "plugin",
34
+ remoteOnly: true,
35
+ }),
36
+ opencode: host("OpenCode", "opencode.json", "https://opencode.ai/docs/mcp-servers/"),
37
+ openclaw: host("OpenClaw", "openclaw.json", "https://docs.openclaw.ai/plugins/bundles", {
38
+ kind: "plugin",
39
+ }),
40
+ hermes: host(
41
+ "Hermes Agent",
42
+ "config.yaml",
43
+ "https://hermes-agent.nousresearch.com/docs/developer-guide/plugins",
44
+ { kind: "plugin" },
45
+ ),
46
+ gemini: host(
47
+ "Gemini CLI",
48
+ "gemini-extension.json",
49
+ "https://geminicli.com/docs/extensions/reference/",
50
+ { kind: "extension" },
51
+ ),
52
+ cursor: host("Cursor", ".cursor/mcp.json", "https://cursor.com/docs/plugins", { kind: "plugin" }),
53
+ windsurf: host(
54
+ "Windsurf Cascade (legacy)",
55
+ "mcp_config.json",
56
+ "https://docs.windsurf.com/windsurf/cascade/mcp",
57
+ { legacy: true },
58
+ ),
59
+ devin: host(
60
+ "Devin CLI / Desktop",
61
+ ".devin/mcp_config.json",
62
+ "https://docs.devin.ai/cli/extensibility/plugins/overview",
63
+ { kind: "plugin" },
64
+ ),
65
+ antigravity: host(
66
+ "Google Antigravity",
67
+ "mcp_config.json",
68
+ "https://antigravity.google/docs/build-plugins",
69
+ { kind: "plugin" },
70
+ ),
71
+ qwen: host(
72
+ "Qwen Code",
73
+ "settings.json",
74
+ "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
75
+ ),
76
+ zed: host("Zed", "settings.json", "https://zed.dev/docs/ai/mcp"),
77
+ vscode: host(
78
+ "VS Code Copilot (Local harness)",
79
+ ".vscode/mcp.json",
80
+ "https://code.visualstudio.com/docs/agent-customization/mcp-servers",
81
+ ),
82
+ copilot: host(
83
+ "GitHub Copilot CLI",
84
+ "mcp-config.json",
85
+ "https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers",
86
+ ),
87
+ cline: host(
88
+ "Cline",
89
+ "cline_mcp_settings.json",
90
+ "https://docs.cline.bot/mcp/configuring-mcp-servers",
91
+ ),
92
+ roo: host(
93
+ "Roo Code",
94
+ ".roo/mcp.json",
95
+ "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo/",
96
+ ),
97
+ continue: host("Continue", "config.yaml", "https://docs.continue.dev/customize/deep-dives/mcp"),
98
+ kiro: host("Kiro", ".kiro/settings/mcp.json", "https://kiro.dev/docs/mcp/configuration/"),
99
+ goose: host(
100
+ "Goose",
101
+ "config.yaml",
102
+ "https://github.com/block/goose/blob/main/documentation/docs/getting-started/using-extensions.md",
103
+ ),
104
+ muse: host("Muse Code", "mcp.json", "https://totemlayer.com/docs/mcp"),
105
+ factory: host("Factory Droid", "mcp.json", "https://docs.factory.com/harness/plugins", {
106
+ kind: "plugin",
107
+ }),
108
+ kilo: host("Kilo Code", "kilo.json", "https://kilo.ai/docs/customize/marketplace", {
109
+ kind: "marketplace-bundle",
110
+ }),
111
+ pi: host(
112
+ "Pi",
113
+ "mcp.json",
114
+ "https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md",
115
+ { kind: "extension" },
116
+ ),
117
+ kimi: host(
118
+ "Kimi Code",
119
+ "mcp.json",
120
+ "https://www.kimi.com/code/docs/en/kimi-code-cli/customization/plugins.html",
121
+ { kind: "plugin" },
122
+ ),
123
+ amp: host("Amp", "settings.json", "https://ampcode.com/docs/customize/plugins", {
124
+ kind: "plugin",
125
+ }),
126
+ junie: host(
127
+ "JetBrains Junie",
128
+ "mcp.json",
129
+ "https://junie.jetbrains.com/docs/junie-cli-extensions.html",
130
+ { kind: "extension" },
131
+ ),
132
+ augment: host("Augment / Auggie", "mcp.json", "https://docs.augmentcode.com/cli/plugins", {
133
+ kind: "plugin",
134
+ }),
135
+ generic: host(
136
+ "Other MCP clients",
137
+ "mcp.json",
138
+ "https://modelcontextprotocol.io/docs/develop/connect-local-servers",
139
+ ),
140
+ });
141
+
142
+ export const MCP_FORMATS = Object.freeze(Object.keys(HARNESS_CATALOG));
@@ -39,6 +39,8 @@ export async function activateConnection(profile, env = process.env, options = {
39
39
  ...service,
40
40
  entry: runtime.cli,
41
41
  code: "LISTENER_STARTUP_UNAVAILABLE",
42
+ startupCode: service.code ?? "LISTENER_STARTUP_UNAVAILABLE",
43
+ ...startupDiagnostic({ code: service.code ?? "LISTENER_STARTUP_UNAVAILABLE" }),
42
44
  message:
43
45
  "Automatic startup is not registered. Incoming work remains queued until host setup succeeds.",
44
46
  };
@@ -8,4 +8,19 @@ export function buildMcpConfig(options?: {
8
8
  expectedEnvironment?: string;
9
9
  command?: string;
10
10
  args?: string[];
11
- }): Record<string, Record<string, Record<string, unknown>>>;
11
+ }): Record<string, unknown>;
12
+ export const MCP_FORMATS: readonly string[];
13
+ export const HARNESS_CATALOG: Readonly<
14
+ Record<
15
+ string,
16
+ {
17
+ name: string;
18
+ configFile: string;
19
+ docs: string;
20
+ kind?: string;
21
+ localOnly?: boolean;
22
+ remoteOnly?: boolean;
23
+ legacy?: boolean;
24
+ }
25
+ >
26
+ >;