@cortexlabs/forge 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +33 -0
  2. package/SECURITY.md +5 -0
  3. package/dist/api-client.d.ts +17 -0
  4. package/dist/api-client.js +71 -0
  5. package/dist/api-client.js.map +1 -0
  6. package/dist/audit.d.ts +1 -0
  7. package/dist/audit.js +19 -0
  8. package/dist/audit.js.map +1 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +38 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands.d.ts +3 -0
  13. package/dist/commands.js +391 -0
  14. package/dist/commands.js.map +1 -0
  15. package/dist/config.d.ts +8 -0
  16. package/dist/config.js +81 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/credentials.d.ts +5 -0
  19. package/dist/credentials.js +72 -0
  20. package/dist/credentials.js.map +1 -0
  21. package/dist/extensions.d.ts +7 -0
  22. package/dist/extensions.js +32 -0
  23. package/dist/extensions.js.map +1 -0
  24. package/dist/interactive.d.ts +9 -0
  25. package/dist/interactive.js +59 -0
  26. package/dist/interactive.js.map +1 -0
  27. package/dist/output.d.ts +2 -0
  28. package/dist/output.js +46 -0
  29. package/dist/output.js.map +1 -0
  30. package/dist/parser.d.ts +7 -0
  31. package/dist/parser.js +65 -0
  32. package/dist/parser.js.map +1 -0
  33. package/dist/paths.d.ts +14 -0
  34. package/dist/paths.js +31 -0
  35. package/dist/paths.js.map +1 -0
  36. package/dist/permissions.d.ts +4 -0
  37. package/dist/permissions.js +16 -0
  38. package/dist/permissions.js.map +1 -0
  39. package/dist/project.d.ts +6 -0
  40. package/dist/project.js +40 -0
  41. package/dist/project.js.map +1 -0
  42. package/dist/sessions.d.ts +13 -0
  43. package/dist/sessions.js +83 -0
  44. package/dist/sessions.js.map +1 -0
  45. package/dist/storage.d.ts +5 -0
  46. package/dist/storage.js +41 -0
  47. package/dist/storage.js.map +1 -0
  48. package/dist/trust.d.ts +10 -0
  49. package/dist/trust.js +91 -0
  50. package/dist/trust.js.map +1 -0
  51. package/dist/types.d.ts +79 -0
  52. package/dist/types.js +2 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/usage.d.ts +15 -0
  55. package/dist/usage.js +40 -0
  56. package/dist/usage.js.map +1 -0
  57. package/package.json +34 -0
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # Forge CLI
2
+
3
+ Forge is a local engineering terminal backed by the Forge platform.
4
+
5
+ ## Install
6
+
7
+ The package is not public until the release checklist passes. For a packed local build:
8
+
9
+ ```bash
10
+ npm install
11
+ npm test
12
+ npm pack
13
+ npm install -g ./cortexlabs-forge-0.1.0-alpha.1.tgz
14
+ forge doctor
15
+ ```
16
+
17
+ After publication:
18
+
19
+ ```bash
20
+ npm install -g @cortexlabs/forge
21
+ ```
22
+
23
+ ## Start
24
+
25
+ ```bash
26
+ forge login
27
+ forge status
28
+ forge
29
+ ```
30
+
31
+ `forge` opens the interactive terminal. Use `/help` inside it. Use `--json` with a command for machine-readable newline-delimited events.
32
+
33
+ Forge never enables paid over-limit usage, changes spending limits, publishes, deploys, or performs destructive work without an explicit approval flow.
package/SECURITY.md ADDED
@@ -0,0 +1,5 @@
1
+ # Security
2
+
3
+ Do not report vulnerabilities in public issues. Contact the Cortex Labs security address configured in the Forge account portal. Include the CLI version, operating system, reproduction steps, and whether a workspace had been trusted.
4
+
5
+ Forge CLI never requires provider credentials. Use a scoped Forge API key. Revoke affected keys from Forge Settings if a local machine or transcript may be compromised.
@@ -0,0 +1,17 @@
1
+ import type { CliEvent, TaskMode } from "./types.js";
2
+ import type { UsageEventData } from "./usage.js";
3
+ export declare function getBootstrap(apiBaseUrl: string): Promise<Record<string, unknown>>;
4
+ export declare function getBillingStatus(apiBaseUrl: string): Promise<Record<string, unknown>>;
5
+ export declare function runRemote(params: {
6
+ apiBaseUrl: string;
7
+ prompt: string;
8
+ model: string;
9
+ mode: TaskMode;
10
+ sessionId: string;
11
+ project: Record<string, unknown>;
12
+ signal: AbortSignal;
13
+ emit(event: Omit<CliEvent, "at">): void;
14
+ }): Promise<{
15
+ text: string;
16
+ usage: UsageEventData | null;
17
+ }>;
@@ -0,0 +1,71 @@
1
+ import { getApiKey } from "./credentials.js";
2
+ async function headers() {
3
+ const apiKey = await getApiKey();
4
+ if (!apiKey)
5
+ throw new Error("Not authenticated. Run `forge login` or set FORGE_API_KEY.");
6
+ return { authorization: `Bearer ${apiKey}`, "content-type": "application/json" };
7
+ }
8
+ async function apiError(response) {
9
+ const body = await response.json().catch(() => ({}));
10
+ const diagnostic = `HTTP ${response.status}`;
11
+ if (response.status === 401)
12
+ return new Error(`Forge authentication failed (${diagnostic}). Check or revoke the API key.`);
13
+ if (response.status === 402)
14
+ return new Error(`You have reached your included Forge usage (${diagnostic}). Balance: ${body.balance ?? "unknown"}. Run forge credits.`);
15
+ if (response.status === 429)
16
+ return new Error(`Forge is temporarily rate limited (${diagnostic}). Retry after ${body.retryAfter ?? response.headers.get("retry-after") ?? "the reset"}.`);
17
+ if (response.status >= 500)
18
+ return new Error(`Forge is temporarily unavailable (${diagnostic}). Your local session remains safe.`);
19
+ return new Error(`${body.error ?? "Forge request failed"} (${diagnostic}).`);
20
+ }
21
+ export async function getBootstrap(apiBaseUrl) {
22
+ const response = await fetch(`${apiBaseUrl}/api/forge/cli/bootstrap`, { headers: await headers(), signal: AbortSignal.timeout(15_000) });
23
+ if (!response.ok)
24
+ throw await apiError(response);
25
+ return response.json();
26
+ }
27
+ export async function getBillingStatus(apiBaseUrl) {
28
+ const response = await fetch(`${apiBaseUrl}/api/forge/cli/billing`, { headers: await headers(), signal: AbortSignal.timeout(15_000) });
29
+ if (!response.ok)
30
+ throw await apiError(response);
31
+ return response.json();
32
+ }
33
+ export async function runRemote(params) {
34
+ const response = await fetch(`${params.apiBaseUrl}/api/forge/cli/run`, {
35
+ method: "POST", headers: await headers(), signal: params.signal,
36
+ body: JSON.stringify({ prompt: params.prompt, model: params.model, mode: params.mode, sessionId: params.sessionId, requestId: crypto.randomUUID(), project: params.project }),
37
+ });
38
+ if (!response.ok)
39
+ throw await apiError(response);
40
+ if (!response.body)
41
+ throw new Error("Forge API returned an empty stream.");
42
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
43
+ let pending = "";
44
+ let text = "";
45
+ let usage = null;
46
+ let streamError = null;
47
+ while (true) {
48
+ const { done, value } = await reader.read();
49
+ pending += value ?? "";
50
+ const lines = pending.split("\n");
51
+ pending = lines.pop() ?? "";
52
+ for (const line of lines) {
53
+ if (!line.trim())
54
+ continue;
55
+ const event = JSON.parse(line);
56
+ if (event.type === "text-delta")
57
+ text += event.message ?? "";
58
+ if (event.type === "usage")
59
+ usage = (event.data ?? {});
60
+ if (event.type === "error")
61
+ streamError = event.message ?? "Forge run failed.";
62
+ params.emit(event);
63
+ }
64
+ if (done)
65
+ break;
66
+ }
67
+ if (streamError)
68
+ throw new Error(streamError);
69
+ return { text, usage };
70
+ }
71
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function audit(action: string, data?: Record<string, unknown>): Promise<void>;
package/dist/audit.js ADDED
@@ -0,0 +1,19 @@
1
+ import { appendPrivateJsonLine } from "./storage.js";
2
+ import { forgePaths } from "./paths.js";
3
+ const secretPattern = /forge_[a-f0-9]{16,}/gi;
4
+ function redact(value) {
5
+ if (typeof value === "string")
6
+ return value.replace(secretPattern, "forge_[REDACTED]");
7
+ if (Array.isArray(value))
8
+ return value.map(redact);
9
+ if (value && typeof value === "object") {
10
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, /token|secret|key/i.test(key) ? "[REDACTED]" : redact(item)]));
11
+ }
12
+ return value;
13
+ }
14
+ export async function audit(action, data = {}) {
15
+ await appendPrivateJsonLine(forgePaths().audit, {
16
+ at: new Date().toISOString(), action, data: redact(data),
17
+ });
18
+ }
19
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAE9C,SAAS,MAAM,CAAC,KAAc;IAC5B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC,aAAa,EAAE,kBAAkB,CAAC,CAAC;IACvF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5I,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,MAAc,EAAE,OAAgC,EAAE;IAC5E,MAAM,qBAAqB,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE;QAC9C,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC;KACzD,CAAC,CAAC;AACL,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from "node:path";
3
+ import { createEmitter } from "./output.js";
4
+ import { interactiveLoop, requestApproval, runCommandLine } from "./interactive.js";
5
+ import { readFile } from "node:fs/promises";
6
+ import { fileURLToPath } from "node:url";
7
+ const args = process.argv.slice(2);
8
+ const outputMode = args.includes("--json") ? "json" : "human";
9
+ const filtered = args.filter((arg) => arg !== "--json");
10
+ const emit = createEmitter(outputMode);
11
+ const controller = new AbortController();
12
+ let interrupted = false;
13
+ process.on("SIGINT", () => {
14
+ if (interrupted)
15
+ process.exit(130);
16
+ interrupted = true;
17
+ controller.abort(new Error("Cancelled by user."));
18
+ emit({ type: "warning", message: "Cancelled. Press Ctrl-C again to exit immediately." });
19
+ });
20
+ async function main() {
21
+ const cwd = resolve(process.cwd());
22
+ if (filtered.includes("--help") || filtered.includes("-h"))
23
+ return runCommandLine("help", { cwd, outputMode, signal: controller.signal, emit, requestApproval });
24
+ if (filtered.includes("--version") || filtered.includes("-v")) {
25
+ const packageJson = JSON.parse(await readFile(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
26
+ process.stdout.write(`${packageJson.version}\n`);
27
+ return 0;
28
+ }
29
+ if (!filtered.length)
30
+ return interactiveLoop({ cwd, outputMode, signal: controller.signal, emit });
31
+ const input = filtered.map((arg) => JSON.stringify(arg)).join(" ");
32
+ return runCommandLine(input, { cwd, outputMode, signal: controller.signal, emit, requestApproval });
33
+ }
34
+ main().then((code) => { process.exitCode = code === 99 ? 0 : code; }).catch((error) => {
35
+ emit({ type: "error", message: error instanceof Error ? error.message : String(error) });
36
+ process.exitCode = 1;
37
+ });
38
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { CommandDefinition } from "./types.js";
2
+ export declare function resolveCommand(name: string): CommandDefinition | null;
3
+ export declare function allCommands(): CommandDefinition[];
@@ -0,0 +1,391 @@
1
+ import { audit } from "./audit.js";
2
+ import { getBillingStatus, getBootstrap, runRemote } from "./api-client.js";
3
+ import { clearApiKey, credentialSource, getApiKey, readMaskedApiKey, saveApiKey } from "./credentials.js";
4
+ import { loadConfig, loadConfigWithSources, saveConfig } from "./config.js";
5
+ import { permissionDecision } from "./permissions.js";
6
+ import { collectProjectManifest } from "./project.js";
7
+ import { clearSession, compactSession, createSession, deleteSession, forkSession, getCurrentSession, listSessions, loadSession, saveSession, sessionsOlderThan, setCurrentSession } from "./sessions.js";
8
+ import { findTrustRecord, trustWorkspace, trustWorkspaceOnce } from "./trust.js";
9
+ import { inspectExtensions } from "./extensions.js";
10
+ import { writePrivateJson } from "./storage.js";
11
+ import { join, relative } from "node:path";
12
+ import { contextSummary, contextWarning, mergeUsage } from "./usage.js";
13
+ const registry = new Map();
14
+ function define(command) {
15
+ registry.set(command.name, command);
16
+ for (const alias of command.aliases ?? [])
17
+ registry.set(alias, command);
18
+ }
19
+ function text(context, message) { context.emit({ type: "info", message }); }
20
+ define({ name: "help", aliases: ["?"], summary: "Show commands", usage: "forge help", interactive: true, async run(ctx) {
21
+ const query = ctx.args.join(" ").toLowerCase();
22
+ const commands = [...new Set(registry.values())].filter((command) => !query || `${command.name} ${command.summary}`.toLowerCase().includes(query)).sort((a, b) => a.name.localeCompare(b.name));
23
+ if (!commands.length) {
24
+ ctx.emit({ type: "warning", message: `No commands match "${query}".` });
25
+ return 1;
26
+ }
27
+ for (const command of commands)
28
+ text(ctx, `${command.name.padEnd(14)} ${command.summary}`);
29
+ return 0;
30
+ } });
31
+ define({ name: "login", summary: "Save a Forge API key", usage: "forge login", interactive: true, async run(ctx) {
32
+ const key = await readMaskedApiKey();
33
+ await saveApiKey(key);
34
+ await audit("auth.login", { source: "private-file" });
35
+ ctx.emit({ type: "success", message: "Forge API key saved in a mode-0600 local file." });
36
+ ctx.emit({ type: "warning", message: "This preview does not use the OS keychain yet. Prefer FORGE_API_KEY on shared machines." });
37
+ return 0;
38
+ } });
39
+ define({ name: "logout", summary: "Remove the saved API key", usage: "forge logout", interactive: true, async run(ctx) {
40
+ await clearApiKey();
41
+ await audit("auth.logout");
42
+ ctx.emit({ type: "success", message: "Saved Forge credentials removed." });
43
+ return 0;
44
+ } });
45
+ define({ name: "status", summary: "Show workspace and account status", usage: "forge status", interactive: true, async run(ctx) {
46
+ const config = await loadConfig();
47
+ const trust = await findTrustRecord(ctx.cwd);
48
+ const session = await getCurrentSession();
49
+ const project = await collectProjectManifest(ctx.cwd);
50
+ let account = null;
51
+ if (await getApiKey())
52
+ account = await getBootstrap(config.apiBaseUrl).catch(() => null);
53
+ text(ctx, `workspace: ${trust.current.repositoryRoot ?? trust.current.path}`);
54
+ text(ctx, `trust: ${trust.record ? "trusted" : "not trusted"}`);
55
+ text(ctx, `mode: ${config.taskMode} / permissions: ${config.permissionMode} / model: ${config.model}`);
56
+ text(ctx, `project: ${project.rootName}${project.branch ? ` @ ${project.branch}` : ""}`);
57
+ text(ctx, `session: ${session?.id ?? "none"} / state: ${session?.state ?? "idle"}`);
58
+ text(ctx, contextSummary(session?.usage));
59
+ const warning = contextWarning(session?.usage);
60
+ if (warning)
61
+ ctx.emit({ type: "warning", message: warning });
62
+ const accountPlan = account?.plan;
63
+ const accountCredits = account?.credits;
64
+ text(ctx, `plan: ${accountPlan?.name ?? "unknown"} / credits: ${accountCredits?.unlimited ? "unlimited" : accountCredits?.balance?.toLocaleString() ?? "unknown"} / session cost: ${session?.usage?.credits ?? 0}`);
65
+ text(ctx, `agents: 0 / provider: ${account ? "connected" : "disconnected"}`);
66
+ text(ctx, `credentials: ${(await getApiKey()) ? credentialSource() : "missing"}`);
67
+ return 0;
68
+ } });
69
+ define({ name: "doctor", summary: "Check local and API readiness", usage: "forge doctor", interactive: true, async run(ctx) {
70
+ const config = await loadConfig();
71
+ const checks = [
72
+ ["Node.js 20+", Number(process.versions.node.split(".")[0]) >= 20, process.versions.node],
73
+ ["API key", Boolean(await getApiKey()), (await getApiKey()) ? credentialSource() : "missing"],
74
+ ["API URL", /^https?:\/\//.test(config.apiBaseUrl), config.apiBaseUrl],
75
+ ];
76
+ for (const [name, ok, detail] of checks)
77
+ ctx.emit({ type: ok ? "success" : "warning", message: `${name}: ${detail}` });
78
+ if (await getApiKey()) {
79
+ try {
80
+ await getBootstrap(config.apiBaseUrl);
81
+ ctx.emit({ type: "success", message: "Forge API connection is healthy." });
82
+ }
83
+ catch (error) {
84
+ ctx.emit({ type: "error", message: error instanceof Error ? error.message : String(error) });
85
+ return 1;
86
+ }
87
+ }
88
+ return checks.every(([, ok]) => ok) ? 0 : 1;
89
+ } });
90
+ define({ name: "trust", summary: "Inspect or trust this workspace", usage: "forge trust [--always]", interactive: true, async run(ctx) {
91
+ const result = await findTrustRecord(ctx.cwd);
92
+ text(ctx, `path: ${result.current.path}`);
93
+ text(ctx, `instructions: ${result.current.instructionFiles.join(", ") || "none"}`);
94
+ text(ctx, `executable config: ${result.current.executableConfigFiles.join(", ") || "none"}`);
95
+ if (result.record) {
96
+ ctx.emit({ type: "success", message: "Workspace fingerprint is trusted." });
97
+ return 0;
98
+ }
99
+ if (ctx.flags.has("read-only")) {
100
+ const config = await loadConfig();
101
+ await saveConfig({ ...config, permissionMode: "read-only", taskMode: "read-only" });
102
+ ctx.emit({ type: "success", message: "Opened in read-only mode without trusting project configuration." });
103
+ return 0;
104
+ }
105
+ const approved = ctx.flags.has("always") || ctx.flags.has("once") || await ctx.requestApproval?.("Trust once? Use --always to persist, or --read-only to avoid trust.", false);
106
+ if (!approved) {
107
+ ctx.emit({ type: "warning", message: "Workspace remains untrusted." });
108
+ return 1;
109
+ }
110
+ const permanent = ctx.flags.has("always");
111
+ if (permanent)
112
+ await trustWorkspace(ctx.cwd);
113
+ else
114
+ await trustWorkspaceOnce(ctx.cwd);
115
+ await audit("workspace.trust", { path: result.current.path, scope: permanent ? "always" : "once" });
116
+ ctx.emit({ type: "success", message: `Workspace trusted ${permanent ? "permanently" : "for this process"}. Configuration metadata changes invalidate trust.` });
117
+ return 0;
118
+ } });
119
+ define({ name: "permissions", summary: "View or set permission mode", usage: "forge permissions [mode]", interactive: true, async run(ctx) {
120
+ const config = await loadConfig();
121
+ const requested = ctx.args[0];
122
+ const modes = ["read-only", "ask", "workspace-write", "trusted-session", "custom", "autonomous"];
123
+ if (!requested) {
124
+ text(ctx, `current: ${config.permissionMode}`);
125
+ for (const action of ["read", "write", "command", "network", "destructive", "billing", "publish"])
126
+ text(ctx, `${action}: ${permissionDecision(config.permissionMode, action)}`);
127
+ return 0;
128
+ }
129
+ if (!modes.includes(requested))
130
+ throw new Error(`Unknown permission mode: ${requested}`);
131
+ await saveConfig({ ...config, permissionMode: requested });
132
+ ctx.emit({ type: "success", message: `Permission mode set to ${requested}.` });
133
+ return 0;
134
+ } });
135
+ define({ name: "model", summary: "View or select the product model", usage: "forge model [forge-spark|forge-anvil|forge-foundry]", interactive: true, async run(ctx) {
136
+ const config = await loadConfig();
137
+ const model = ctx.args[0];
138
+ const bootstrap = await getBootstrap(config.apiBaseUrl);
139
+ const models = Array.isArray(bootstrap.models) ? bootstrap.models : [];
140
+ if (!model) {
141
+ for (const item of models)
142
+ text(ctx, `${item.id === config.model ? "*" : " "} ${item.id} ${item.name ?? ""} context ${item.contextWindow?.toLocaleString() ?? "unknown"}`);
143
+ return 0;
144
+ }
145
+ if (!models.some((item) => item.id === model))
146
+ throw new Error(`Model is unavailable: ${model}`);
147
+ await saveConfig({ ...config, model });
148
+ ctx.emit({ type: "success", message: `Model set to ${model}.` });
149
+ return 0;
150
+ } });
151
+ for (const name of ["usage", "credits", "plan"])
152
+ define({ name, summary: `Show account ${name}`, usage: `forge ${name}`, interactive: true, async run(ctx) {
153
+ const config = await loadConfig();
154
+ const data = await getBootstrap(config.apiBaseUrl);
155
+ ctx.emit({ type: "info", message: JSON.stringify(name === "credits" ? data.credits : name === "plan" ? data.plan : data, null, 2) });
156
+ return 0;
157
+ } });
158
+ define({ name: "config", summary: "Show effective configuration", usage: "forge config", interactive: true, async run(ctx) { text(ctx, JSON.stringify(await loadConfigWithSources(ctx.cwd), null, 2)); return 0; } });
159
+ define({ name: "sessions", summary: "List saved sessions", usage: "forge sessions", interactive: true, async run(ctx) {
160
+ const sessions = await listSessions();
161
+ if (!sessions.length)
162
+ text(ctx, "No saved sessions.");
163
+ for (const item of sessions)
164
+ text(ctx, `${item.id} ${item.updatedAt} ${item.title}`);
165
+ return 0;
166
+ } });
167
+ define({ name: "new", summary: "Create a session", usage: "forge new [title]", interactive: true, async run(ctx) {
168
+ const session = await createSession(ctx.cwd, await loadConfig(), ctx.args.join(" ") || undefined);
169
+ ctx.emit({ type: "success", message: `Created session ${session.id}.` });
170
+ return 0;
171
+ } });
172
+ define({ name: "resume", summary: "Resume a saved session", usage: "forge resume <id>", interactive: true, async run(ctx) {
173
+ const id = ctx.args[0];
174
+ if (!id)
175
+ throw new Error("Session id is required.");
176
+ const session = await loadSession(id);
177
+ if (!session)
178
+ throw new Error("Session not found.");
179
+ await setCurrentSession(id);
180
+ ctx.emit({ type: "success", message: `Resumed ${session.title}.` });
181
+ return 0;
182
+ } });
183
+ define({ name: "fork", summary: "Fork the current session", usage: "forge fork", interactive: true, async run(ctx) {
184
+ const session = await getCurrentSession();
185
+ if (!session)
186
+ throw new Error("No current session.");
187
+ const copy = await forkSession(session);
188
+ ctx.emit({ type: "success", message: `Forked to ${copy.id}.` });
189
+ return 0;
190
+ } });
191
+ define({ name: "rename", summary: "Rename the current session", usage: "forge rename <title>", interactive: true, async run(ctx) {
192
+ const session = await getCurrentSession();
193
+ if (!session)
194
+ throw new Error("No current session.");
195
+ const title = ctx.args.join(" ").trim();
196
+ if (!title)
197
+ throw new Error("Title is required.");
198
+ await saveSession({ ...session, title });
199
+ ctx.emit({ type: "success", message: `Renamed to ${title}.` });
200
+ return 0;
201
+ } });
202
+ define({ name: "clear", summary: "Clear current session messages", usage: "forge clear", interactive: true, async run(ctx) {
203
+ const session = await getCurrentSession();
204
+ if (!session)
205
+ throw new Error("No current session.");
206
+ await clearSession(session);
207
+ ctx.emit({ type: "success", message: "Session cleared." });
208
+ return 0;
209
+ } });
210
+ define({ name: "compact", summary: "Compact the current session", usage: "forge compact", interactive: true, async run(ctx) {
211
+ const session = await getCurrentSession();
212
+ if (!session)
213
+ throw new Error("No current session.");
214
+ const before = session.messages.length;
215
+ const next = await compactSession(session);
216
+ ctx.emit({ type: "success", message: before === next.messages.length ? "Session is already compact." : `Compacted ${before} messages to ${next.messages.length}; full transcript archived locally.` });
217
+ return 0;
218
+ } });
219
+ define({ name: "prune", summary: "Delete old sessions after confirmation", usage: "forge prune [--days 30]", interactive: true, async run(ctx) {
220
+ const days = Number(ctx.flags.get("days") ?? 30);
221
+ if (!Number.isInteger(days) || days < 1)
222
+ throw new Error("--days must be a positive integer.");
223
+ const current = await getCurrentSession();
224
+ const candidates = (await sessionsOlderThan(days)).filter((item) => item.id !== current?.id);
225
+ if (!candidates.length) {
226
+ text(ctx, `No inactive sessions older than ${days} days.`);
227
+ return 0;
228
+ }
229
+ const approved = await ctx.requestApproval?.(`Delete ${candidates.length} inactive session files older than ${days} days? This is irreversible.`, false);
230
+ if (!approved) {
231
+ ctx.emit({ type: "warning", message: "Prune cancelled." });
232
+ return 1;
233
+ }
234
+ for (const item of candidates)
235
+ await deleteSession(item.id);
236
+ await audit("sessions.prune", { count: candidates.length, days });
237
+ ctx.emit({ type: "success", message: `Deleted ${candidates.length} sessions.` });
238
+ return 0;
239
+ } });
240
+ define({ name: "init", summary: "Create a local .forge project marker", usage: "forge init", interactive: true, async run(ctx) {
241
+ const trust = await findTrustRecord(ctx.cwd);
242
+ if (!trust.record)
243
+ throw new Error("Trust the workspace before initializing Forge.");
244
+ const root = trust.current.repositoryRoot ?? trust.current.path;
245
+ const target = join(root, ".forge", "config.json");
246
+ if (relative(root, target).startsWith(".."))
247
+ throw new Error("Forge config path escapes the workspace.");
248
+ const approved = await ctx.requestApproval?.(`Create ${target}? This reversible write enables project-local Forge configuration.`, false);
249
+ if (!approved) {
250
+ ctx.emit({ type: "warning", message: "Initialization cancelled." });
251
+ return 1;
252
+ }
253
+ await writePrivateJson(target, { version: 1, taskMode: "plan", permissionMode: "ask" });
254
+ await audit("workspace.init", { target });
255
+ ctx.emit({ type: "success", message: `Created ${target}.` });
256
+ return 0;
257
+ } });
258
+ define({ name: "diff", summary: "Show local changes made by Forge", usage: "forge diff", interactive: true, async run(ctx) {
259
+ ctx.emit({ type: "info", message: "No Forge-applied local patch exists in this session. Phase 2 remote runs cannot mutate files." });
260
+ return 0;
261
+ } });
262
+ for (const kind of ["memory", "skills", "agents", "plugins", "mcp", "hooks"])
263
+ define({ name: kind, summary: `Inspect loaded ${kind}`, usage: `forge ${kind}`, interactive: true, async run(ctx) {
264
+ const singular = kind === "skills" ? "skill" : kind === "agents" ? "agent" : kind === "plugins" ? "plugin" : kind === "hooks" ? "hook" : kind;
265
+ const items = (await inspectExtensions(ctx.cwd)).filter((item) => item.kind === singular);
266
+ if (!items.length) {
267
+ text(ctx, `No trusted ${kind} detected.`);
268
+ return 0;
269
+ }
270
+ for (const item of items)
271
+ text(ctx, `${item.name} source=${item.source} ${item.path}`);
272
+ return 0;
273
+ } });
274
+ define({ name: "over-limit", summary: "Inspect paid over-limit controls", usage: "forge over-limit", interactive: true, async run(ctx) {
275
+ const config = await loadConfig();
276
+ const billing = await getBillingStatus(config.apiBaseUrl);
277
+ text(ctx, JSON.stringify(billing, null, 2));
278
+ ctx.emit({ type: "warning", message: "Not configured. This CLI cannot enable paid usage, change limits, or purchase credits." });
279
+ return 0;
280
+ } });
281
+ define({ name: "completion", summary: "Generate shell completion", usage: "forge completion [bash|zsh|fish]", interactive: true, async run(ctx) {
282
+ const shell = ctx.args[0] ?? "zsh";
283
+ const names = [...new Set(registry.values())].map((command) => command.name).sort().join(" ");
284
+ if (shell === "bash")
285
+ process.stdout.write(`complete -W "${names}" forge\n`);
286
+ else if (shell === "zsh")
287
+ process.stdout.write(`#compdef forge\n_arguments '1:command:(${names})'\n`);
288
+ else if (shell === "fish")
289
+ process.stdout.write(names.split(" ").map((name) => `complete -c forge -f -a '${name}'`).join("\n") + "\n");
290
+ else
291
+ throw new Error("Shell must be bash, zsh, or fish.");
292
+ return 0;
293
+ } });
294
+ for (const modeName of ["plan", "execute", "review"])
295
+ define({ name: modeName === "review" ? "review" : modeName, summary: `${modeName} a task`, usage: `forge ${modeName} <task>`, interactive: true, async run(ctx) {
296
+ const config = await loadConfig();
297
+ let session = await getCurrentSession();
298
+ let prompt = ctx.args.join(" ").trim();
299
+ if (!prompt && modeName === "execute" && session?.plan?.status === "approved")
300
+ prompt = `Execute this approved plan:\n${session.plan.steps.map((step, index) => `${index + 1}. ${step}`).join("\n")}`;
301
+ if (!prompt)
302
+ throw new Error(modeName === "execute" ? "Task text or an approved plan is required." : "Task text is required.");
303
+ const trust = await findTrustRecord(ctx.cwd);
304
+ if (!trust.record && config.permissionMode !== "read-only")
305
+ throw new Error("Workspace is not trusted. Run `forge trust --once`, `forge trust --always`, or `forge trust --read-only`.");
306
+ if (!session)
307
+ session = await createSession(ctx.cwd, config, prompt.slice(0, 72));
308
+ const project = await collectProjectManifest(ctx.cwd);
309
+ await saveSession({ ...session, state: modeName === "plan" ? "planning" : "executing" });
310
+ let response;
311
+ try {
312
+ response = await runRemote({ apiBaseUrl: config.apiBaseUrl, prompt, model: config.model, mode: modeName, sessionId: session.id, project, signal: ctx.signal, emit: ctx.emit });
313
+ }
314
+ catch (error) {
315
+ await saveSession({ ...session, state: ctx.signal.aborted ? "cancelled" : "failed" });
316
+ throw error;
317
+ }
318
+ const messages = [...session.messages,
319
+ { role: "user", content: prompt, at: new Date().toISOString() },
320
+ { role: "assistant", content: response.text, at: new Date().toISOString() },
321
+ ];
322
+ const draftPlan = modeName === "plan" ? {
323
+ task: prompt,
324
+ steps: response.text.split("\n").map((line) => line.replace(/^\s*(?:\d+[.)]|[-*])\s*/, "").trim()).filter(Boolean),
325
+ status: "draft",
326
+ createdAt: new Date().toISOString(),
327
+ } : session.plan;
328
+ const baseNext = response.usage
329
+ ? { ...session, state: "completed", messages, usage: mergeUsage(session.usage, response.usage) }
330
+ : { ...session, state: "completed", messages };
331
+ const next = draftPlan ? { ...baseNext, plan: draftPlan } : baseNext;
332
+ await saveSession(next);
333
+ const warning = contextWarning(next.usage);
334
+ if (warning)
335
+ ctx.emit({ type: "warning", message: warning });
336
+ process.stdout.write(ctx.outputMode === "human" ? "\n" : "");
337
+ return 0;
338
+ } });
339
+ define({ name: "run", aliases: ["ask"], summary: "Run a task using the configured mode", usage: "forge run <task>", interactive: true, async run(ctx) {
340
+ const config = await loadConfig();
341
+ const command = registry.get(config.taskMode === "read-only" ? "review" : config.taskMode);
342
+ if (!command)
343
+ throw new Error(`Unsupported task mode: ${config.taskMode}`);
344
+ return command.run(ctx);
345
+ } });
346
+ define({ name: "view-plan", summary: "View the current task plan", usage: "forge view-plan", interactive: true, async run(ctx) {
347
+ const session = await getCurrentSession();
348
+ if (!session?.plan)
349
+ throw new Error("No plan in the current session.");
350
+ text(ctx, `${session.plan.task} [${session.plan.status}]`);
351
+ session.plan.steps.forEach((step, index) => text(ctx, `${index + 1}. ${step}`));
352
+ return 0;
353
+ } });
354
+ define({ name: "approve-plan", summary: "Approve the current task plan", usage: "forge approve-plan", interactive: true, async run(ctx) {
355
+ const session = await getCurrentSession();
356
+ if (!session?.plan)
357
+ throw new Error("No plan in the current session.");
358
+ await saveSession({ ...session, state: "waiting-for-approval" });
359
+ const approved = await ctx.requestApproval?.("Approve this plan for execution?", false);
360
+ if (!approved) {
361
+ await saveSession({ ...session, state: "completed" });
362
+ ctx.emit({ type: "warning", message: "Plan was not approved." });
363
+ return 1;
364
+ }
365
+ await saveSession({ ...session, state: "completed", plan: { ...session.plan, status: "approved", approvedAt: new Date().toISOString() } });
366
+ await audit("plan.approve", { sessionId: session.id });
367
+ ctx.emit({ type: "success", message: "Plan approved. Run `forge execute`." });
368
+ return 0;
369
+ } });
370
+ define({ name: "edit-plan", summary: "Replace the current plan steps", usage: "forge edit-plan <step; step>", interactive: true, async run(ctx) {
371
+ const session = await getCurrentSession();
372
+ if (!session?.plan)
373
+ throw new Error("No plan in the current session.");
374
+ const steps = ctx.args.join(" ").split(";").map((step) => step.trim()).filter(Boolean);
375
+ if (!steps.length)
376
+ throw new Error("Provide semicolon-separated plan steps.");
377
+ await saveSession({ ...session, plan: { ...session.plan, steps, status: "draft" } });
378
+ ctx.emit({ type: "success", message: "Plan updated and returned to draft." });
379
+ return 0;
380
+ } });
381
+ for (const [name, summary] of [
382
+ ["cancel", "Cancel the active run"],
383
+ ])
384
+ define({ name, summary, usage: `forge ${name}`, interactive: true, async run(ctx) {
385
+ ctx.emit({ type: "warning", message: "No task is active at the prompt. Press Ctrl-C while a task is streaming to cancel it." });
386
+ return 2;
387
+ } });
388
+ define({ name: "exit", aliases: ["quit"], summary: "Exit interactive mode", usage: "/exit", interactive: true, async run() { return 99; } });
389
+ export function resolveCommand(name) { return registry.get(name) ?? null; }
390
+ export function allCommands() { return [...new Set(registry.values())]; }
391
+ //# sourceMappingURL=commands.js.map