@getforgeai/cli 0.1.0-beta.1 → 0.1.0-beta.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.
Files changed (39) hide show
  1. package/dist/cli.js +10 -1
  2. package/dist/commands/auth-setup-agent.d.ts +15 -0
  3. package/dist/commands/auth-setup-agent.js +98 -0
  4. package/dist/commands/config-set.js +14 -3
  5. package/dist/commands/connect.js +23 -7
  6. package/dist/config/config-manager.d.ts +2 -0
  7. package/dist/lib/connection-manager.d.ts +11 -0
  8. package/dist/lib/connection-manager.js +48 -25
  9. package/dist/lib/daemon-entry.js +23 -6
  10. package/dist/mcp/mcp-probe.d.ts +20 -0
  11. package/dist/mcp/mcp-probe.js +24 -4
  12. package/dist/orchestrator/agent-profiles.d.ts +65 -0
  13. package/dist/orchestrator/agent-profiles.js +236 -0
  14. package/dist/orchestrator/agent-spawner.d.ts +2 -0
  15. package/dist/orchestrator/agent-spawner.js +25 -14
  16. package/dist/orchestrator/billing-detect.d.ts +20 -0
  17. package/dist/orchestrator/billing-detect.js +120 -0
  18. package/dist/orchestrator/codex-jsonl-extractor.d.ts +52 -0
  19. package/dist/orchestrator/codex-jsonl-extractor.js +210 -0
  20. package/dist/orchestrator/connectors/connector.d.ts +2 -0
  21. package/dist/orchestrator/connectors/docker-connector.js +192 -175
  22. package/dist/orchestrator/opencode-jsonl-extractor.d.ts +47 -0
  23. package/dist/orchestrator/opencode-jsonl-extractor.js +228 -0
  24. package/dist/orchestrator/resume-handler.js +2 -0
  25. package/dist/orchestrator/stream-json-extractor.d.ts +38 -10
  26. package/dist/orchestrator/stream-json-extractor.js +10 -105
  27. package/dist/vm/agent-auth-manager.d.ts +32 -0
  28. package/dist/vm/agent-auth-manager.js +68 -0
  29. package/dist/vm/docker-process-proxy.d.ts +2 -1
  30. package/dist/vm/docker-process-proxy.js +17 -10
  31. package/dist/vm/image-manager.js +1 -1
  32. package/dist/vm/secure-store.d.ts +24 -0
  33. package/dist/vm/secure-store.js +110 -0
  34. package/dist/vm/session-manager.d.ts +33 -5
  35. package/dist/vm/session-manager.js +129 -19
  36. package/dist/vm/session-snapshot.d.ts +13 -8
  37. package/dist/vm/session-snapshot.js +52 -36
  38. package/package.json +2 -2
  39. package/rootfs/Dockerfile +5 -2
package/dist/cli.js CHANGED
@@ -20,6 +20,7 @@ import { sessionRespondAction } from "./commands/session-respond.js";
20
20
  import { taskOpenAction } from "./commands/task-open.js";
21
21
  import { workflowOpenAction } from "./commands/workflow-open.js";
22
22
  import { vmInitAction, vmStatusAction, vmCleanupAction, vmStopAllAction, setupClaudeTokenAction, } from "./commands/vm.js";
23
+ import { setupCodexAuthAction, setupOpenCodeAuthAction, } from "./commands/auth-setup-agent.js";
23
24
  const program = new Command();
24
25
  program
25
26
  .name("forge")
@@ -46,6 +47,14 @@ authCmd
46
47
  .command("setup-claude")
47
48
  .description("Obtain a long-lived Claude Code OAuth token")
48
49
  .action(() => setupClaudeTokenAction());
50
+ authCmd
51
+ .command("setup-codex")
52
+ .description("Store Codex credentials (import codex login or OpenAI API key)")
53
+ .action(() => setupCodexAuthAction());
54
+ authCmd
55
+ .command("setup-opencode")
56
+ .description("Store OpenCode credentials (import opencode login or provider API key)")
57
+ .action(() => setupOpenCodeAuthAction());
49
58
  program
50
59
  .command("connect")
51
60
  .description("Connect to ForgeAI Cloud (spawns background daemon by default)")
@@ -114,7 +123,7 @@ const configSetCmd = configCmd
114
123
  .description("Set a configuration value");
115
124
  configSetCmd
116
125
  .command("agent-type <value>")
117
- .description("Set agent type (CLAUDE_CODE or OPENCODE)")
126
+ .description("Set agent type (CLAUDE_CODE, CODEX or OPENCODE)")
118
127
  .action((value) => configSetAgentTypeAction(value));
119
128
  configSetCmd
120
129
  .command("agent-slots <value>")
@@ -0,0 +1,15 @@
1
+ /**
2
+ * forge auth setup-codex / setup-opencode — store credentials for the Codex
3
+ * and OpenCode agent engines.
4
+ *
5
+ * Two paths per engine:
6
+ * - Import the auth.json produced by the engine's own login flow on this
7
+ * machine (ChatGPT-plan login for Codex, OAuth/Pro-Max for OpenCode).
8
+ * - Paste a provider API key.
9
+ *
10
+ * Credentials are stored via agent-auth-manager (Keychain on macOS,
11
+ * encrypted file elsewhere) and injected into agent containers at spawn.
12
+ */
13
+ export declare function setupCodexAuthAction(): Promise<void>;
14
+ export declare function setupOpenCodeAuthAction(): Promise<void>;
15
+ //# sourceMappingURL=auth-setup-agent.d.ts.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * forge auth setup-codex / setup-opencode — store credentials for the Codex
3
+ * and OpenCode agent engines.
4
+ *
5
+ * Two paths per engine:
6
+ * - Import the auth.json produced by the engine's own login flow on this
7
+ * machine (ChatGPT-plan login for Codex, OAuth/Pro-Max for OpenCode).
8
+ * - Paste a provider API key.
9
+ *
10
+ * Credentials are stored via agent-auth-manager (Keychain on macOS,
11
+ * encrypted file elsewhere) and injected into agent containers at spawn.
12
+ */
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { createInterface } from "node:readline";
15
+ import chalk from "chalk";
16
+ import { getHostAuthJsonPath, saveAgentAuth, } from "../vm/agent-auth-manager.js";
17
+ import { setConfig } from "../config/config-manager.js";
18
+ function ask(question) {
19
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
20
+ return new Promise((resolve) => {
21
+ rl.question(question, (answer) => {
22
+ rl.close();
23
+ resolve(answer.trim());
24
+ });
25
+ });
26
+ }
27
+ /** Offer to import the engine's own auth.json from the host. Returns true if imported. */
28
+ async function tryImportHostAuthJson(agent) {
29
+ const hostPath = getHostAuthJsonPath(agent);
30
+ if (!existsSync(hostPath))
31
+ return false;
32
+ const answer = await ask(`Found existing credentials at ${hostPath}. Import them? [Y/n] `);
33
+ if (answer && !/^y(es)?$/i.test(answer))
34
+ return false;
35
+ const content = readFileSync(hostPath, "utf-8");
36
+ await saveAgentAuth(agent, { kind: "auth_json", value: content });
37
+ console.log(chalk.green("Credentials imported and stored securely."));
38
+ return true;
39
+ }
40
+ export async function setupCodexAuthAction() {
41
+ try {
42
+ if (await tryImportHostAuthJson("CODEX"))
43
+ return;
44
+ console.log(chalk.dim("No Codex login found to import. You can paste an OpenAI API key instead\n" +
45
+ "(or run `codex login` first, then re-run this command to import it)."));
46
+ const key = await ask("OpenAI API key (sk-...): ");
47
+ if (!key) {
48
+ console.error(chalk.red("No API key provided."));
49
+ process.exit(1);
50
+ }
51
+ if (!key.startsWith("sk-")) {
52
+ console.log(chalk.yellow("Warning: key does not start with 'sk-' — storing as-is."));
53
+ }
54
+ await saveAgentAuth("CODEX", { kind: "api_key", value: key });
55
+ console.log(chalk.green("Codex API key stored. Agents can now authenticate."));
56
+ }
57
+ catch (err) {
58
+ console.error(chalk.red(`Failed: ${err instanceof Error ? err.message : String(err)}`));
59
+ process.exit(1);
60
+ }
61
+ }
62
+ const OPENCODE_PROVIDER_ENV_VARS = {
63
+ anthropic: "ANTHROPIC_API_KEY",
64
+ openai: "OPENAI_API_KEY",
65
+ openrouter: "OPENROUTER_API_KEY",
66
+ };
67
+ export async function setupOpenCodeAuthAction() {
68
+ try {
69
+ const imported = await tryImportHostAuthJson("OPENCODE");
70
+ if (!imported) {
71
+ console.log(chalk.dim("No OpenCode login found to import. You can configure a provider API key instead\n" +
72
+ "(or run `opencode auth login` first, then re-run this command to import it)."));
73
+ const provider = (await ask("Provider (anthropic / openai / openrouter) [anthropic]: ")).toLowerCase() || "anthropic";
74
+ const envVar = OPENCODE_PROVIDER_ENV_VARS[provider];
75
+ if (!envVar) {
76
+ console.error(chalk.red(`Unknown provider "${provider}". Allowed: ${Object.keys(OPENCODE_PROVIDER_ENV_VARS).join(", ")}`));
77
+ process.exit(1);
78
+ }
79
+ const key = await ask(`${envVar} value: `);
80
+ if (!key) {
81
+ console.error(chalk.red("No API key provided."));
82
+ process.exit(1);
83
+ }
84
+ await saveAgentAuth("OPENCODE", { kind: "api_key", envVar, value: key });
85
+ console.log(chalk.green("OpenCode API key stored. Agents can now authenticate."));
86
+ }
87
+ const model = await ask("OpenCode model (provider/model, e.g. anthropic/claude-sonnet-4-5) — leave empty for OpenCode's default: ");
88
+ if (model) {
89
+ setConfig({ opencodeModel: model });
90
+ console.log(chalk.green(`OpenCode model set to ${model}.`));
91
+ }
92
+ }
93
+ catch (err) {
94
+ console.error(chalk.red(`Failed: ${err instanceof Error ? err.message : String(err)}`));
95
+ process.exit(1);
96
+ }
97
+ }
98
+ //# sourceMappingURL=auth-setup-agent.js.map
@@ -1,19 +1,27 @@
1
1
  import chalk from "chalk";
2
+ import { AGENT_TYPE, isAgentType } from "@getforgeai/protocol";
2
3
  import { getConfig, setConfig } from "../config/config-manager.js";
3
4
  import { isDaemonRunning } from "../lib/daemon.js";
4
5
  import { DaemonNotRunningError, sendIpcCommand } from "../lib/ipc-client.js";
5
- const VALID_AGENT_TYPES = ["CLAUDE_CODE"];
6
6
  const VALID_OPENERS = ["terminal", "vscode", "finder"];
7
+ const AGENT_AUTH_SETUP_HINTS = {
8
+ CODEX: "forge auth setup-codex",
9
+ OPENCODE: "forge auth setup-opencode",
10
+ };
7
11
  export async function configSetAgentTypeAction(value) {
8
12
  const upper = value.toUpperCase();
9
- if (!VALID_AGENT_TYPES.includes(upper)) {
10
- console.error(chalk.red(`Invalid agent type "${value}". Allowed: ${VALID_AGENT_TYPES.join(", ")}`));
13
+ if (!isAgentType(upper)) {
14
+ console.error(chalk.red(`Invalid agent type "${value}". Allowed: ${AGENT_TYPE.join(", ")}`));
11
15
  process.exitCode = 1;
12
16
  return;
13
17
  }
14
18
  setConfig({ agentType: upper });
15
19
  const applied = await tryReRegister();
16
20
  console.log(chalk.green(`Agent type set to ${upper}. ${applied ? "(Applied immediately)" : "(Will apply on next connect)"}`));
21
+ const hint = AGENT_AUTH_SETUP_HINTS[upper];
22
+ if (hint) {
23
+ console.log(chalk.dim(`Make sure credentials are configured: ${hint}`));
24
+ }
17
25
  }
18
26
  export async function configSetAgentSlotsAction(value) {
19
27
  const num = Number.parseInt(value, 10);
@@ -40,6 +48,9 @@ export function configGetAction() {
40
48
  const config = getConfig();
41
49
  console.log(chalk.dim("Current configuration:"));
42
50
  console.log(` Agent type: ${config.agentType ?? "CLAUDE_CODE"}`);
51
+ if (config.agentType === "OPENCODE") {
52
+ console.log(` OpenCode model: ${config.opencodeModel ?? "(OpenCode default)"}`);
53
+ }
43
54
  console.log(` Agent slots: ${config.agentSlots ?? 5}`);
44
55
  console.log(` Opener: ${config.opener ?? "terminal"}`);
45
56
  console.log(` Server URL: ${config.serverUrl}`);
@@ -122,14 +122,30 @@ async function connectForeground(options) {
122
122
  const ctx = resolveConnectContext(options);
123
123
  if (!ctx)
124
124
  return;
125
- // Ensure Claude OAuth token is available for container authentication
126
- try {
127
- const { ensureClaudeToken } = await import("../vm/claude-token-manager.js");
128
- await ensureClaudeToken();
125
+ // Ensure agent credentials are available for container authentication
126
+ if (ctx.agentType === "CODEX" || ctx.agentType === "OPENCODE") {
127
+ try {
128
+ const { hasAgentAuth } = await import("../vm/agent-auth-manager.js");
129
+ if (!(await hasAgentAuth(ctx.agentType))) {
130
+ const setupCmd = ctx.agentType === "CODEX"
131
+ ? "forge auth setup-codex"
132
+ : "forge auth setup-opencode";
133
+ console.warn(chalk.yellow(`[connect] No ${ctx.agentType} credentials stored. Agents may fail to authenticate. Run: ${setupCmd}`));
134
+ }
135
+ }
136
+ catch {
137
+ /* Non-fatal */
138
+ }
129
139
  }
130
- catch (err) {
131
- console.warn(chalk.yellow(`[connect] Claude token setup failed: ${err instanceof Error ? err.message : String(err)}`));
132
- console.warn(chalk.yellow("[connect] Agents may fail to authenticate. Run: forge auth setup-claude"));
140
+ else {
141
+ try {
142
+ const { ensureClaudeToken } = await import("../vm/claude-token-manager.js");
143
+ await ensureClaudeToken();
144
+ }
145
+ catch (err) {
146
+ console.warn(chalk.yellow(`[connect] Claude token setup failed: ${err instanceof Error ? err.message : String(err)}`));
147
+ console.warn(chalk.yellow("[connect] Agents may fail to authenticate. Run: forge auth setup-claude"));
148
+ }
133
149
  }
134
150
  // Initialize VM manager (helper process or direct addon)
135
151
  try {
@@ -11,6 +11,8 @@ export type CliConfig = {
11
11
  cliId?: string;
12
12
  cliName?: string;
13
13
  agentType?: string;
14
+ /** OpenCode model in "provider/model" form (used when agentType is OPENCODE). */
15
+ opencodeModel?: string;
14
16
  agentSlots?: number;
15
17
  opener?: Opener;
16
18
  lastSyncCursor?: string;
@@ -10,9 +10,20 @@ export type OrgConnection = {
10
10
  journal: EventJournal;
11
11
  emitter: ResilientEmitter;
12
12
  heartbeatInterval: ReturnType<typeof setInterval>;
13
+ /** Mutable context shared by this org's dispatch/resume handlers. */
14
+ spawnerCtx: {
15
+ agentType: string;
16
+ agentSlots: number;
17
+ };
13
18
  };
14
19
  /** Active connections keyed by orgId. */
15
20
  export declare const activeConnections: Map<string, OrgConnection>;
21
+ /**
22
+ * Apply a new agent engine to every live org connection's spawner context, so
23
+ * already-registered dispatch/resume handlers pick it up without reconnecting.
24
+ * Called from the cli:config:updated handler and the daemon re-register IPC.
25
+ */
26
+ export declare function applyAgentTypeToActiveConnections(agentType: string): void;
16
27
  export type ConnectOptions = {
17
28
  server?: string;
18
29
  org?: string;
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import chalk from "chalk";
3
+ import { isAgentType } from "@getforgeai/protocol";
3
4
  import { getConfig, getServerUrl, setConfig, } from "../config/config-manager.js";
4
5
  import { loadToken, stableHostname } from "../config/token-manager.js";
5
6
  import { createRestClient, RestClientError } from "./rest-client.js";
@@ -19,6 +20,16 @@ let mcpReprobeTimer = null;
19
20
  let mcpProbeInFlight = false; // Prevent concurrent probe runs
20
21
  /** Active connections keyed by orgId. */
21
22
  export const activeConnections = new Map();
23
+ /**
24
+ * Apply a new agent engine to every live org connection's spawner context, so
25
+ * already-registered dispatch/resume handlers pick it up without reconnecting.
26
+ * Called from the cli:config:updated handler and the daemon re-register IPC.
27
+ */
28
+ export function applyAgentTypeToActiveConnections(agentType) {
29
+ for (const conn of activeConnections.values()) {
30
+ conn.spawnerCtx.agentType = agentType;
31
+ }
32
+ }
22
33
  /**
23
34
  * Run MCP health probe and manage re-probe lifecycle.
24
35
  * Called once after connections are established.
@@ -190,40 +201,40 @@ export function establishConnections(ctx, append = false) {
190
201
  registerArtifactHandlers(client.socket, org.orgId);
191
202
  // Register branch handlers (Story 5c.1)
192
203
  registerBranchHandlers(client.socket, org.orgId);
193
- // Register agent dispatch handler (Story 6.3, queue added Story 6.5)
194
- const drainCtx = registerDispatchHandler(client.socket, org.orgId, {
204
+ // Single shared spawner context for all handlers of this org connection.
205
+ // Server-pushed config updates (agentType) mutate it so already-registered
206
+ // handlers pick up the change without re-registering.
207
+ const spawnerCtx = {
195
208
  restClient,
196
209
  socketClient: client,
197
210
  agentType: ctx.agentType,
198
211
  agentSlots: ctx.agentSlots,
199
212
  queue: dispatchQueue,
200
- });
213
+ };
214
+ // Register agent dispatch handler (Story 6.3, queue added Story 6.5)
215
+ const drainCtx = registerDispatchHandler(client.socket, org.orgId, spawnerCtx);
201
216
  // Register agent resume handler (Story 6.4, queue added Story 6.5)
202
- registerResumeHandler(client.socket, org.orgId, {
203
- restClient,
204
- socketClient: client,
205
- agentType: ctx.agentType,
206
- agentSlots: ctx.agentSlots,
207
- queue: dispatchQueue,
208
- });
217
+ registerResumeHandler(client.socket, org.orgId, spawnerCtx);
209
218
  // Register workflow step dispatch handler (Story 6b-7)
210
- registerWorkflowStepDispatchHandler(client.socket, org.orgId, {
211
- restClient,
212
- socketClient: client,
213
- agentType: ctx.agentType,
214
- agentSlots: ctx.agentSlots,
215
- queue: dispatchQueue,
216
- });
219
+ registerWorkflowStepDispatchHandler(client.socket, org.orgId, spawnerCtx);
217
220
  // Register workflow agent dismiss handler (Story 6b-10)
218
- registerWorkflowAgentDismissHandler(client.socket, {
219
- restClient,
220
- socketClient: client,
221
- agentType: ctx.agentType,
222
- agentSlots: ctx.agentSlots,
223
- queue: dispatchQueue,
224
- });
225
- // Listen for server-pushed config updates (e.g., admin changed agentSlots in UI)
221
+ registerWorkflowAgentDismissHandler(client.socket, spawnerCtx);
222
+ // Listen for server-pushed config updates (e.g., admin changed agentSlots
223
+ // or the agent engine in the Cloud UI)
226
224
  client.socket.on("cli:config:updated", (payload) => {
225
+ if (payload.agentType && isAgentType(payload.agentType)) {
226
+ if (payload.agentType !== ctx.agentType) {
227
+ const previousType = ctx.agentType;
228
+ ctx.agentType = payload.agentType;
229
+ // All orgs share one engine — update every live spawner context,
230
+ // not only this org's.
231
+ spawnerCtx.agentType = payload.agentType;
232
+ applyAgentTypeToActiveConnections(payload.agentType);
233
+ // Persist so future sessions start with the new engine
234
+ setConfig({ agentType: payload.agentType });
235
+ console.log(chalk.green(`${prefix(org.orgSlug)}Agent type updated by server: ${previousType} → ${payload.agentType}`));
236
+ }
237
+ }
227
238
  if (payload.agentSlots != null && payload.agentSlots !== ctx.agentSlots) {
228
239
  const previous = ctx.agentSlots;
229
240
  ctx.agentSlots = payload.agentSlots;
@@ -272,6 +283,17 @@ export function establishConnections(ctx, append = false) {
272
283
  agentSlots: ctx.agentSlots,
273
284
  });
274
285
  console.log(chalk.green(`${prefix(org.orgSlug)}CLI registered as "${ctx.cliName}"`));
286
+ // The server is source of truth for agentType after first registration
287
+ // (a cockpit edit made while this CLI was offline must survive the
288
+ // reconnect). Adopt the returned value.
289
+ if (isAgentType(device.agentType) &&
290
+ device.agentType !== ctx.agentType) {
291
+ const previousType = ctx.agentType;
292
+ ctx.agentType = device.agentType;
293
+ applyAgentTypeToActiveConnections(device.agentType);
294
+ setConfig({ agentType: device.agentType });
295
+ console.log(chalk.green(`${prefix(org.orgSlug)}Agent type synced from server: ${previousType} → ${device.agentType}`));
296
+ }
275
297
  // Join member room so we receive workflow:step:dispatch events
276
298
  if (device.memberId) {
277
299
  client.socket.emit("join:member", device.memberId);
@@ -364,6 +386,7 @@ export function establishConnections(ctx, append = false) {
364
386
  journal,
365
387
  emitter,
366
388
  heartbeatInterval,
389
+ spawnerCtx,
367
390
  });
368
391
  }
369
392
  }
@@ -16,12 +16,24 @@ if (!ctx) {
16
16
  console.error("[daemon] Failed to resolve connection context — exiting");
17
17
  process.exit(1);
18
18
  }
19
- // Check Claude OAuth token availability
19
+ // Check agent credential availability for the configured engine
20
20
  try {
21
- const { loadClaudeToken } = await import("../vm/claude-token-manager.js");
22
- if (!(await loadClaudeToken())) {
23
- console.warn(chalk.yellow("[daemon] No Claude OAuth token stored — agents will fail to authenticate."));
24
- console.warn(chalk.yellow("[daemon] Run: forge auth setup-claude"));
21
+ if (ctx.agentType === "CODEX" || ctx.agentType === "OPENCODE") {
22
+ const { hasAgentAuth } = await import("../vm/agent-auth-manager.js");
23
+ if (!(await hasAgentAuth(ctx.agentType))) {
24
+ const setupCmd = ctx.agentType === "CODEX"
25
+ ? "forge auth setup-codex"
26
+ : "forge auth setup-opencode";
27
+ console.warn(chalk.yellow(`[daemon] No ${ctx.agentType} credentials stored — agents will fail to authenticate.`));
28
+ console.warn(chalk.yellow(`[daemon] Run: ${setupCmd}`));
29
+ }
30
+ }
31
+ else {
32
+ const { loadClaudeToken } = await import("../vm/claude-token-manager.js");
33
+ if (!(await loadClaudeToken())) {
34
+ console.warn(chalk.yellow("[daemon] No Claude OAuth token stored — agents will fail to authenticate."));
35
+ console.warn(chalk.yellow("[daemon] Run: forge auth setup-claude"));
36
+ }
25
37
  }
26
38
  }
27
39
  catch {
@@ -153,8 +165,13 @@ async function handleReRegisterCommand() {
153
165
  };
154
166
  }
155
167
  ctx = freshCtxReg;
168
+ // Propagate the (possibly changed) engine to the live spawner contexts so
169
+ // `forge config set agent-type` applies to subsequent spawns immediately,
170
+ // not only after a daemon restart.
171
+ const { applyAgentTypeToActiveConnections } = await import("./connection-manager.js");
172
+ applyAgentTypeToActiveConnections(ctx.agentType);
156
173
  // Re-register on all active connections using PATCH to explicitly update config
157
- // (PATCH includes agentSlots from the CLI config to sync with Cloud)
174
+ // (PATCH includes agentType/agentSlots from the CLI config to sync with Cloud)
158
175
  for (const conn of activeConnections.values()) {
159
176
  try {
160
177
  await conn.restClient.updateCliConfig({
@@ -1,3 +1,23 @@
1
+ /**
2
+ * Resolve the ForgeAI CLI binary to spawn for the MCP probe.
3
+ *
4
+ * This must point at the CLI entry point (dist/bin/forgeai.js), which is the
5
+ * only script that parses an `mcp` subcommand. It used to use
6
+ * `process.argv[1]`, which is correct when the probe runs from the CLI itself
7
+ * but WRONG in the case that actually matters: the probe normally runs inside
8
+ * the daemon, where argv[1] is dist/lib/daemon-entry.js. That entry point
9
+ * parses no arguments, so `daemon-entry.js mcp ...` silently started another
10
+ * daemon instead of an MCP server — the probe then timed out every time,
11
+ * `mcpHealthy` stayed false, and the Cloud skipped this CLI for dispatch
12
+ * (task-dispatch-engine skips CLIs reporting mcpHealthy === false), while each
13
+ * re-probe leaked one more detached daemon process.
14
+ *
15
+ * Resolve it from this module's own location instead: dist/mcp/ -> dist/bin/.
16
+ */
17
+ export declare function resolveForgeBinary(): {
18
+ command: string;
19
+ prefixArgs: string[];
20
+ };
1
21
  /**
2
22
  * Probe the MCP server to verify it can start and respond to JSON-RPC initialize.
3
23
  *
@@ -1,13 +1,33 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
2
4
  import chalk from "chalk";
3
5
  const PROBE_TIMEOUT_MS = 10_000;
4
6
  /**
5
- * Resolve the ForgeAI CLI binary for MCP probe.
6
- * Same logic as agent-spawner's resolveForgeBinary.
7
+ * Resolve the ForgeAI CLI binary to spawn for the MCP probe.
8
+ *
9
+ * This must point at the CLI entry point (dist/bin/forgeai.js), which is the
10
+ * only script that parses an `mcp` subcommand. It used to use
11
+ * `process.argv[1]`, which is correct when the probe runs from the CLI itself
12
+ * but WRONG in the case that actually matters: the probe normally runs inside
13
+ * the daemon, where argv[1] is dist/lib/daemon-entry.js. That entry point
14
+ * parses no arguments, so `daemon-entry.js mcp ...` silently started another
15
+ * daemon instead of an MCP server — the probe then timed out every time,
16
+ * `mcpHealthy` stayed false, and the Cloud skipped this CLI for dispatch
17
+ * (task-dispatch-engine skips CLIs reporting mcpHealthy === false), while each
18
+ * re-probe leaked one more detached daemon process.
19
+ *
20
+ * Resolve it from this module's own location instead: dist/mcp/ -> dist/bin/.
7
21
  */
8
- function resolveForgeBinary() {
22
+ export function resolveForgeBinary() {
23
+ const binPath = join(import.meta.dirname, "..", "bin", "forgeai.js");
24
+ if (existsSync(binPath)) {
25
+ return { command: process.execPath, prefixArgs: [binPath] };
26
+ }
27
+ // Fallback for unusual layouts: use argv[1] only when it really is the CLI
28
+ // entry point, never the daemon entry.
9
29
  const scriptPath = process.argv[1];
10
- if (scriptPath) {
30
+ if (scriptPath?.includes(join("bin", "forgeai"))) {
11
31
  return { command: process.execPath, prefixArgs: [scriptPath] };
12
32
  }
13
33
  return { command: "forgeai", prefixArgs: [] };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Per-engine agent profiles: everything that differs between Claude Code,
3
+ * Codex CLI and OpenCode when running headless inside a ForgeAI Docker
4
+ * session. The rest of the pipeline (connector lifecycle, MCP bridge,
5
+ * StreamBatcher, Socket.io contract, snapshot transport) is engine-agnostic.
6
+ *
7
+ * | Engine | Binary | Config file | Resume |
8
+ * |-------------|------------|------------------------------------------|--------------|
9
+ * | CLAUDE_CODE | claude | /session/workspace/.claude/mcp_config.json (via --mcp-config) | --continue |
10
+ * | CODEX | codex | /home/agent/.codex/config.toml | exec resume --last |
11
+ * | OPENCODE | opencode | /home/agent/.config/opencode/opencode.json | run --continue |
12
+ */
13
+ import type { AgentType } from "@getforgeai/protocol";
14
+ import type { AgentStreamExtractor, QuestionData, ToolActivityData, TokenUsageData } from "./stream-json-extractor.js";
15
+ export type McpBridgeConfig = {
16
+ host: string;
17
+ port: number;
18
+ token: string;
19
+ };
20
+ export type AgentConfigOptions = {
21
+ /** OpenCode model in "provider/model" form (from CLI config, optional). */
22
+ model?: string;
23
+ };
24
+ export type AgentAuthErrorPatterns = {
25
+ /** Substrings matched against stderr only (avoids stdout false positives). */
26
+ stderr: string[];
27
+ /** Substrings matched against combined stderr + stdout tail. */
28
+ combined: string[];
29
+ };
30
+ export type AgentProfile = {
31
+ type: AgentType;
32
+ /** Binary name resolved via PATH inside the container. */
33
+ binary: string;
34
+ /**
35
+ * Container-absolute path where the generated agent config file (MCP wiring
36
+ * + permissions) is installed. The file is staged on the host volume as
37
+ * /session/mcp_config.json regardless of its actual format.
38
+ */
39
+ configTargetPath: string;
40
+ /** Build the agent config file content (JSON or TOML depending on engine). */
41
+ buildConfig(mcp: McpBridgeConfig, opts?: AgentConfigOptions): string;
42
+ /** argv for a fresh non-interactive run. */
43
+ buildArgs(prompt: string): string[];
44
+ /** argv to continue the latest on-disk session in the same home dir. */
45
+ buildContinueArgs(prompt: string): string[];
46
+ /** Non-secret env vars added to every agent spawn. */
47
+ spawnEnv: Record<string, string>;
48
+ /** Container dir holding the session state snapshotted for resume. */
49
+ stateDir: string;
50
+ /** tar --exclude patterns — credentials and cache junk never leave the container. */
51
+ snapshotExcludes: string[];
52
+ authErrorPatterns: AgentAuthErrorPatterns;
53
+ /** Command the user should run to (re-)configure auth for this engine. */
54
+ authSetupHint: string;
55
+ };
56
+ /** Narrow an arbitrary config/dispatch string to a supported AgentType. */
57
+ export declare function resolveAgentType(value: string | undefined): AgentType;
58
+ export declare function getAgentProfile(agentType: string | undefined): AgentProfile;
59
+ /**
60
+ * Instantiate the stream extractor matching the agent engine.
61
+ * All three classes share the AgentStreamExtractor surface and an identical
62
+ * constructor signature, so spawn call sites stay engine-agnostic.
63
+ */
64
+ export declare function createStreamExtractor(agentType: string | undefined, onText: (delta: string) => void, onQuestion?: (questions: QuestionData[]) => void, onToolActivity?: (activity: ToolActivityData) => void, onTokenUsage?: (usage: TokenUsageData) => void, sessionId?: string): AgentStreamExtractor;
65
+ //# sourceMappingURL=agent-profiles.d.ts.map