@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.
- package/dist/cli.js +10 -1
- package/dist/commands/auth-setup-agent.d.ts +15 -0
- package/dist/commands/auth-setup-agent.js +98 -0
- package/dist/commands/config-set.js +14 -3
- package/dist/commands/connect.js +23 -7
- package/dist/config/config-manager.d.ts +2 -0
- package/dist/lib/connection-manager.d.ts +11 -0
- package/dist/lib/connection-manager.js +48 -25
- package/dist/lib/daemon-entry.js +23 -6
- package/dist/mcp/mcp-probe.d.ts +20 -0
- package/dist/mcp/mcp-probe.js +24 -4
- package/dist/orchestrator/agent-profiles.d.ts +65 -0
- package/dist/orchestrator/agent-profiles.js +236 -0
- package/dist/orchestrator/agent-spawner.d.ts +2 -0
- package/dist/orchestrator/agent-spawner.js +25 -14
- package/dist/orchestrator/billing-detect.d.ts +20 -0
- package/dist/orchestrator/billing-detect.js +120 -0
- package/dist/orchestrator/codex-jsonl-extractor.d.ts +52 -0
- package/dist/orchestrator/codex-jsonl-extractor.js +210 -0
- package/dist/orchestrator/connectors/connector.d.ts +2 -0
- package/dist/orchestrator/connectors/docker-connector.js +192 -175
- package/dist/orchestrator/opencode-jsonl-extractor.d.ts +47 -0
- package/dist/orchestrator/opencode-jsonl-extractor.js +228 -0
- package/dist/orchestrator/resume-handler.js +2 -0
- package/dist/orchestrator/stream-json-extractor.d.ts +38 -10
- package/dist/orchestrator/stream-json-extractor.js +10 -105
- package/dist/vm/agent-auth-manager.d.ts +32 -0
- package/dist/vm/agent-auth-manager.js +68 -0
- package/dist/vm/docker-process-proxy.d.ts +2 -1
- package/dist/vm/docker-process-proxy.js +17 -10
- package/dist/vm/image-manager.js +1 -1
- package/dist/vm/secure-store.d.ts +24 -0
- package/dist/vm/secure-store.js +110 -0
- package/dist/vm/session-manager.d.ts +33 -5
- package/dist/vm/session-manager.js +129 -19
- package/dist/vm/session-snapshot.d.ts +13 -8
- package/dist/vm/session-snapshot.js +52 -36
- package/package.json +2 -2
- 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 (!
|
|
10
|
-
console.error(chalk.red(`Invalid agent type "${value}". Allowed: ${
|
|
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}`);
|
package/dist/commands/connect.js
CHANGED
|
@@ -122,14 +122,30 @@ async function connectForeground(options) {
|
|
|
122
122
|
const ctx = resolveConnectContext(options);
|
|
123
123
|
if (!ctx)
|
|
124
124
|
return;
|
|
125
|
-
// Ensure
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
//
|
|
194
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
}
|
package/dist/lib/daemon-entry.js
CHANGED
|
@@ -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
|
|
19
|
+
// Check agent credential availability for the configured engine
|
|
20
20
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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({
|
package/dist/mcp/mcp-probe.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/mcp/mcp-probe.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|