codelocal 1.5.0-beta.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.
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # CodeLocal
2
+
3
+ Local code intelligence and execution runtime for ChatGPT.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -g codelocal
9
+ ```
10
+
11
+ ## Run
12
+
13
+ ```bash
14
+ codelocal
15
+ ```
16
+
17
+ CodeLocal starts the machine runtime in your terminal and connects it to ChatGPT.
18
+
19
+ To authorize the current project once:
20
+
21
+ ```bash
22
+ codelocal .
23
+ ```
24
+
25
+ This npm package contains compiled runtime files only. The CodeLocal source repository and cloud backend are not distributed in this package.
26
+
27
+ Copyright © CodeLocal. All rights reserved.
@@ -0,0 +1,105 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { DEFAULT_STATE_DIR, readJsonFile, writeJsonAtomic } from "./state.js";
5
+ function workspaceHash(workspaceKey) {
6
+ return createHash("sha256").update(workspaceKey).digest("hex").slice(0, 24);
7
+ }
8
+ export class ApprovalMemory {
9
+ rootDir;
10
+ constructor(rootDir = path.join(DEFAULT_STATE_DIR, "approvals")) {
11
+ this.rootDir = rootDir;
12
+ }
13
+ fileFor(workspaceKey) {
14
+ return path.join(this.rootDir, `${workspaceHash(workspaceKey)}.json`);
15
+ }
16
+ async read(workspaceKey) {
17
+ const value = await readJsonFile(this.fileFor(workspaceKey), { version: 1, workspaceKey, approvals: [] });
18
+ const approvals = Array.isArray(value.approvals)
19
+ ? value.approvals.filter((entry) => entry?.workspaceKey === workspaceKey && typeof entry.actionKey === "string")
20
+ : [];
21
+ return { version: 1, workspaceKey, approvals };
22
+ }
23
+ async write(workspaceKey, approvals) {
24
+ const unique = new Map();
25
+ for (const entry of approvals)
26
+ unique.set(entry.actionKey, entry);
27
+ await writeJsonAtomic(this.fileFor(workspaceKey), {
28
+ version: 1,
29
+ workspaceKey,
30
+ approvals: [...unique.values()].sort((a, b) => b.lastUsedAt - a.lastUsedAt),
31
+ });
32
+ }
33
+ async find(workspaceKey, actionKey) {
34
+ const data = await this.read(workspaceKey);
35
+ return data.approvals.find((entry) => entry.actionKey === actionKey) ?? null;
36
+ }
37
+ async remember(workspaceKey, decision) {
38
+ if (decision.approvalPolicy !== "rememberable" || !decision.approvalKey)
39
+ return null;
40
+ const data = await this.read(workspaceKey);
41
+ const previous = data.approvals.find((entry) => entry.actionKey === decision.approvalKey);
42
+ const now = Date.now();
43
+ const entry = {
44
+ id: previous?.id ?? randomUUID(),
45
+ workspaceKey,
46
+ actionKey: decision.approvalKey,
47
+ label: decision.approvalLabel ?? decision.redactedCommand,
48
+ redactedCommand: decision.redactedCommand,
49
+ riskLevel: decision.riskLevel,
50
+ matchedRules: [...decision.matchedRules],
51
+ createdAt: previous?.createdAt ?? now,
52
+ lastUsedAt: now,
53
+ useCount: (previous?.useCount ?? 0) + 1,
54
+ };
55
+ await this.write(workspaceKey, [...data.approvals.filter((item) => item.actionKey !== entry.actionKey), entry]);
56
+ return entry;
57
+ }
58
+ async touch(workspaceKey, actionKey) {
59
+ const data = await this.read(workspaceKey);
60
+ const entry = data.approvals.find((item) => item.actionKey === actionKey);
61
+ if (!entry)
62
+ return null;
63
+ entry.lastUsedAt = Date.now();
64
+ entry.useCount = Math.max(0, Number(entry.useCount) || 0) + 1;
65
+ await this.write(workspaceKey, data.approvals);
66
+ return entry;
67
+ }
68
+ async list(workspaceKey) {
69
+ if (workspaceKey)
70
+ return (await this.read(workspaceKey)).approvals;
71
+ const names = await fs.readdir(this.rootDir).catch(() => []);
72
+ const output = [];
73
+ for (const name of names) {
74
+ if (!name.endsWith(".json"))
75
+ continue;
76
+ const value = await readJsonFile(path.join(this.rootDir, name), { version: 1, workspaceKey: "", approvals: [] });
77
+ if (!value.workspaceKey || !Array.isArray(value.approvals))
78
+ continue;
79
+ output.push(...value.approvals.filter((entry) => entry?.workspaceKey === value.workspaceKey && typeof entry.actionKey === "string"));
80
+ }
81
+ return output.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
82
+ }
83
+ async revoke(identifier, workspaceKey) {
84
+ const targets = workspaceKey ? [workspaceKey] : [...new Set((await this.list()).map((entry) => entry.workspaceKey))];
85
+ let removed = 0;
86
+ for (const key of targets) {
87
+ const data = await this.read(key);
88
+ const next = data.approvals.filter((entry) => entry.id !== identifier && entry.actionKey !== identifier);
89
+ removed += data.approvals.length - next.length;
90
+ if (next.length !== data.approvals.length)
91
+ await this.write(key, next);
92
+ }
93
+ return removed;
94
+ }
95
+ async reset(workspaceKey) {
96
+ if (workspaceKey) {
97
+ const data = await this.read(workspaceKey);
98
+ await fs.rm(this.fileFor(workspaceKey), { force: true });
99
+ return data.approvals.length;
100
+ }
101
+ const count = (await this.list()).length;
102
+ await fs.rm(this.rootDir, { recursive: true, force: true });
103
+ return count;
104
+ }
105
+ }
package/dist/audit.js ADDED
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+ import { appendPrivateJsonl, DEFAULT_STATE_DIR } from "./state.js";
3
+ import { redactCommand } from "./security-policy.js";
4
+ const AUDIT_ENABLED = process.env.CODELOCAL_AUDIT_FILE !== "0";
5
+ const AUDIT_FILE = process.env.CODELOCAL_AUDIT_PATH ?? path.join(DEFAULT_STATE_DIR, "audit.jsonl");
6
+ function sanitize(value, key = "") {
7
+ if (value instanceof Error)
8
+ return { name: value.name, message: value.message };
9
+ if (Array.isArray(value))
10
+ return value.map((item) => sanitize(item));
11
+ if (typeof value === "string") {
12
+ if (/command|detail|query/i.test(key))
13
+ return redactCommand(value).slice(0, 2000);
14
+ if (/content|patch|input|oldText|newText/i.test(key))
15
+ return `[${Buffer.byteLength(value, "utf8")} bytes]`;
16
+ return value.slice(0, 4000);
17
+ }
18
+ if (!value || typeof value !== "object")
19
+ return value;
20
+ const out = {};
21
+ for (const [childKey, childValue] of Object.entries(value)) {
22
+ if (/token|secret|password|authorization|cookie|credentialSecret/i.test(childKey))
23
+ out[childKey] = "[REDACTED]";
24
+ else
25
+ out[childKey] = sanitize(childValue, childKey);
26
+ }
27
+ return out;
28
+ }
29
+ export async function audit(event) {
30
+ if (!AUDIT_ENABLED)
31
+ return;
32
+ const record = sanitize({ ts: event.ts ?? new Date().toISOString(), ...event });
33
+ await appendPrivateJsonl(AUDIT_FILE, record).catch(() => undefined);
34
+ }
@@ -0,0 +1,77 @@
1
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
2
+ function hash(value) {
3
+ return createHash("sha256").update(value).digest("hex");
4
+ }
5
+ function sameHex(a, b) {
6
+ const aa = Buffer.from(a, "hex");
7
+ const bb = Buffer.from(b, "hex");
8
+ return aa.length === bb.length && timingSafeEqual(aa, bb);
9
+ }
10
+ export class ChatApprovalBroker {
11
+ ttlMs;
12
+ pending = new Map();
13
+ constructor(ttlMs = Number(process.env.CODELOCAL_CHAT_APPROVAL_TTL_MS ?? 5 * 60_000)) {
14
+ this.ttlMs = ttlMs;
15
+ }
16
+ fingerprint(command, cwd, decision) {
17
+ return hash(JSON.stringify({
18
+ rawCommandHash: hash(command),
19
+ redactedCommand: decision.redactedCommand,
20
+ cwd,
21
+ rules: [...decision.matchedRules].sort(),
22
+ risk: decision.riskLevel,
23
+ approvalPolicy: decision.approvalPolicy,
24
+ approvalKey: decision.approvalKey ?? null,
25
+ }));
26
+ }
27
+ prune() {
28
+ const now = Date.now();
29
+ for (const [id, approval] of this.pending)
30
+ if (approval.expiresAt <= now)
31
+ this.pending.delete(id);
32
+ }
33
+ preflight(command, cwd, decision) {
34
+ this.prune();
35
+ if (decision.blocked)
36
+ return { status: "blocked", riskLevel: decision.riskLevel, reason: decision.reason, matchedRules: decision.matchedRules, command: decision.redactedCommand, approvalPolicy: decision.approvalPolicy, approvalKey: decision.approvalKey, approvalLabel: decision.approvalLabel };
37
+ if (!decision.requiresApproval)
38
+ return { status: "safe", riskLevel: decision.riskLevel, reason: decision.reason, matchedRules: decision.matchedRules, command: decision.redactedCommand, approvalPolicy: decision.approvalPolicy, approvalKey: decision.approvalKey, approvalLabel: decision.approvalLabel };
39
+ const approvalToken = randomUUID() + randomUUID().replaceAll("-", "");
40
+ const id = randomUUID();
41
+ const expiresAt = Date.now() + this.ttlMs;
42
+ this.pending.set(id, { tokenHash: hash(approvalToken), fingerprint: this.fingerprint(command, cwd, decision), expiresAt });
43
+ return {
44
+ status: "approval_required",
45
+ riskLevel: decision.riskLevel,
46
+ reason: decision.reason,
47
+ matchedRules: decision.matchedRules,
48
+ command: decision.redactedCommand,
49
+ approvalPolicy: decision.approvalPolicy,
50
+ approvalKey: decision.approvalKey,
51
+ approvalLabel: decision.approvalLabel,
52
+ approvalToken: `${id}.${approvalToken}`,
53
+ expiresAt,
54
+ };
55
+ }
56
+ consume(approvalToken, command, cwd, decision) {
57
+ this.prune();
58
+ if (!decision.requiresApproval || decision.blocked)
59
+ return !decision.blocked;
60
+ if (!approvalToken)
61
+ return false;
62
+ const dot = approvalToken.indexOf(".");
63
+ if (dot <= 0)
64
+ return false;
65
+ const id = approvalToken.slice(0, dot);
66
+ const secret = approvalToken.slice(dot + 1);
67
+ const pending = this.pending.get(id);
68
+ if (!pending)
69
+ return false;
70
+ this.pending.delete(id);
71
+ if (pending.expiresAt <= Date.now())
72
+ return false;
73
+ if (!sameHex(pending.tokenHash, hash(secret)))
74
+ return false;
75
+ return sameHex(pending.fingerprint, this.fingerprint(command, cwd, decision));
76
+ }
77
+ }
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { promises as fs } from "node:fs";
5
+ import { spawn } from "node:child_process";
6
+ import { defaultDeviceIdentity, deleteLocalCredential, loadLocalCredential, saveLocalCredential } from "./identity.js";
7
+ import { WorkspaceRegistry } from "./workspace-registry.js";
8
+ import { ApprovalMemory } from "./approval-memory.js";
9
+ import { RuntimeDaemon } from "./runtime-daemon.js";
10
+ const DEFAULT_CLOUD = process.env.CODELOCAL_SERVER ?? "https://codelocal.cloud";
11
+ function usage() {
12
+ console.log(`CodeLocal CLI · machine runtime
13
+
14
+ Quick start:
15
+ codelocal
16
+
17
+ One-time workspace access:
18
+ codelocal grant ~/Projects/my-app
19
+
20
+ Then keep only this running:
21
+ codelocal
22
+
23
+ ChatGPT can list your previously granted workspaces and activate the one you choose in chat.
24
+
25
+ Commands:
26
+ codelocal Start the machine runtime; no project cwd required
27
+ codelocal grant <project> Authorize a project folder locally
28
+ codelocal ungrant <id|project> Remove a project's local authorization
29
+ codelocal workspaces List authorized local workspaces
30
+ codelocal . Backward-compatible: grant + activate current project
31
+ codelocal <project-path> Backward-compatible: grant + activate a project
32
+ codelocal login Open CodeLocal login
33
+ codelocal dashboard Open CodeLocal dashboard
34
+ codelocal pair [gateway] Pair this machine manually
35
+ codelocal status Show pairing + workspace status
36
+ codelocal approvals List remembered local approvals
37
+ codelocal approvals revoke <id> Revoke one remembered approval
38
+ codelocal approvals reset Forget all remembered approvals
39
+ codelocal doctor <project> Run local environment checks
40
+ codelocal mcp ... Manage local MCP extensions
41
+
42
+ Default cloud:
43
+ ${DEFAULT_CLOUD}
44
+ `);
45
+ }
46
+ function normalizeBase(value) {
47
+ const url = new URL(value);
48
+ if (url.protocol === "ws:")
49
+ url.protocol = "http:";
50
+ if (url.protocol === "wss:")
51
+ url.protocol = "https:";
52
+ url.pathname = "";
53
+ url.search = "";
54
+ url.hash = "";
55
+ return url.toString().replace(/\/$/, "");
56
+ }
57
+ function httpToWs(base) {
58
+ const url = new URL(normalizeBase(base));
59
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
60
+ url.pathname = "/client";
61
+ return url.toString();
62
+ }
63
+ function wsToHttp(value) {
64
+ return normalizeBase(value);
65
+ }
66
+ function openBrowser(url) {
67
+ try {
68
+ let command;
69
+ let args;
70
+ if (process.platform === "darwin") {
71
+ command = "open";
72
+ args = [url];
73
+ }
74
+ else if (process.platform === "win32") {
75
+ command = "cmd";
76
+ args = ["/c", "start", "", url];
77
+ }
78
+ else {
79
+ command = "xdg-open";
80
+ args = [url];
81
+ }
82
+ const child = spawn(command, args, { detached: true, stdio: "ignore", shell: false });
83
+ child.unref();
84
+ return true;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }
90
+ async function pair(baseArg = DEFAULT_CLOUD) {
91
+ const base = normalizeBase(baseArg || DEFAULT_CLOUD);
92
+ const wsUrl = httpToWs(base);
93
+ const existing = await loadLocalCredential(wsUrl);
94
+ if (existing)
95
+ return existing;
96
+ const device = defaultDeviceIdentity();
97
+ const response = await fetch(`${base}/pair/start`, {
98
+ method: "POST",
99
+ headers: { "content-type": "application/json" },
100
+ body: JSON.stringify(device),
101
+ signal: AbortSignal.timeout(15_000),
102
+ });
103
+ if (!response.ok)
104
+ throw new Error(`Unable to start device pairing (${response.status}).`);
105
+ const pairing = await response.json();
106
+ console.log(`\nCodeLocal needs to pair this machine.\n`);
107
+ console.log(`Pairing code: ${pairing.code}`);
108
+ console.log(`Approve: ${pairing.approveUrl}\n`);
109
+ if (openBrowser(pairing.approveUrl))
110
+ console.log("Opened your default browser. Sign in to CodeLocal and approve this device.");
111
+ else
112
+ console.log("Open the approval URL in your browser, sign in, and approve this device.");
113
+ console.log("Waiting for approval…");
114
+ while (Date.now() < pairing.expiresAt) {
115
+ await new Promise((resolve) => setTimeout(resolve, 1800));
116
+ const claim = await fetch(`${base}/pair/claim`, {
117
+ method: "POST",
118
+ headers: { "content-type": "application/json" },
119
+ body: JSON.stringify({ pairingId: pairing.pairingId, code: pairing.code }),
120
+ signal: AbortSignal.timeout(10_000),
121
+ }).catch(() => null);
122
+ if (!claim?.ok)
123
+ continue;
124
+ const credential = await claim.json();
125
+ const saved = await saveLocalCredential({ ...credential, serverUrl: wsUrl });
126
+ console.log(`✓ ${credential.deviceName} paired with CodeLocal Cloud.\n`);
127
+ return saved;
128
+ }
129
+ throw new Error("Pairing expired. Run `codelocal` again to create a new pairing request.");
130
+ }
131
+ async function validateCredential(server, credential) {
132
+ try {
133
+ const response = await fetch(`${wsToHttp(server)}/api/client/auth/check`, {
134
+ method: "POST",
135
+ headers: {
136
+ "content-type": "application/json",
137
+ "x-codelocal-credential-id": credential.credentialId,
138
+ authorization: `Device ${credential.credentialSecret}`,
139
+ },
140
+ body: "{}",
141
+ signal: AbortSignal.timeout(8_000),
142
+ });
143
+ if (response.status === 401 || response.status === 403 || response.status === 404)
144
+ return false;
145
+ return true;
146
+ }
147
+ catch {
148
+ return true;
149
+ }
150
+ }
151
+ async function resolvedRuntime(serverArg) {
152
+ // The selected gateway must come from an explicit override or this build's default.
153
+ // Never let a credential saved for an older gateway silently retarget CodeLocal.
154
+ const configured = serverArg || process.env.SERVER_URL || httpToWs(DEFAULT_CLOUD);
155
+ const server = configured.startsWith("ws://") || configured.startsWith("wss://") ? configured : httpToWs(configured);
156
+ let credential = await loadLocalCredential(server);
157
+ if (credential && !(await validateCredential(server, credential))) {
158
+ console.log("Stored CodeLocal credential is no longer valid or the gateway is incompatible. Pairing this machine again…");
159
+ await deleteLocalCredential();
160
+ credential = null;
161
+ }
162
+ if (!credential)
163
+ credential = await pair(wsToHttp(server));
164
+ return { server, credential };
165
+ }
166
+ async function runRuntime(projectArg, serverArg) {
167
+ const registry = new WorkspaceRegistry();
168
+ let initialWorkspaceId;
169
+ if (projectArg) {
170
+ const granted = await registry.grant(projectArg);
171
+ initialWorkspaceId = granted.workspaceId;
172
+ console.log(`✓ Workspace granted: ${granted.workspaceName}`);
173
+ }
174
+ const { server, credential } = await resolvedRuntime(serverArg);
175
+ const daemon = new RuntimeDaemon({ baseUrl: wsToHttp(server), serverUrl: server, credential, initialWorkspaceId });
176
+ const stop = async () => { await daemon.stop(); process.exit(0); };
177
+ process.once("SIGINT", () => { void stop(); });
178
+ process.once("SIGTERM", () => { void stop(); });
179
+ await daemon.run();
180
+ }
181
+ async function grant(projectArg) {
182
+ if (!projectArg)
183
+ throw new Error("Usage: codelocal grant <project-folder>");
184
+ const entry = await new WorkspaceRegistry().grant(projectArg);
185
+ console.log(`✓ Granted ${entry.workspaceName}`);
186
+ console.log(` ID: ${entry.workspaceId}`);
187
+ console.log(` Path: ${entry.localPath}`);
188
+ console.log("If `codelocal` is already running, it will sync this workspace shortly.");
189
+ }
190
+ async function ungrant(identifier) {
191
+ if (!identifier)
192
+ throw new Error("Usage: codelocal ungrant <workspace-id|project-folder>");
193
+ const removed = await new WorkspaceRegistry().revoke(identifier);
194
+ console.log(removed ? "✓ Workspace authorization removed." : "Workspace was not found in the local authorization registry.");
195
+ }
196
+ async function listWorkspaces() {
197
+ const workspaces = await new WorkspaceRegistry().list();
198
+ if (!workspaces.length) {
199
+ console.log("No authorized workspaces. Use `codelocal grant /path/to/project`.");
200
+ return;
201
+ }
202
+ for (const workspace of workspaces) {
203
+ console.log(`${workspace.workspaceName}\n ${workspace.workspaceId}\n ${workspace.localPath}${workspace.lastActivatedAt ? `\n last activated ${new Date(workspace.lastActivatedAt).toLocaleString()}` : ""}\n`);
204
+ }
205
+ }
206
+ async function approvalsCommand(args) {
207
+ const memory = new ApprovalMemory();
208
+ const action = args[0] ?? "list";
209
+ if (action === "list") {
210
+ const approvals = await memory.list();
211
+ if (!approvals.length) {
212
+ console.log("No remembered approvals.");
213
+ return;
214
+ }
215
+ for (const approval of approvals) {
216
+ console.log(`${approval.label}\n ID: ${approval.id}\n Workspace: ${approval.workspaceKey}\n Key: ${approval.actionKey}\n Used: ${approval.useCount} · last ${new Date(approval.lastUsedAt).toLocaleString()}\n`);
217
+ }
218
+ return;
219
+ }
220
+ if (action === "revoke") {
221
+ const id = args[1];
222
+ if (!id)
223
+ throw new Error("Usage: codelocal approvals revoke <id|action-key>");
224
+ const removed = await memory.revoke(id);
225
+ console.log(removed ? `✓ Removed ${removed} remembered approval${removed === 1 ? "" : "s"}.` : "Approval not found.");
226
+ return;
227
+ }
228
+ if (action === "reset") {
229
+ const removed = await memory.reset();
230
+ console.log(`✓ Forgot ${removed} remembered approval${removed === 1 ? "" : "s"}.`);
231
+ return;
232
+ }
233
+ throw new Error("Usage: codelocal approvals [list|revoke <id|action-key>|reset]");
234
+ }
235
+ async function status() {
236
+ const credential = await loadLocalCredential();
237
+ const workspaces = await new WorkspaceRegistry().list();
238
+ console.log(JSON.stringify({
239
+ device: defaultDeviceIdentity(),
240
+ paired: !!credential,
241
+ authorizedWorkspaces: workspaces.map(({ workspaceId, workspaceName, localPath, grantedAt, lastActivatedAt }) => ({ workspaceId, workspaceName, localPath, grantedAt, lastActivatedAt })),
242
+ credential: credential ? {
243
+ credentialId: credential.credentialId,
244
+ deviceId: credential.deviceId,
245
+ deviceName: credential.deviceName,
246
+ serverUrl: credential.serverUrl,
247
+ createdAt: credential.createdAt,
248
+ credentialSecret: "[REDACTED]",
249
+ } : null,
250
+ stateDir: process.env.CODELOCAL_STATE_DIR ?? path.join(os.homedir(), ".codelocal"),
251
+ }, null, 2));
252
+ }
253
+ async function login(baseArg = DEFAULT_CLOUD, dashboard = false) {
254
+ const base = normalizeBase(baseArg || DEFAULT_CLOUD);
255
+ const url = dashboard ? `${base}/dashboard` : `${base}/login`;
256
+ console.log(url);
257
+ if (!openBrowser(url))
258
+ console.log("Open the URL above in your browser.");
259
+ }
260
+ async function looksLikeProjectPath(value) {
261
+ if (!value || value.startsWith("-"))
262
+ return false;
263
+ if (value === "." || value === ".." || value.startsWith("./") || value.startsWith("../") || path.isAbsolute(value))
264
+ return true;
265
+ return fs.stat(path.resolve(value)).then((stat) => stat.isDirectory()).catch(() => false);
266
+ }
267
+ async function delegateLegacyCli() {
268
+ await import("./cli.js");
269
+ }
270
+ const [, , command, ...args] = process.argv;
271
+ try {
272
+ if (!command)
273
+ await runRuntime();
274
+ else if (command === "help" || command === "--help" || command === "-h")
275
+ usage();
276
+ else if (command === "login")
277
+ await login(args[0] ?? DEFAULT_CLOUD, false);
278
+ else if (command === "dashboard")
279
+ await login(args[0] ?? DEFAULT_CLOUD, true);
280
+ else if (command === "pair") {
281
+ await pair(args[0] ?? DEFAULT_CLOUD);
282
+ }
283
+ else if (command === "status")
284
+ await status();
285
+ else if (command === "approvals")
286
+ await approvalsCommand(args);
287
+ else if (command === "workspaces")
288
+ await listWorkspaces();
289
+ else if (command === "grant")
290
+ await grant(args[0]);
291
+ else if (command === "ungrant")
292
+ await ungrant(args[0]);
293
+ else if (command === "start")
294
+ await runRuntime(args[0] ?? ".", args[1]);
295
+ else if (command === "doctor" || command === "mcp")
296
+ await delegateLegacyCli();
297
+ else if (command === "rotate" || command === "revoke") {
298
+ console.log("Device rotation/revocation is account-scoped in SaaS mode. Opening Security/Devices dashboard…");
299
+ await login(DEFAULT_CLOUD, true);
300
+ }
301
+ else if (await looksLikeProjectPath(command))
302
+ await runRuntime(command, args[0]);
303
+ else {
304
+ usage();
305
+ process.exitCode = 1;
306
+ }
307
+ }
308
+ catch (error) {
309
+ console.error(error instanceof Error ? error.message : String(error));
310
+ process.exitCode = 1;
311
+ }