@getforgeai/cli 0.1.0-beta.3 → 0.1.0-beta.5
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 +6 -2
- package/dist/commands/auth-setup-agent.d.ts +1 -0
- package/dist/commands/auth-setup-agent.js +30 -0
- package/dist/commands/config-set.js +4 -0
- package/dist/commands/connect.js +5 -5
- package/dist/config/config-manager.d.ts +2 -0
- package/dist/lib/daemon-entry.js +5 -5
- package/dist/orchestrator/agent-profiles.d.ts +25 -1
- package/dist/orchestrator/agent-profiles.js +100 -0
- package/dist/orchestrator/connectors/docker-connector.js +12 -5
- package/dist/vm/agent-auth-manager.d.ts +1 -1
- package/dist/vm/agent-auth-manager.js +15 -3
- package/dist/vm/session-manager.js +15 -1
- package/package.json +2 -2
- package/rootfs/Dockerfile +18 -16
package/dist/cli.js
CHANGED
|
@@ -20,7 +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
|
+
import { setupCodexAuthAction, setupKiloAuthAction, setupOpenCodeAuthAction, } from "./commands/auth-setup-agent.js";
|
|
24
24
|
const program = new Command();
|
|
25
25
|
program
|
|
26
26
|
.name("forge")
|
|
@@ -55,6 +55,10 @@ authCmd
|
|
|
55
55
|
.command("setup-opencode")
|
|
56
56
|
.description("Store OpenCode credentials (import opencode login or provider API key)")
|
|
57
57
|
.action(() => setupOpenCodeAuthAction());
|
|
58
|
+
authCmd
|
|
59
|
+
.command("setup-kilo")
|
|
60
|
+
.description("Store Kilo credentials (import kilo login or gateway API key)")
|
|
61
|
+
.action(() => setupKiloAuthAction());
|
|
58
62
|
program
|
|
59
63
|
.command("connect")
|
|
60
64
|
.description("Connect to ForgeAI Cloud (spawns background daemon by default)")
|
|
@@ -123,7 +127,7 @@ const configSetCmd = configCmd
|
|
|
123
127
|
.description("Set a configuration value");
|
|
124
128
|
configSetCmd
|
|
125
129
|
.command("agent-type <value>")
|
|
126
|
-
.description("Set agent type (CLAUDE_CODE, CODEX or
|
|
130
|
+
.description("Set agent type (CLAUDE_CODE, CODEX, OPENCODE or KILO)")
|
|
127
131
|
.action((value) => configSetAgentTypeAction(value));
|
|
128
132
|
configSetCmd
|
|
129
133
|
.command("agent-slots <value>")
|
|
@@ -11,5 +11,6 @@
|
|
|
11
11
|
* encrypted file elsewhere) and injected into agent containers at spawn.
|
|
12
12
|
*/
|
|
13
13
|
export declare function setupCodexAuthAction(): Promise<void>;
|
|
14
|
+
export declare function setupKiloAuthAction(): Promise<void>;
|
|
14
15
|
export declare function setupOpenCodeAuthAction(): Promise<void>;
|
|
15
16
|
//# sourceMappingURL=auth-setup-agent.d.ts.map
|
|
@@ -59,6 +59,36 @@ export async function setupCodexAuthAction() {
|
|
|
59
59
|
process.exit(1);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
export async function setupKiloAuthAction() {
|
|
63
|
+
try {
|
|
64
|
+
const imported = await tryImportHostAuthJson("KILO");
|
|
65
|
+
if (!imported) {
|
|
66
|
+
console.log(chalk.dim("No Kilo login found to import. You can paste a Kilo gateway API key instead\n" +
|
|
67
|
+
"(from your profile at app.kilo.ai — or run `kilo auth login` first, then\n" +
|
|
68
|
+
"re-run this command to import it)."));
|
|
69
|
+
const key = await ask("Kilo API key: ");
|
|
70
|
+
if (!key) {
|
|
71
|
+
console.error(chalk.red("No API key provided."));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
await saveAgentAuth("KILO", {
|
|
75
|
+
kind: "api_key",
|
|
76
|
+
envVar: "KILO_API_KEY",
|
|
77
|
+
value: key,
|
|
78
|
+
});
|
|
79
|
+
console.log(chalk.green("Kilo API key stored. Agents can now authenticate."));
|
|
80
|
+
}
|
|
81
|
+
const model = await ask("Kilo model (e.g. kilo/anthropic/claude-sonnet-4.5) — leave empty for Kilo's default: ");
|
|
82
|
+
if (model) {
|
|
83
|
+
setConfig({ kiloModel: model });
|
|
84
|
+
console.log(chalk.green(`Kilo model set to ${model}.`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.error(chalk.red(`Failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
62
92
|
const OPENCODE_PROVIDER_ENV_VARS = {
|
|
63
93
|
anthropic: "ANTHROPIC_API_KEY",
|
|
64
94
|
openai: "OPENAI_API_KEY",
|
|
@@ -7,6 +7,7 @@ const VALID_OPENERS = ["terminal", "vscode", "finder"];
|
|
|
7
7
|
const AGENT_AUTH_SETUP_HINTS = {
|
|
8
8
|
CODEX: "forge auth setup-codex",
|
|
9
9
|
OPENCODE: "forge auth setup-opencode",
|
|
10
|
+
KILO: "forge auth setup-kilo",
|
|
10
11
|
};
|
|
11
12
|
export async function configSetAgentTypeAction(value) {
|
|
12
13
|
const upper = value.toUpperCase();
|
|
@@ -51,6 +52,9 @@ export function configGetAction() {
|
|
|
51
52
|
if (config.agentType === "OPENCODE") {
|
|
52
53
|
console.log(` OpenCode model: ${config.opencodeModel ?? "(OpenCode default)"}`);
|
|
53
54
|
}
|
|
55
|
+
if (config.agentType === "KILO") {
|
|
56
|
+
console.log(` Kilo model: ${config.kiloModel ?? "(Kilo default)"}`);
|
|
57
|
+
}
|
|
54
58
|
console.log(` Agent slots: ${config.agentSlots ?? 5}`);
|
|
55
59
|
console.log(` Opener: ${config.opener ?? "terminal"}`);
|
|
56
60
|
console.log(` Server URL: ${config.serverUrl}`);
|
package/dist/commands/connect.js
CHANGED
|
@@ -123,14 +123,14 @@ async function connectForeground(options) {
|
|
|
123
123
|
if (!ctx)
|
|
124
124
|
return;
|
|
125
125
|
// Ensure agent credentials are available for container authentication
|
|
126
|
-
if (ctx.agentType === "CODEX" ||
|
|
126
|
+
if (ctx.agentType === "CODEX" ||
|
|
127
|
+
ctx.agentType === "OPENCODE" ||
|
|
128
|
+
ctx.agentType === "KILO") {
|
|
127
129
|
try {
|
|
128
130
|
const { hasAgentAuth } = await import("../vm/agent-auth-manager.js");
|
|
129
131
|
if (!(await hasAgentAuth(ctx.agentType))) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
: "forge auth setup-opencode";
|
|
133
|
-
console.warn(chalk.yellow(`[connect] No ${ctx.agentType} credentials stored. Agents may fail to authenticate. Run: ${setupCmd}`));
|
|
132
|
+
const { getAgentProfile } = await import("../orchestrator/agent-profiles.js");
|
|
133
|
+
console.warn(chalk.yellow(`[connect] No ${ctx.agentType} credentials stored. Agents may fail to authenticate. Run: ${getAgentProfile(ctx.agentType).authSetupHint}`));
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
catch {
|
|
@@ -13,6 +13,8 @@ export type CliConfig = {
|
|
|
13
13
|
agentType?: string;
|
|
14
14
|
/** OpenCode model in "provider/model" form (used when agentType is OPENCODE). */
|
|
15
15
|
opencodeModel?: string;
|
|
16
|
+
/** Kilo model, e.g. "kilo/anthropic/claude-sonnet-4.5" (used when agentType is KILO). */
|
|
17
|
+
kiloModel?: string;
|
|
16
18
|
agentSlots?: number;
|
|
17
19
|
opener?: Opener;
|
|
18
20
|
lastSyncCursor?: string;
|
package/dist/lib/daemon-entry.js
CHANGED
|
@@ -18,14 +18,14 @@ if (!ctx) {
|
|
|
18
18
|
}
|
|
19
19
|
// Check agent credential availability for the configured engine
|
|
20
20
|
try {
|
|
21
|
-
if (ctx.agentType === "CODEX" ||
|
|
21
|
+
if (ctx.agentType === "CODEX" ||
|
|
22
|
+
ctx.agentType === "OPENCODE" ||
|
|
23
|
+
ctx.agentType === "KILO") {
|
|
22
24
|
const { hasAgentAuth } = await import("../vm/agent-auth-manager.js");
|
|
23
25
|
if (!(await hasAgentAuth(ctx.agentType))) {
|
|
24
|
-
const
|
|
25
|
-
? "forge auth setup-codex"
|
|
26
|
-
: "forge auth setup-opencode";
|
|
26
|
+
const { getAgentProfile } = await import("../orchestrator/agent-profiles.js");
|
|
27
27
|
console.warn(chalk.yellow(`[daemon] No ${ctx.agentType} credentials stored — agents will fail to authenticate.`));
|
|
28
|
-
console.warn(chalk.yellow(`[daemon] Run: ${
|
|
28
|
+
console.warn(chalk.yellow(`[daemon] Run: ${getAgentProfile(ctx.agentType).authSetupHint}`));
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
else {
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* | CLAUDE_CODE | claude | /session/workspace/.claude/mcp_config.json (via --mcp-config) | --continue |
|
|
10
10
|
* | CODEX | codex | /home/agent/.codex/config.toml | exec resume --last |
|
|
11
11
|
* | OPENCODE | opencode | /home/agent/.config/opencode/opencode.json | run --continue |
|
|
12
|
+
* | KILO | kilo | /home/agent/.config/kilo/kilo.json | run --continue |
|
|
13
|
+
*
|
|
14
|
+
* KILO (kilo.ai) is an OpenCode fork: same config shape, same JSONL event
|
|
15
|
+
* stream (so it shares OpenCodeJsonlExtractor), Kilo gateway auth on top.
|
|
12
16
|
*/
|
|
13
17
|
import type { AgentType } from "@getforgeai/protocol";
|
|
14
18
|
import type { AgentStreamExtractor, QuestionData, ToolActivityData, TokenUsageData } from "./stream-json-extractor.js";
|
|
@@ -18,7 +22,12 @@ export type McpBridgeConfig = {
|
|
|
18
22
|
token: string;
|
|
19
23
|
};
|
|
20
24
|
export type AgentConfigOptions = {
|
|
21
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Model in "provider/model" form (from CLI config, optional).
|
|
27
|
+
* OpenCode: e.g. "anthropic/claude-sonnet-4-5".
|
|
28
|
+
* Kilo: gateway models are "kilo/<vendor>/<model>", e.g.
|
|
29
|
+
* "kilo/anthropic/claude-sonnet-4.5".
|
|
30
|
+
*/
|
|
22
31
|
model?: string;
|
|
23
32
|
};
|
|
24
33
|
export type AgentAuthErrorPatterns = {
|
|
@@ -45,6 +54,12 @@ export type AgentProfile = {
|
|
|
45
54
|
buildContinueArgs(prompt: string): string[];
|
|
46
55
|
/** Non-secret env vars added to every agent spawn. */
|
|
47
56
|
spawnEnv: Record<string, string>;
|
|
57
|
+
/**
|
|
58
|
+
* Whether the engine discovers skills deposited in .claude/skills on its
|
|
59
|
+
* own (Claude Code and OpenCode do). When false, prompts must carry an
|
|
60
|
+
* explicit pointer to the SKILL.md — see appendSkillPointer().
|
|
61
|
+
*/
|
|
62
|
+
nativeSkillDiscovery: boolean;
|
|
48
63
|
/** Container dir holding the session state snapshotted for resume. */
|
|
49
64
|
stateDir: string;
|
|
50
65
|
/** tar --exclude patterns — credentials and cache junk never leave the container. */
|
|
@@ -55,6 +70,15 @@ export type AgentProfile = {
|
|
|
55
70
|
};
|
|
56
71
|
/** Narrow an arbitrary config/dispatch string to a supported AgentType. */
|
|
57
72
|
export declare function resolveAgentType(value: string | undefined): AgentType;
|
|
73
|
+
/**
|
|
74
|
+
* Make sure a cloud-provided prompt still leads the agent to the deposited
|
|
75
|
+
* skill. Cloud task/step commands (e.g. "forge_merge_back") replace the
|
|
76
|
+
* default prompt that pointed at the SKILL.md; engines with native skill
|
|
77
|
+
* discovery find the skill anyway, but Codex would otherwise never learn the
|
|
78
|
+
* skill exists. No-op when the engine discovers skills natively or when no
|
|
79
|
+
* skill was deposited.
|
|
80
|
+
*/
|
|
81
|
+
export declare function appendSkillPointer(prompt: string, profile: AgentProfile, skillName: string | undefined): string;
|
|
58
82
|
export declare function getAgentProfile(agentType: string | undefined): AgentProfile;
|
|
59
83
|
/**
|
|
60
84
|
* Instantiate the stream extractor matching the agent engine.
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* | CLAUDE_CODE | claude | /session/workspace/.claude/mcp_config.json (via --mcp-config) | --continue |
|
|
10
10
|
* | CODEX | codex | /home/agent/.codex/config.toml | exec resume --last |
|
|
11
11
|
* | OPENCODE | opencode | /home/agent/.config/opencode/opencode.json | run --continue |
|
|
12
|
+
* | KILO | kilo | /home/agent/.config/kilo/kilo.json | run --continue |
|
|
13
|
+
*
|
|
14
|
+
* KILO (kilo.ai) is an OpenCode fork: same config shape, same JSONL event
|
|
15
|
+
* stream (so it shares OpenCodeJsonlExtractor), Kilo gateway auth on top.
|
|
12
16
|
*/
|
|
13
17
|
import { DEFAULT_AGENT_TYPE, isAgentType } from "@getforgeai/protocol";
|
|
14
18
|
import { CodexJsonlExtractor } from "./codex-jsonl-extractor.js";
|
|
@@ -20,6 +24,19 @@ export function resolveAgentType(value) {
|
|
|
20
24
|
return value;
|
|
21
25
|
return DEFAULT_AGENT_TYPE;
|
|
22
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Make sure a cloud-provided prompt still leads the agent to the deposited
|
|
29
|
+
* skill. Cloud task/step commands (e.g. "forge_merge_back") replace the
|
|
30
|
+
* default prompt that pointed at the SKILL.md; engines with native skill
|
|
31
|
+
* discovery find the skill anyway, but Codex would otherwise never learn the
|
|
32
|
+
* skill exists. No-op when the engine discovers skills natively or when no
|
|
33
|
+
* skill was deposited.
|
|
34
|
+
*/
|
|
35
|
+
export function appendSkillPointer(prompt, profile, skillName) {
|
|
36
|
+
if (profile.nativeSkillDiscovery || !skillName)
|
|
37
|
+
return prompt;
|
|
38
|
+
return `${prompt}\n\nThe "${skillName}" skill at .claude/skills/${skillName}/SKILL.md describes the procedure to follow — read it before starting.`;
|
|
39
|
+
}
|
|
23
40
|
// ---------------------------------------------------------------------------
|
|
24
41
|
// Claude Code
|
|
25
42
|
// ---------------------------------------------------------------------------
|
|
@@ -60,6 +77,7 @@ const claudeProfile = {
|
|
|
60
77
|
return ["--continue", ...this.buildArgs(prompt)];
|
|
61
78
|
},
|
|
62
79
|
spawnEnv: {},
|
|
80
|
+
nativeSkillDiscovery: true,
|
|
63
81
|
stateDir: "/home/agent/.claude",
|
|
64
82
|
snapshotExcludes: [
|
|
65
83
|
"settings.json",
|
|
@@ -132,6 +150,7 @@ const codexProfile = {
|
|
|
132
150
|
];
|
|
133
151
|
},
|
|
134
152
|
spawnEnv: {},
|
|
153
|
+
nativeSkillDiscovery: false,
|
|
135
154
|
stateDir: "/home/agent/.codex",
|
|
136
155
|
snapshotExcludes: ["auth.json", "config.toml", "log"],
|
|
137
156
|
authErrorPatterns: {
|
|
@@ -192,6 +211,8 @@ const openCodeProfile = {
|
|
|
192
211
|
return ["run", "--continue", "--format", "json", prompt];
|
|
193
212
|
},
|
|
194
213
|
spawnEnv: { OPENCODE_PERMISSION: OPENCODE_PERMISSION_JSON },
|
|
214
|
+
// OpenCode reads .claude/skills natively (OPENCODE_DISABLE_CLAUDE_CODE_SKILLS opts out)
|
|
215
|
+
nativeSkillDiscovery: true,
|
|
195
216
|
stateDir: "/home/agent/.local/share/opencode",
|
|
196
217
|
snapshotExcludes: ["auth.json", "log", "bin", "cache"],
|
|
197
218
|
authErrorPatterns: {
|
|
@@ -207,12 +228,88 @@ const openCodeProfile = {
|
|
|
207
228
|
authSetupHint: "forge auth setup-opencode",
|
|
208
229
|
};
|
|
209
230
|
// ---------------------------------------------------------------------------
|
|
231
|
+
// Kilo (kilo.ai — OpenCode fork with the Kilo gateway on top)
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
/**
|
|
234
|
+
* Permission override merged over any config (KILO_PERMISSION env is merged
|
|
235
|
+
* over file config), so a repo-level kilo.json can never re-introduce "ask"
|
|
236
|
+
* prompts. Belt to the --auto flag's suspenders.
|
|
237
|
+
*/
|
|
238
|
+
const KILO_PERMISSION_JSON = JSON.stringify({
|
|
239
|
+
"*": "allow",
|
|
240
|
+
external_directory: { "**": "allow" },
|
|
241
|
+
});
|
|
242
|
+
const kiloProfile = {
|
|
243
|
+
type: "KILO",
|
|
244
|
+
binary: "kilo",
|
|
245
|
+
configTargetPath: "/home/agent/.config/kilo/kilo.json",
|
|
246
|
+
buildConfig(mcp, opts) {
|
|
247
|
+
return JSON.stringify({
|
|
248
|
+
$schema: "https://app.kilo.ai/config.json",
|
|
249
|
+
...(opts?.model ? { model: opts.model } : {}),
|
|
250
|
+
permission: {
|
|
251
|
+
"*": "allow",
|
|
252
|
+
external_directory: { "**": "allow" },
|
|
253
|
+
},
|
|
254
|
+
// Headless container hygiene: no auto-upgrade, no product telemetry
|
|
255
|
+
autoupdate: false,
|
|
256
|
+
experimental: { openTelemetry: false },
|
|
257
|
+
mcp: {
|
|
258
|
+
forgeai: {
|
|
259
|
+
type: "local",
|
|
260
|
+
command: ["node", "/session/forge-mcp-relay.mjs"],
|
|
261
|
+
environment: {
|
|
262
|
+
FORGEAI_MCP_HOST: mcp.host,
|
|
263
|
+
FORGEAI_MCP_PORT: String(mcp.port),
|
|
264
|
+
FORGEAI_MCP_TOKEN: mcp.token,
|
|
265
|
+
},
|
|
266
|
+
enabled: true,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
}, null, 2);
|
|
270
|
+
},
|
|
271
|
+
buildArgs(prompt) {
|
|
272
|
+
// --auto: auto-approve permissions (headless runs REJECT asks otherwise).
|
|
273
|
+
// Spawn must close stdin: with a non-TTY open stdin, `kilo run` blocks
|
|
274
|
+
// reading it to EOF before sending the prompt (Bun.stdin.text()).
|
|
275
|
+
return ["run", "--auto", "--format", "json", prompt];
|
|
276
|
+
},
|
|
277
|
+
buildContinueArgs(prompt) {
|
|
278
|
+
return ["run", "--continue", "--auto", "--format", "json", prompt];
|
|
279
|
+
},
|
|
280
|
+
spawnEnv: {
|
|
281
|
+
KILO_PERMISSION: KILO_PERMISSION_JSON,
|
|
282
|
+
KILO_DISABLE_AUTOUPDATE: "1",
|
|
283
|
+
KILO_TELEMETRY_LEVEL: "off",
|
|
284
|
+
},
|
|
285
|
+
// Kilo scans .claude/skills/**/SKILL.md natively (OpenCode heritage)
|
|
286
|
+
nativeSkillDiscovery: true,
|
|
287
|
+
// Sessions live in SQLite (kilo.db + -wal/-shm) keyed to the project dir
|
|
288
|
+
stateDir: "/home/agent/.local/share/kilo",
|
|
289
|
+
snapshotExcludes: ["auth.json", "mcp-auth.json", "log", "repos", "bin"],
|
|
290
|
+
authErrorPatterns: {
|
|
291
|
+
// Kilo surfaces auth failures as error events from the gateway/provider —
|
|
292
|
+
// it never prints "Not logged in" on the run path, and env-var names in
|
|
293
|
+
// the stdout tail would be tool-output false positives (ForgeAI itself
|
|
294
|
+
// injects KILO_API_KEY). Keep only API-error-shaped tokens.
|
|
295
|
+
stderr: [],
|
|
296
|
+
combined: [
|
|
297
|
+
'"authentication_error"',
|
|
298
|
+
"invalid_api_key",
|
|
299
|
+
"Incorrect API key",
|
|
300
|
+
"AuthError",
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
authSetupHint: "forge auth setup-kilo",
|
|
304
|
+
};
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
210
306
|
// Registry
|
|
211
307
|
// ---------------------------------------------------------------------------
|
|
212
308
|
const PROFILES = {
|
|
213
309
|
CLAUDE_CODE: claudeProfile,
|
|
214
310
|
CODEX: codexProfile,
|
|
215
311
|
OPENCODE: openCodeProfile,
|
|
312
|
+
KILO: kiloProfile,
|
|
216
313
|
};
|
|
217
314
|
export function getAgentProfile(agentType) {
|
|
218
315
|
return PROFILES[resolveAgentType(agentType)];
|
|
@@ -228,6 +325,9 @@ export function createStreamExtractor(agentType, onText, onQuestion, onToolActiv
|
|
|
228
325
|
case "CODEX":
|
|
229
326
|
return new CodexJsonlExtractor(onText, onQuestion, onToolActivity, onTokenUsage, sessionId);
|
|
230
327
|
case "OPENCODE":
|
|
328
|
+
// Kilo is an OpenCode fork emitting the same JSONL event stream
|
|
329
|
+
// (text/tool_use/step_start/step_finish/error with sessionID + part).
|
|
330
|
+
case "KILO":
|
|
231
331
|
return new OpenCodeJsonlExtractor(onText, onQuestion, onToolActivity, onTokenUsage, sessionId);
|
|
232
332
|
default:
|
|
233
333
|
return new StreamJsonExtractor(onText, onQuestion, onToolActivity, onTokenUsage, sessionId);
|
|
@@ -16,7 +16,7 @@ import { getDockerBridgeGatewayIp } from "../../vm/docker-executor.js";
|
|
|
16
16
|
import { activeConnections } from "../../lib/connection-manager.js";
|
|
17
17
|
import { vmLog } from "../../vm/log.js";
|
|
18
18
|
import { getConfig } from "../../config/config-manager.js";
|
|
19
|
-
import { getAgentProfile } from "../agent-profiles.js";
|
|
19
|
+
import { appendSkillPointer, getAgentProfile } from "../agent-profiles.js";
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// Helpers
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
@@ -40,7 +40,9 @@ async function resolveMcpRelayHost() {
|
|
|
40
40
|
* Format and install path depend on the agent engine (see agent-profiles.ts).
|
|
41
41
|
*/
|
|
42
42
|
function buildAgentConfig(profile, sessionIndex, mcpSecret, mcpHost) {
|
|
43
|
-
|
|
43
|
+
const cliConfig = getConfig();
|
|
44
|
+
const model = profile.type === "KILO" ? cliConfig.kiloModel : cliConfig.opencodeModel;
|
|
45
|
+
return profile.buildConfig({ host: mcpHost, port: 20000 + sessionIndex, token: mcpSecret }, { model });
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
46
48
|
* Bash script installing the staged config + context files into the container.
|
|
@@ -168,8 +170,11 @@ export const dockerConnector = {
|
|
|
168
170
|
await sm.materializeAgentAuth(params.taskId, profile.type);
|
|
169
171
|
// 7. Start MCP bridge (TCP server on host for forge-mcp-relay in container)
|
|
170
172
|
await startSessionMcpBridge(session, params);
|
|
171
|
-
// 8. Spawn agent
|
|
172
|
-
|
|
173
|
+
// 8. Spawn agent. Cloud-provided prompts get a pointer to the deposited
|
|
174
|
+
// skill when the engine has no native skill discovery (Codex).
|
|
175
|
+
const prompt = params.prompt
|
|
176
|
+
? appendSkillPointer(params.prompt, profile, params.skill?.name)
|
|
177
|
+
: buildDefaultPrompt(params.skillName);
|
|
173
178
|
const agentArgs = profile.buildArgs(prompt);
|
|
174
179
|
const env = {};
|
|
175
180
|
vmLog.magenta("docker-connector", `Spawning ${profile.type} agent in container ${sessionId}`, sessionId);
|
|
@@ -330,7 +335,9 @@ export const dockerConnector = {
|
|
|
330
335
|
}
|
|
331
336
|
else {
|
|
332
337
|
vmLog.warn("docker-connector", `Snapshot restore failed — falling back to prompt in ${sessionId}`, sessionId);
|
|
333
|
-
const prompt = params.prompt
|
|
338
|
+
const prompt = params.prompt
|
|
339
|
+
? appendSkillPointer(params.prompt, profile, params.skill?.name)
|
|
340
|
+
: buildDefaultPrompt(params.skillName);
|
|
334
341
|
proxy = await sm.spawnAgent(params.taskId, profile.binary, profile.buildArgs(prompt), {}, profile.type);
|
|
335
342
|
}
|
|
336
343
|
// Workflow: keep container alive for idle window
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Storage: macOS Keychain / encrypted file via secure-store.ts. The payload
|
|
14
14
|
* is stored as a JSON string.
|
|
15
15
|
*/
|
|
16
|
-
export type AuthAgent = "CODEX" | "OPENCODE";
|
|
16
|
+
export type AuthAgent = "CODEX" | "OPENCODE" | "KILO";
|
|
17
17
|
export type AgentAuthPayload = {
|
|
18
18
|
kind: "api_key";
|
|
19
19
|
value: string;
|
|
@@ -30,17 +30,29 @@ const STORE_LOCATIONS = {
|
|
|
30
30
|
fileName: "opencode-auth.json",
|
|
31
31
|
fileSalt: "forgeai-opencode-auth-salt",
|
|
32
32
|
},
|
|
33
|
+
KILO: {
|
|
34
|
+
keychainService: "ForgeAI-kilo-auth",
|
|
35
|
+
keychainAccount: "kilo-auth",
|
|
36
|
+
fileName: "kilo-auth.json",
|
|
37
|
+
fileSalt: "forgeai-kilo-auth-salt",
|
|
38
|
+
},
|
|
33
39
|
};
|
|
34
40
|
/** Container path where each engine expects its auth.json. */
|
|
35
41
|
export const AGENT_AUTH_JSON_PATHS = {
|
|
36
42
|
CODEX: "/home/agent/.codex/auth.json",
|
|
37
43
|
OPENCODE: "/home/agent/.local/share/opencode/auth.json",
|
|
44
|
+
KILO: "/home/agent/.local/share/kilo/auth.json",
|
|
38
45
|
};
|
|
39
46
|
/** Host path each engine's own CLI stores its auth.json at (for import). */
|
|
40
47
|
export function getHostAuthJsonPath(agent) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
switch (agent) {
|
|
49
|
+
case "CODEX":
|
|
50
|
+
return join(homedir(), ".codex", "auth.json");
|
|
51
|
+
case "OPENCODE":
|
|
52
|
+
return join(homedir(), ".local", "share", "opencode", "auth.json");
|
|
53
|
+
case "KILO":
|
|
54
|
+
return join(homedir(), ".local", "share", "kilo", "auth.json");
|
|
55
|
+
}
|
|
44
56
|
}
|
|
45
57
|
export async function saveAgentAuth(agent, payload) {
|
|
46
58
|
await saveSecret(STORE_LOCATIONS[agent], JSON.stringify(payload));
|
|
@@ -318,6 +318,11 @@ export class SessionManager {
|
|
|
318
318
|
secretEnv: await this.getAgentSecretEnv(resolveAgentType(agentType)),
|
|
319
319
|
user: "agent",
|
|
320
320
|
});
|
|
321
|
+
// Agents run one-shot and never receive stdin input. Close it so engines
|
|
322
|
+
// that read piped stdin to EOF before acting (OpenCode and Kilo both
|
|
323
|
+
// concatenate non-TTY stdin into the prompt via Bun.stdin.text()) don't
|
|
324
|
+
// block forever on the open docker-exec pipe.
|
|
325
|
+
dockerExecProcess.stdin?.end();
|
|
321
326
|
// Create a kill function that removes the container when the process exits
|
|
322
327
|
const killFn = async (signal) => {
|
|
323
328
|
try {
|
|
@@ -359,6 +364,13 @@ export class SessionManager {
|
|
|
359
364
|
return { [auth.envVar ?? "ANTHROPIC_API_KEY"]: auth.value };
|
|
360
365
|
}
|
|
361
366
|
}
|
|
367
|
+
if (agentType === "KILO") {
|
|
368
|
+
const auth = await this.getAgentAuth("KILO");
|
|
369
|
+
if (auth.kind === "api_key") {
|
|
370
|
+
// Kilo gateway key (app.kilo.ai profile) — read at request time
|
|
371
|
+
return { [auth.envVar ?? "KILO_API_KEY"]: auth.value };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
362
374
|
return {};
|
|
363
375
|
}
|
|
364
376
|
/** Load (and cache) stored Codex/OpenCode credentials, or throw with a fix hint. */
|
|
@@ -494,7 +506,9 @@ export class SessionManager {
|
|
|
494
506
|
*/
|
|
495
507
|
async refreshToken(agentType = "CLAUDE_CODE") {
|
|
496
508
|
const resolved = resolveAgentType(agentType);
|
|
497
|
-
if (resolved === "CODEX" ||
|
|
509
|
+
if (resolved === "CODEX" ||
|
|
510
|
+
resolved === "OPENCODE" ||
|
|
511
|
+
resolved === "KILO") {
|
|
498
512
|
const previous = this.agentAuthCache[resolved];
|
|
499
513
|
delete this.agentAuthCache[resolved];
|
|
500
514
|
const fresh = await loadAgentAuth(resolved);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getforgeai/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.5",
|
|
4
4
|
"description": "ForgeAI CLI — runs AI coding agents in local Docker containers with your own AI tokens, and connects them to the ForgeAI cockpit.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"ora": "^8.2.0",
|
|
48
48
|
"socket.io-client": "^4.8.3",
|
|
49
49
|
"zod": "^4.3.6",
|
|
50
|
-
"@getforgeai/protocol": "0.1.0-beta.
|
|
50
|
+
"@getforgeai/protocol": "0.1.0-beta.3"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^25.0.2",
|
package/rootfs/Dockerfile
CHANGED
|
@@ -6,23 +6,25 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|
|
6
6
|
RUN apt-get update -qq && \
|
|
7
7
|
apt-get install -y -qq --no-install-recommends \
|
|
8
8
|
ca-certificates curl git openssh-client build-essential \
|
|
9
|
-
python3 procps less sudo gnupg && \
|
|
9
|
+
python3 procps less sudo gnupg passwd xz-utils && \
|
|
10
10
|
rm -rf /var/lib/apt/lists/*
|
|
11
11
|
|
|
12
|
-
# Node.js 20 LTS
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
# Node.js 20 LTS from the official nodejs.org tarball — pinned version, no
|
|
13
|
+
# NodeSource apt repo (which intermittently 403s and breaks builds).
|
|
14
|
+
ARG TARGETARCH=arm64
|
|
15
|
+
ARG NODE_VERSION=20.20.2
|
|
16
|
+
RUN NODE_ARCH=$([ "$TARGETARCH" = "amd64" ] && echo "x64" || echo "$TARGETARCH") && \
|
|
17
|
+
curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz" | \
|
|
18
|
+
tar -xJ -C /usr/local --strip-components=1 && \
|
|
19
|
+
node --version && npm --version
|
|
20
20
|
|
|
21
|
-
# Agent CLIs: Claude Code (primary), Codex and
|
|
21
|
+
# Agent CLIs: Claude Code (primary), Codex, OpenCode and Kilo (alternative engines).
|
|
22
22
|
# No output pipe: `| tail` would mask npm failures (pipeline exit = tail's).
|
|
23
23
|
# The `command -v` checks make a partial install fail the build loudly.
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
# @kilocode/cli ships its platform binary via optionalDependencies + postinstall,
|
|
25
|
+
# so scripts must stay enabled for it.
|
|
26
|
+
RUN npm install -g --loglevel=error @anthropic-ai/claude-code @openai/codex opencode-ai @kilocode/cli && \
|
|
27
|
+
command -v claude && command -v codex && command -v opencode && command -v kilo
|
|
26
28
|
|
|
27
29
|
# ForgeAI MCP relay (baked-in copy for reference only — at runtime the CLI
|
|
28
30
|
# stages its own copy into /session/forge-mcp-relay.mjs so the relay version
|
|
@@ -30,10 +32,10 @@ RUN npm install -g --loglevel=error @anthropic-ai/claude-code @openai/codex open
|
|
|
30
32
|
RUN mkdir -p /opt/forgeai
|
|
31
33
|
COPY forge-mcp-relay.mjs /opt/forgeai/forge-mcp-relay.mjs
|
|
32
34
|
|
|
33
|
-
# Create non-root user (Claude Code refuses --dangerously-skip-permissions as root)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
# Create non-root user (Claude Code refuses --dangerously-skip-permissions as root).
|
|
36
|
+
# passwd is installed with the base packages above: re-running apt-get update
|
|
37
|
+
# here hits the NodeSource repo again, which intermittently 403s and fails builds.
|
|
38
|
+
RUN useradd -m -s /bin/bash -u 1001 agent
|
|
37
39
|
|
|
38
40
|
# Working directories (owned by agent user)
|
|
39
41
|
RUN mkdir -p /workspace /session /shared && \
|