@getforgeai/cli 0.1.0-beta.6 → 0.1.0-beta.7
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 +8 -3
- package/dist/commands/config-set.d.ts +1 -1
- package/dist/commands/config-set.js +25 -4
- package/dist/commands/models-list.d.ts +7 -0
- package/dist/commands/models-list.js +57 -0
- package/dist/lib/connection-manager.js +10 -0
- package/dist/lib/model-catalog.d.ts +34 -0
- package/dist/lib/model-catalog.js +210 -0
- package/dist/vm/agent-auth-manager.d.ts +8 -0
- package/dist/vm/agent-auth-manager.js +8 -0
- package/dist/vm/session-manager.js +5 -3
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ 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
23
|
import { setupCodexAuthAction, setupKiloAuthAction, setupOpenCodeAuthAction, } from "./commands/auth-setup-agent.js";
|
|
24
|
+
import { modelsListAction } from "./commands/models-list.js";
|
|
24
25
|
const program = new Command();
|
|
25
26
|
program
|
|
26
27
|
.name("forge")
|
|
@@ -119,6 +120,10 @@ journalCmd
|
|
|
119
120
|
.command("status")
|
|
120
121
|
.description("Show sync status: last sync time, pending events, cursor")
|
|
121
122
|
.action(() => journalStatusAction());
|
|
123
|
+
program
|
|
124
|
+
.command("models [engine]")
|
|
125
|
+
.description("Show the models declared on this device and the models available for an engine (defaults to the configured agent type)")
|
|
126
|
+
.action((engine) => modelsListAction(engine));
|
|
122
127
|
const configCmd = program
|
|
123
128
|
.command("config")
|
|
124
129
|
.description("Manage CLI configuration");
|
|
@@ -130,9 +135,9 @@ configSetCmd
|
|
|
130
135
|
.description("Set agent type (CLAUDE_CODE, CODEX, OPENCODE or KILO)")
|
|
131
136
|
.action((value) => configSetAgentTypeAction(value));
|
|
132
137
|
configSetCmd
|
|
133
|
-
.command("models <
|
|
134
|
-
.description("Declare the
|
|
135
|
-
.action((
|
|
138
|
+
.command("models <engineOrModels> [models]")
|
|
139
|
+
.description("Declare the usable models, comma-separated (first = default); 'none' clears. Engine optional — defaults to the configured agent type")
|
|
140
|
+
.action((engineOrModels, models) => configSetModelsAction(engineOrModels, models));
|
|
136
141
|
configSetCmd
|
|
137
142
|
.command("agent-slots <value>")
|
|
138
143
|
.description("Set agent slots (1-20)")
|
|
@@ -4,7 +4,7 @@ export declare function configSetAgentTypeAction(value: string): Promise<void>;
|
|
|
4
4
|
* Declares the models usable on this device for an engine (first = default).
|
|
5
5
|
* "none" clears the engine's list. Refused when the org locked the list.
|
|
6
6
|
*/
|
|
7
|
-
export declare function configSetModelsAction(
|
|
7
|
+
export declare function configSetModelsAction(engineOrModels: string, maybeModels?: string): Promise<void>;
|
|
8
8
|
export declare function configSetAgentSlotsAction(value: string): Promise<void>;
|
|
9
9
|
export declare function configSetOpenerAction(value: string): void;
|
|
10
10
|
export declare function configGetAction(): void;
|
|
@@ -29,14 +29,35 @@ export async function configSetAgentTypeAction(value) {
|
|
|
29
29
|
* Declares the models usable on this device for an engine (first = default).
|
|
30
30
|
* "none" clears the engine's list. Refused when the org locked the list.
|
|
31
31
|
*/
|
|
32
|
-
export async function configSetModelsAction(
|
|
33
|
-
const
|
|
32
|
+
export async function configSetModelsAction(engineOrModels, maybeModels) {
|
|
33
|
+
const config = getConfig();
|
|
34
|
+
// Engine is optional: `forge config set models sonnet,haiku` applies to the
|
|
35
|
+
// configured agent type; an explicit engine targets another engine's list.
|
|
36
|
+
let upper;
|
|
37
|
+
let value;
|
|
38
|
+
if (maybeModels === undefined) {
|
|
39
|
+
if (isAgentType(engineOrModels.toUpperCase())) {
|
|
40
|
+
console.error(chalk.red(`Missing model list. Usage: forge config set models ${engineOrModels.toUpperCase()} <m1,m2,...>`));
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
upper = (config.agentType ?? "CLAUDE_CODE").toUpperCase();
|
|
45
|
+
value = engineOrModels;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
upper = engineOrModels.toUpperCase();
|
|
49
|
+
value = maybeModels;
|
|
50
|
+
if (!isAgentType(upper)) {
|
|
51
|
+
console.error(chalk.red(`Invalid engine "${engineOrModels}". Allowed: ${AGENT_TYPE.join(", ")}`));
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
34
56
|
if (!isAgentType(upper)) {
|
|
35
|
-
console.error(chalk.red(`
|
|
57
|
+
console.error(chalk.red(`Configured agent type "${upper}" is not a valid engine.`));
|
|
36
58
|
process.exitCode = 1;
|
|
37
59
|
return;
|
|
38
60
|
}
|
|
39
|
-
const config = getConfig();
|
|
40
61
|
const lockedByOrgs = config.modelsLockedByOrgs ?? [];
|
|
41
62
|
if (lockedByOrgs.length > 0) {
|
|
42
63
|
console.error(chalk.red(`The model list is locked by organization(s): ${lockedByOrgs.join(", ")} — edit it from their cockpit (Settings → CLI Devices).`));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forge models [engine] — show the models declared on this device and the
|
|
3
|
+
* models available for the engine (live catalog for OpenCode/Kilo, curated
|
|
4
|
+
* suggestions for Claude Code/Codex).
|
|
5
|
+
*/
|
|
6
|
+
export declare function modelsListAction(engineArg?: string): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=models-list.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* forge models [engine] — show the models declared on this device and the
|
|
3
|
+
* models available for the engine (live catalog for OpenCode/Kilo, curated
|
|
4
|
+
* suggestions for Claude Code/Codex).
|
|
5
|
+
*/
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { AGENT_TYPE, KNOWN_MODEL_SUGGESTIONS, LIVE_CATALOG_ENGINES, isAgentType, } from "@getforgeai/protocol";
|
|
8
|
+
import { getConfig } from "../config/config-manager.js";
|
|
9
|
+
import { fetchEngineModelCatalog } from "../lib/model-catalog.js";
|
|
10
|
+
import { resolveAgentType } from "../orchestrator/agent-profiles.js";
|
|
11
|
+
export async function modelsListAction(engineArg) {
|
|
12
|
+
const config = getConfig();
|
|
13
|
+
const engine = engineArg
|
|
14
|
+
? engineArg.toUpperCase()
|
|
15
|
+
: resolveAgentType(config.agentType);
|
|
16
|
+
if (!isAgentType(engine)) {
|
|
17
|
+
console.error(chalk.red(`Invalid engine "${engineArg}". Allowed: ${AGENT_TYPE.join(", ")}`));
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const declared = config.availableModels?.[engine] ?? [];
|
|
22
|
+
const lockedBy = config.modelsLockedByOrgs ?? [];
|
|
23
|
+
const locked = lockedBy.length > 0;
|
|
24
|
+
const lockNote = locked
|
|
25
|
+
? chalk.yellow(` (locked by: ${lockedBy.join(", ")})`)
|
|
26
|
+
: "";
|
|
27
|
+
console.log(chalk.bold(`Models — ${engine}`));
|
|
28
|
+
if (declared.length > 0) {
|
|
29
|
+
console.log(` Declared on this device${lockNote}:`);
|
|
30
|
+
declared.forEach((m, i) => {
|
|
31
|
+
console.log(` ${m}${i === 0 ? chalk.dim(" (default)") : ""}`);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.log(chalk.dim(" No models declared on this device — any demanded model makes it ineligible only if a list exists; declare one to advertise what your subscription can run.") + lockNote);
|
|
36
|
+
}
|
|
37
|
+
const live = LIVE_CATALOG_ENGINES.includes(engine);
|
|
38
|
+
console.log(live
|
|
39
|
+
? chalk.dim(` Listing available models via the ${engine} engine...`)
|
|
40
|
+
: chalk.dim(" Known models (curated — no headless listing for this engine):"));
|
|
41
|
+
const catalog = await fetchEngineModelCatalog(engine);
|
|
42
|
+
if (catalog.error) {
|
|
43
|
+
console.error(chalk.yellow(` ${catalog.error}`));
|
|
44
|
+
}
|
|
45
|
+
if (catalog.models.length > 0) {
|
|
46
|
+
const suggestions = KNOWN_MODEL_SUGGESTIONS[engine];
|
|
47
|
+
for (const model of catalog.models) {
|
|
48
|
+
const note = suggestions?.find((s) => s.id === model)?.note;
|
|
49
|
+
const mark = declared.includes(model) ? chalk.green("✓ ") : " ";
|
|
50
|
+
console.log(` ${mark}${model}${note ? chalk.dim(` — ${note}`) : ""}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
console.log(chalk.dim(locked
|
|
54
|
+
? "\nModel lists on this device are locked by your organization — manage them from the cockpit."
|
|
55
|
+
: `\nDeclare with: forge config set models ${engine} <model1,model2,...> (first = default)`));
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=models-list.js.map
|
|
@@ -219,6 +219,16 @@ export function establishConnections(ctx, append = false) {
|
|
|
219
219
|
registerWorkflowStepDispatchHandler(client.socket, org.orgId, spawnerCtx);
|
|
220
220
|
// Register workflow agent dismiss handler (Story 6b-10)
|
|
221
221
|
registerWorkflowAgentDismissHandler(client.socket, spawnerCtx);
|
|
222
|
+
// Cockpit-initiated model catalog listing ("Fetch from device"): run the
|
|
223
|
+
// engine's own listing (OpenCode/Kilo) or answer curated suggestions.
|
|
224
|
+
client.socket.on("models:catalog:request", (payload, callback) => {
|
|
225
|
+
void (async () => {
|
|
226
|
+
const { answerCatalogRequest } = await import("./model-catalog.js");
|
|
227
|
+
callback(await answerCatalogRequest(payload));
|
|
228
|
+
})().catch(() => {
|
|
229
|
+
callback({ models: [], error: "Catalog fetch failed" });
|
|
230
|
+
});
|
|
231
|
+
});
|
|
222
232
|
// Listen for server-pushed config updates (e.g., admin changed agentSlots
|
|
223
233
|
// or the agent engine in the Cloud UI)
|
|
224
234
|
client.socket.on("cli:config:updated", (payload) => {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model catalog discovery per engine.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode and Kilo can list their real catalogs (`opencode models`,
|
|
5
|
+
* `kilo models`) — we run that inside the agent Docker image with the stored
|
|
6
|
+
* credentials, in a throwaway container. Claude Code and Codex have no
|
|
7
|
+
* headless listing; they answer from the curated KNOWN_MODEL_SUGGESTIONS.
|
|
8
|
+
*
|
|
9
|
+
* Used by `forge models` and by the cockpit's "Fetch from device" button
|
|
10
|
+
* (models:catalog:request socket event, answered via answerCatalogRequest).
|
|
11
|
+
*/
|
|
12
|
+
import type { AgentType } from "@getforgeai/protocol";
|
|
13
|
+
export type ModelCatalogResult = {
|
|
14
|
+
models: string[];
|
|
15
|
+
/** "curated" = static suggestions; "live" = listed by the engine itself. */
|
|
16
|
+
source: "curated" | "live";
|
|
17
|
+
error?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Strip ANSI escapes and keep plausible model-id lines. Exported for tests. */
|
|
20
|
+
export declare function parseCatalogOutput(raw: string): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Fetch the model catalog for an engine: live from the engine when it
|
|
23
|
+
* supports listing, curated suggestions otherwise.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fetchEngineModelCatalog(engine: AgentType): Promise<ModelCatalogResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Answer a cockpit models:catalog:request payload. Never throws — every
|
|
28
|
+
* failure maps to a resolved value so the socket ack callback always fires.
|
|
29
|
+
*/
|
|
30
|
+
export declare function answerCatalogRequest(payload: unknown): Promise<{
|
|
31
|
+
models: string[];
|
|
32
|
+
error?: string;
|
|
33
|
+
}>;
|
|
34
|
+
//# sourceMappingURL=model-catalog.d.ts.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model catalog discovery per engine.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode and Kilo can list their real catalogs (`opencode models`,
|
|
5
|
+
* `kilo models`) — we run that inside the agent Docker image with the stored
|
|
6
|
+
* credentials, in a throwaway container. Claude Code and Codex have no
|
|
7
|
+
* headless listing; they answer from the curated KNOWN_MODEL_SUGGESTIONS.
|
|
8
|
+
*
|
|
9
|
+
* Used by `forge models` and by the cockpit's "Fetch from device" button
|
|
10
|
+
* (models:catalog:request socket event, answered via answerCatalogRequest).
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { KNOWN_MODEL_SUGGESTIONS, LIVE_CATALOG_ENGINES, isAgentType, } from "@getforgeai/protocol";
|
|
15
|
+
import { getAgentProfile } from "../orchestrator/agent-profiles.js";
|
|
16
|
+
import { ensureImage, FORGEAI_IMAGE } from "../vm/image-manager.js";
|
|
17
|
+
import { AGENT_API_KEY_DEFAULT_ENV, AGENT_AUTH_JSON_PATHS, loadAgentAuth, } from "../vm/agent-auth-manager.js";
|
|
18
|
+
const CATALOG_TIMEOUT_MS = 45_000;
|
|
19
|
+
/** Strip ANSI escapes and keep plausible model-id lines. Exported for tests. */
|
|
20
|
+
export function parseCatalogOutput(raw) {
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const models = [];
|
|
23
|
+
for (const line of raw.split("\n")) {
|
|
24
|
+
// eslint-disable-next-line no-control-regex
|
|
25
|
+
const clean = line.replaceAll(/\x1b\[[0-9;]*m/g, "").trim();
|
|
26
|
+
// Model ids: single token containing a provider separator, no spaces
|
|
27
|
+
if (!clean || clean.includes(" ") || !clean.includes("/"))
|
|
28
|
+
continue;
|
|
29
|
+
if (!/^[\w.:@/-]+$/.test(clean))
|
|
30
|
+
continue;
|
|
31
|
+
if (clean.length > 120 || seen.has(clean))
|
|
32
|
+
continue;
|
|
33
|
+
seen.add(clean);
|
|
34
|
+
models.push(clean);
|
|
35
|
+
}
|
|
36
|
+
return models;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Best-effort teardown of the throwaway container. Killing the docker CLI
|
|
40
|
+
* client only detaches it — the container (holding credentials in env) keeps
|
|
41
|
+
* running and `--rm` never fires, so it must be removed by name.
|
|
42
|
+
*/
|
|
43
|
+
function forceRemoveContainer(name) {
|
|
44
|
+
const rm = spawn("docker", ["rm", "-f", name], { stdio: "ignore" });
|
|
45
|
+
rm.on("error", () => { });
|
|
46
|
+
rm.unref();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Run the engine's own catalog command in a throwaway agent container.
|
|
50
|
+
* Credentials travel as name-only `-e` env vars (values stay out of argv,
|
|
51
|
+
* same pattern as docker-executor's secretEnv).
|
|
52
|
+
*/
|
|
53
|
+
async function runLiveCatalog(engine) {
|
|
54
|
+
const auth = await loadAgentAuth(engine);
|
|
55
|
+
if (!auth) {
|
|
56
|
+
return {
|
|
57
|
+
models: [],
|
|
58
|
+
source: "live",
|
|
59
|
+
error: `No ${engine} credentials stored — run forge auth setup-${engine.toLowerCase()} first`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const profile = getAgentProfile(engine);
|
|
63
|
+
const binary = profile.binary;
|
|
64
|
+
const containerEnv = { ...profile.spawnEnv };
|
|
65
|
+
let setup = "";
|
|
66
|
+
if (auth.kind === "api_key") {
|
|
67
|
+
const envVar = auth.envVar ?? AGENT_API_KEY_DEFAULT_ENV[engine];
|
|
68
|
+
containerEnv[envVar] = auth.value;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const target = AGENT_AUTH_JSON_PATHS[engine];
|
|
72
|
+
const dir = target.slice(0, target.lastIndexOf("/"));
|
|
73
|
+
containerEnv.FORGE_AGENT_SECRET = auth.value;
|
|
74
|
+
setup = `mkdir -p ${dir} && printenv FORGE_AGENT_SECRET > ${target} && chmod 600 ${target} && `;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await ensureImage();
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return {
|
|
81
|
+
models: [],
|
|
82
|
+
source: "live",
|
|
83
|
+
error: `Agent image unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const containerName = `forge-catalog-${randomBytes(6).toString("hex")}`;
|
|
87
|
+
const script = `${setup}${binary} models`;
|
|
88
|
+
const args = [
|
|
89
|
+
"run",
|
|
90
|
+
"--rm",
|
|
91
|
+
"--name",
|
|
92
|
+
containerName,
|
|
93
|
+
"--label",
|
|
94
|
+
"forgeai.managed=true",
|
|
95
|
+
"--platform",
|
|
96
|
+
"linux/arm64",
|
|
97
|
+
...Object.keys(containerEnv).flatMap((key) => ["-e", key]),
|
|
98
|
+
FORGEAI_IMAGE,
|
|
99
|
+
"bash",
|
|
100
|
+
"-c",
|
|
101
|
+
script,
|
|
102
|
+
];
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const child = spawn("docker", args, {
|
|
105
|
+
env: { ...process.env, ...containerEnv },
|
|
106
|
+
});
|
|
107
|
+
let stdout = "";
|
|
108
|
+
let stderr = "";
|
|
109
|
+
let timedOut = false;
|
|
110
|
+
const timer = setTimeout(() => {
|
|
111
|
+
timedOut = true;
|
|
112
|
+
child.kill("SIGKILL");
|
|
113
|
+
forceRemoveContainer(containerName);
|
|
114
|
+
resolve({
|
|
115
|
+
models: [],
|
|
116
|
+
source: "live",
|
|
117
|
+
error: `Catalog listing timed out (${CATALOG_TIMEOUT_MS / 1000}s)`,
|
|
118
|
+
});
|
|
119
|
+
}, CATALOG_TIMEOUT_MS);
|
|
120
|
+
child.stdout.on("data", (d) => {
|
|
121
|
+
stdout += d.toString();
|
|
122
|
+
});
|
|
123
|
+
child.stderr.on("data", (d) => {
|
|
124
|
+
stderr += d.toString();
|
|
125
|
+
});
|
|
126
|
+
child.on("error", (err) => {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
resolve({
|
|
129
|
+
models: [],
|
|
130
|
+
source: "live",
|
|
131
|
+
error: err.code === "ENOENT"
|
|
132
|
+
? "Docker is not installed or not running on this device"
|
|
133
|
+
: err.message,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
child.on("exit", (code) => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
if (timedOut) {
|
|
139
|
+
forceRemoveContainer(containerName);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const models = parseCatalogOutput(stdout);
|
|
143
|
+
if (models.length > 0) {
|
|
144
|
+
resolve({ models, source: "live" });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const cause = stderr.trim().slice(0, 200);
|
|
148
|
+
let error;
|
|
149
|
+
if (code === 0) {
|
|
150
|
+
error = "The engine returned no parsable model ids";
|
|
151
|
+
}
|
|
152
|
+
else if (code === 125) {
|
|
153
|
+
error = cause ? `Docker error: ${cause}` : "Docker error (exit 125)";
|
|
154
|
+
}
|
|
155
|
+
else if (code === 127) {
|
|
156
|
+
error = `${binary} not found in the agent image — pull the latest getforgeai/agent image`;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
error = `${binary} models exited with code ${code}${cause ? `: ${cause}` : ""}`;
|
|
160
|
+
}
|
|
161
|
+
resolve({ models: [], source: "live", error });
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* One catalog container per engine at a time: concurrent requests (cockpit
|
|
167
|
+
* spam, `forge models` racing a socket request) share the in-flight run.
|
|
168
|
+
*/
|
|
169
|
+
const inFlight = new Map();
|
|
170
|
+
/**
|
|
171
|
+
* Fetch the model catalog for an engine: live from the engine when it
|
|
172
|
+
* supports listing, curated suggestions otherwise.
|
|
173
|
+
*/
|
|
174
|
+
export async function fetchEngineModelCatalog(engine) {
|
|
175
|
+
if (LIVE_CATALOG_ENGINES.includes(engine)) {
|
|
176
|
+
const running = inFlight.get(engine);
|
|
177
|
+
if (running)
|
|
178
|
+
return running;
|
|
179
|
+
const run = runLiveCatalog(engine).finally(() => {
|
|
180
|
+
inFlight.delete(engine);
|
|
181
|
+
});
|
|
182
|
+
inFlight.set(engine, run);
|
|
183
|
+
return run;
|
|
184
|
+
}
|
|
185
|
+
const suggestions = KNOWN_MODEL_SUGGESTIONS[engine] ?? [];
|
|
186
|
+
return { models: suggestions.map((s) => s.id), source: "curated" };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Answer a cockpit models:catalog:request payload. Never throws — every
|
|
190
|
+
* failure maps to a resolved value so the socket ack callback always fires.
|
|
191
|
+
*/
|
|
192
|
+
export async function answerCatalogRequest(payload) {
|
|
193
|
+
try {
|
|
194
|
+
const engine = payload && typeof payload === "object"
|
|
195
|
+
? payload.engine
|
|
196
|
+
: undefined;
|
|
197
|
+
if (typeof engine !== "string" || !isAgentType(engine)) {
|
|
198
|
+
return { models: [], error: "Unknown engine" };
|
|
199
|
+
}
|
|
200
|
+
const result = await fetchEngineModelCatalog(engine);
|
|
201
|
+
return { models: result.models, error: result.error };
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
return {
|
|
205
|
+
models: [],
|
|
206
|
+
error: err instanceof Error ? err.message : "Catalog fetch failed",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=model-catalog.js.map
|
|
@@ -24,6 +24,14 @@ export type AgentAuthPayload = {
|
|
|
24
24
|
};
|
|
25
25
|
/** Container path where each engine expects its auth.json. */
|
|
26
26
|
export declare const AGENT_AUTH_JSON_PATHS: Record<AuthAgent, string>;
|
|
27
|
+
/**
|
|
28
|
+
* Default provider env var when an api_key payload carries no explicit envVar.
|
|
29
|
+
* (Codex api keys go through `codex login --with-api-key`, never an env var.)
|
|
30
|
+
*/
|
|
31
|
+
export declare const AGENT_API_KEY_DEFAULT_ENV: {
|
|
32
|
+
readonly OPENCODE: "ANTHROPIC_API_KEY";
|
|
33
|
+
readonly KILO: "KILO_API_KEY";
|
|
34
|
+
};
|
|
27
35
|
/** Host path each engine's own CLI stores its auth.json at (for import). */
|
|
28
36
|
export declare function getHostAuthJsonPath(agent: AuthAgent): string;
|
|
29
37
|
export declare function saveAgentAuth(agent: AuthAgent, payload: AgentAuthPayload): Promise<void>;
|
|
@@ -43,6 +43,14 @@ export const AGENT_AUTH_JSON_PATHS = {
|
|
|
43
43
|
OPENCODE: "/home/agent/.local/share/opencode/auth.json",
|
|
44
44
|
KILO: "/home/agent/.local/share/kilo/auth.json",
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* Default provider env var when an api_key payload carries no explicit envVar.
|
|
48
|
+
* (Codex api keys go through `codex login --with-api-key`, never an env var.)
|
|
49
|
+
*/
|
|
50
|
+
export const AGENT_API_KEY_DEFAULT_ENV = {
|
|
51
|
+
OPENCODE: "ANTHROPIC_API_KEY",
|
|
52
|
+
KILO: "KILO_API_KEY",
|
|
53
|
+
};
|
|
46
54
|
/** Host path each engine's own CLI stores its auth.json at (for import). */
|
|
47
55
|
export function getHostAuthJsonPath(agent) {
|
|
48
56
|
switch (agent) {
|
|
@@ -24,7 +24,7 @@ import { gitCredentialConfigArgs, gitCredentialSecretEnv, } from "./git-credenti
|
|
|
24
24
|
import { ensureImage, FORGEAI_IMAGE } from "./image-manager.js";
|
|
25
25
|
import { vmLog } from "./log.js";
|
|
26
26
|
import { ensureClaudeToken } from "./claude-token-manager.js";
|
|
27
|
-
import { AGENT_AUTH_JSON_PATHS, loadAgentAuth, } from "./agent-auth-manager.js";
|
|
27
|
+
import { AGENT_API_KEY_DEFAULT_ENV, AGENT_AUTH_JSON_PATHS, loadAgentAuth, } from "./agent-auth-manager.js";
|
|
28
28
|
import { getAgentProfile, resolveAgentType, } from "../orchestrator/agent-profiles.js";
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
// Paths
|
|
@@ -361,14 +361,16 @@ export class SessionManager {
|
|
|
361
361
|
if (agentType === "OPENCODE") {
|
|
362
362
|
const auth = await this.getAgentAuth("OPENCODE");
|
|
363
363
|
if (auth.kind === "api_key") {
|
|
364
|
-
return {
|
|
364
|
+
return {
|
|
365
|
+
[auth.envVar ?? AGENT_API_KEY_DEFAULT_ENV.OPENCODE]: auth.value,
|
|
366
|
+
};
|
|
365
367
|
}
|
|
366
368
|
}
|
|
367
369
|
if (agentType === "KILO") {
|
|
368
370
|
const auth = await this.getAgentAuth("KILO");
|
|
369
371
|
if (auth.kind === "api_key") {
|
|
370
372
|
// Kilo gateway key (app.kilo.ai profile) — read at request time
|
|
371
|
-
return { [auth.envVar ??
|
|
373
|
+
return { [auth.envVar ?? AGENT_API_KEY_DEFAULT_ENV.KILO]: auth.value };
|
|
372
374
|
}
|
|
373
375
|
}
|
|
374
376
|
return {};
|
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.7",
|
|
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.5"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^25.0.2",
|