@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 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 OPENCODE)")
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}`);
@@ -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" || ctx.agentType === "OPENCODE") {
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 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}`));
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;
@@ -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" || ctx.agentType === "OPENCODE") {
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 setupCmd = ctx.agentType === "CODEX"
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: ${setupCmd}`));
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
- /** OpenCode model in "provider/model" form (from CLI config, optional). */
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
- return profile.buildConfig({ host: mcpHost, port: 20000 + sessionIndex, token: mcpSecret }, { model: getConfig().opencodeModel });
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
- const prompt = params.prompt ?? buildDefaultPrompt(params.skillName);
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 ?? buildDefaultPrompt(params.skillName);
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
- return agent === "CODEX"
42
- ? join(homedir(), ".codex", "auth.json")
43
- : join(homedir(), ".local", "share", "opencode", "auth.json");
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" || resolved === "OPENCODE") {
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",
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.2"
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 via NodeSource
13
- RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | \
14
- gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
15
- echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | \
16
- tee /etc/apt/sources.list.d/nodesource.list && \
17
- apt-get update -qq && \
18
- apt-get install -y -qq nodejs && \
19
- rm -rf /var/lib/apt/lists/*
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 OpenCode (alternative engines).
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
- RUN npm install -g --loglevel=error @anthropic-ai/claude-code @openai/codex opencode-ai && \
25
- command -v claude && command -v codex && command -v opencode
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
- RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends passwd && \
35
- rm -rf /var/lib/apt/lists/* && \
36
- useradd -m -s /bin/bash -u 1001 agent
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 && \