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/dist/cli.js ADDED
@@ -0,0 +1,344 @@
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 { createInterface } from "node:readline/promises";
6
+ import { defaultDeviceIdentity, deleteLocalCredential, loadLocalCredential, saveLocalCredential } from "./identity.js";
7
+ import { McpHub, parseEnvReference, parseHeaderEnvReference } from "./mcp-hub.js";
8
+ function usage() {
9
+ console.log(`CodeLocal CLI
10
+
11
+ Usage:
12
+ codelocal .
13
+ codelocal <project-path>
14
+
15
+ Core commands:
16
+ codelocal doctor <project>
17
+ codelocal pair <https://gateway>
18
+ codelocal start <project> [wss://gateway/client]
19
+ codelocal status
20
+ codelocal rotate <https://gateway>
21
+ codelocal revoke <https://gateway>
22
+ codelocal login <https://gateway>
23
+
24
+ MCP Hub:
25
+ codelocal mcp add <name> -- <command> [args...]
26
+ codelocal mcp add <name> --stdio <command> [--arg <arg> ...]
27
+ codelocal mcp add <name> --url <https://server/mcp>
28
+ codelocal mcp list [--json]
29
+ codelocal mcp info <name>
30
+ codelocal mcp probe <name>
31
+ codelocal mcp search <query> [--server <name>]
32
+ codelocal mcp remove <name> [--global|--workspace]
33
+
34
+ MCP add options:
35
+ --global Install for every workspace (default: current workspace)
36
+ --workspace Install only for the current workspace
37
+ --cwd <path> Working directory for stdio MCP
38
+ --env KEY[=SOURCE_ENV] Pass an environment variable by reference; secret is not stored
39
+ --header-env H=ENV HTTP header value from an environment variable
40
+ --bearer-env ENV Authorization: Bearer <value from ENV>
41
+ --no-probe Save config without connecting/listing tools
42
+ `);
43
+ }
44
+ function httpToWs(base) {
45
+ const url = new URL(base);
46
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
47
+ url.pathname = "/client";
48
+ url.search = "";
49
+ url.hash = "";
50
+ return url.toString();
51
+ }
52
+ function normalizeBase(value) {
53
+ const url = new URL(value);
54
+ url.pathname = "";
55
+ url.search = "";
56
+ url.hash = "";
57
+ return url.toString().replace(/\/$/, "");
58
+ }
59
+ async function commandExists(command) {
60
+ const dirs = (process.env.PATH ?? "").split(path.delimiter);
61
+ const suffixes = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
62
+ for (const dir of dirs) {
63
+ for (const suffix of suffixes) {
64
+ try {
65
+ await fs.access(path.join(dir, `${command}${suffix}`));
66
+ return true;
67
+ }
68
+ catch { }
69
+ }
70
+ }
71
+ return false;
72
+ }
73
+ async function doctor(projectArg) {
74
+ const project = await fs.realpath(path.resolve(projectArg || "."));
75
+ const checks = [];
76
+ checks.push({ name: "project root", ok: true, detail: project });
77
+ for (const command of ["git", "rg", "node", "npm"])
78
+ checks.push({ name: command, ok: await commandExists(command) });
79
+ for (const command of ["pyright-langserver", "rust-analyzer", "gopls", "clangd", "jdtls", "lua-language-server", "sourcekit-lsp", "zls"]) {
80
+ checks.push({ name: `semantic:${command}`, ok: await commandExists(command), detail: "optional; text fallback remains available" });
81
+ }
82
+ const markers = ["package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle", "CMakeLists.txt", "Package.swift", "pubspec.yaml"];
83
+ const detected = [];
84
+ for (const marker of markers)
85
+ if (await fs.stat(path.join(project, marker)).then(() => true).catch(() => false))
86
+ detected.push(marker);
87
+ console.log(`\nCodeLocal doctor\nProject: ${project}\nDetected: ${detected.join(", ") || "generic workspace"}\n`);
88
+ for (const check of checks)
89
+ console.log(`${check.ok ? "✓" : "·"} ${check.name}${check.detail ? ` — ${check.detail}` : ""}`);
90
+ console.log("\nRequired core tools missing:", checks.filter((c) => ["git", "node", "npm"].includes(c.name) && !c.ok).map((c) => c.name).join(", ") || "none");
91
+ console.log("Semantic servers are optional because CodeLocal falls back to TypeScript AST/text search.\n");
92
+ }
93
+ async function pair(serverArg) {
94
+ const base = normalizeBase(serverArg);
95
+ const identity = defaultDeviceIdentity();
96
+ const response = await fetch(`${base}/pair/start`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(identity) });
97
+ if (!response.ok)
98
+ throw new Error(`Pair start failed: ${response.status} ${await response.text()}`);
99
+ const pairing = await response.json();
100
+ console.log(`\nPairing code: ${pairing.code}\nApprove URL: ${pairing.approveUrl}\n`);
101
+ console.log("Open the URL, enter this pairing code and your CodeLocal passphrase. Waiting for approval...");
102
+ while (Date.now() < pairing.expiresAt) {
103
+ await new Promise((resolve) => setTimeout(resolve, 2000));
104
+ const claim = await fetch(`${base}/pair/claim`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ pairingId: pairing.pairingId, code: pairing.code }) });
105
+ if (claim.ok) {
106
+ const credential = await claim.json();
107
+ const wsUrl = httpToWs(base);
108
+ await saveLocalCredential({ ...credential, serverUrl: wsUrl });
109
+ console.log(`\n✓ Device paired. Credential saved privately for ${wsUrl}.`);
110
+ return;
111
+ }
112
+ }
113
+ throw new Error("Pairing expired before approval.");
114
+ }
115
+ async function start(projectArg, serverArg) {
116
+ const project = await fs.realpath(path.resolve(projectArg || "."));
117
+ const credential = await loadLocalCredential();
118
+ const server = serverArg ? (serverArg.startsWith("ws") ? serverArg : httpToWs(normalizeBase(serverArg))) : process.env.SERVER_URL ?? credential?.serverUrl;
119
+ if (!server)
120
+ throw new Error("No gateway configured. Run `codelocal pair https://gateway` or pass a websocket URL.");
121
+ process.env.PROJECT_ROOT = project;
122
+ process.env.SERVER_URL = server;
123
+ process.env.CODELOCAL_ALLOW_SHELL ??= "1";
124
+ process.env.CODELOCAL_APPROVAL_MODE ??= "prompt";
125
+ await import("./client-entry-v2.js");
126
+ }
127
+ async function promptSecret(label) {
128
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
129
+ try {
130
+ return (await rl.question(`${label}: `)).trim();
131
+ }
132
+ finally {
133
+ rl.close();
134
+ }
135
+ }
136
+ async function rotate(serverArg) {
137
+ const base = normalizeBase(serverArg);
138
+ const ws = httpToWs(base);
139
+ const credential = await loadLocalCredential(ws);
140
+ if (!credential)
141
+ throw new Error("No matching local credential.");
142
+ const password = await promptSecret("CodeLocal passphrase");
143
+ const response = await fetch(`${base}/devices/rotate`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ credentialId: credential.credentialId, password }) });
144
+ if (!response.ok)
145
+ throw new Error(`Rotate failed: ${response.status}`);
146
+ const rotated = await response.json();
147
+ await saveLocalCredential({ ...credential, credentialSecret: rotated.credentialSecret, serverUrl: ws, createdAt: credential.createdAt });
148
+ console.log("✓ Device credential rotated.");
149
+ }
150
+ async function revoke(serverArg) {
151
+ const base = normalizeBase(serverArg);
152
+ const ws = httpToWs(base);
153
+ const credential = await loadLocalCredential(ws);
154
+ if (!credential)
155
+ throw new Error("No matching local credential.");
156
+ const password = await promptSecret("CodeLocal passphrase");
157
+ const response = await fetch(`${base}/devices/revoke`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ credentialId: credential.credentialId, password }) });
158
+ if (!response.ok)
159
+ throw new Error(`Revoke failed: ${response.status}`);
160
+ await deleteLocalCredential();
161
+ console.log("✓ Device revoked and local credential removed.");
162
+ }
163
+ async function status() {
164
+ const credential = await loadLocalCredential();
165
+ console.log(JSON.stringify({
166
+ device: defaultDeviceIdentity(),
167
+ paired: !!credential,
168
+ credential: credential ? { credentialId: credential.credentialId, deviceId: credential.deviceId, deviceName: credential.deviceName, serverUrl: credential.serverUrl, createdAt: credential.createdAt, credentialSecret: "[REDACTED]" } : null,
169
+ stateDir: process.env.CODELOCAL_STATE_DIR ?? path.join(os.homedir(), ".codelocal"),
170
+ }, null, 2));
171
+ }
172
+ async function login(serverArg) {
173
+ const base = normalizeBase(serverArg);
174
+ console.log(`Gateway: ${base}\nMCP endpoint: ${base}/mcp\nOAuth authorization happens when you connect this MCP endpoint from ChatGPT.`);
175
+ }
176
+ function optionValues(args, option) {
177
+ const values = [];
178
+ for (let i = 0; i < args.length; i++) {
179
+ if (args[i] !== option)
180
+ continue;
181
+ const value = args[i + 1];
182
+ if (!value || value.startsWith("--"))
183
+ throw new Error(`${option} requires a value.`);
184
+ values.push(value);
185
+ i++;
186
+ }
187
+ return values;
188
+ }
189
+ function optionValue(args, option) {
190
+ const values = optionValues(args, option);
191
+ if (values.length > 1)
192
+ throw new Error(`${option} may only be specified once.`);
193
+ return values[0];
194
+ }
195
+ async function mcpCommand(args) {
196
+ const [actionRaw, nameOrQuery, ...rest] = args;
197
+ const action = actionRaw === "install" ? "add" : actionRaw;
198
+ const hub = new McpHub(process.cwd());
199
+ if (!action || action === "help" || action === "--help" || action === "-h") {
200
+ usage();
201
+ return;
202
+ }
203
+ if (action === "list") {
204
+ const servers = await hub.listServers();
205
+ if (rest.includes("--json") || nameOrQuery === "--json")
206
+ console.log(JSON.stringify({ servers }, null, 2));
207
+ else if (!servers.length)
208
+ console.log("No MCP servers installed for this workspace.");
209
+ else {
210
+ console.log("\nCodeLocal MCP servers\n");
211
+ for (const server of servers)
212
+ console.log(`${server.enabled ? "✓" : "·"} ${server.name} ${server.transport} ${server.scope} tools:${server.toolsCached}${server.connected ? " connected" : ""}`);
213
+ console.log("");
214
+ }
215
+ return;
216
+ }
217
+ if (action === "info") {
218
+ if (!nameOrQuery)
219
+ throw new Error("Usage: codelocal mcp info <name>");
220
+ console.log(JSON.stringify(await hub.serverInfo(nameOrQuery), null, 2));
221
+ return;
222
+ }
223
+ if (action === "probe") {
224
+ if (!nameOrQuery)
225
+ throw new Error("Usage: codelocal mcp probe <name>");
226
+ const result = await hub.probe(nameOrQuery);
227
+ console.log(`✓ ${nameOrQuery}: connected, ${result.toolCount} tools discovered.`);
228
+ for (const tool of result.tools.slice(0, 20))
229
+ console.log(` - ${tool.name}${tool.description ? ` — ${tool.description.slice(0, 100)}` : ""}`);
230
+ if (result.truncated || result.toolCount > 20)
231
+ console.log(` … ${result.toolCount - Math.min(20, result.toolCount)} more`);
232
+ await hub.shutdown();
233
+ return;
234
+ }
235
+ if (action === "search") {
236
+ const queryParts = [nameOrQuery, ...rest.filter((value, index) => value !== "--server" && (index === 0 || rest[index - 1] !== "--server"))].filter((value) => !!value && !value.startsWith("--"));
237
+ const server = optionValue(rest, "--server");
238
+ const query = queryParts.join(" ").trim();
239
+ if (!query)
240
+ throw new Error("Usage: codelocal mcp search <query> [--server <name>]");
241
+ console.log(JSON.stringify(await hub.searchTools(query, { server }), null, 2));
242
+ await hub.shutdown();
243
+ return;
244
+ }
245
+ if (action === "remove" || action === "uninstall") {
246
+ if (!nameOrQuery)
247
+ throw new Error("Usage: codelocal mcp remove <name> [--global|--workspace]");
248
+ if (rest.includes("--global") && rest.includes("--workspace"))
249
+ throw new Error("Choose only one of --global or --workspace.");
250
+ const scope = rest.includes("--global") ? "global" : rest.includes("--workspace") ? "workspace" : undefined;
251
+ const result = await hub.removeServer(nameOrQuery, scope);
252
+ console.log(result.removed ? `✓ Removed ${nameOrQuery} (${result.removed} config${result.removed === 1 ? "" : "s"}).` : `No matching MCP config found for ${nameOrQuery}.`);
253
+ return;
254
+ }
255
+ if (action !== "add")
256
+ throw new Error(`Unknown MCP action: ${action}`);
257
+ if (!nameOrQuery)
258
+ throw new Error("Usage: codelocal mcp add <name> -- <command> [args...] OR --url <url>");
259
+ const separator = rest.indexOf("--");
260
+ const commandTail = separator >= 0 ? rest.slice(separator + 1) : [];
261
+ const options = separator >= 0 ? rest.slice(0, separator) : rest;
262
+ if (options.includes("--global") && options.includes("--workspace"))
263
+ throw new Error("Choose only one of --global or --workspace.");
264
+ const scope = options.includes("--global") ? "global" : "workspace";
265
+ const stdioOption = optionValue(options, "--stdio");
266
+ const remoteUrl = optionValue(options, "--url");
267
+ if (commandTail.length && stdioOption)
268
+ throw new Error("Use either `-- <command>` or --stdio, not both.");
269
+ if (remoteUrl && (commandTail.length || stdioOption))
270
+ throw new Error("Use either a stdio command or --url, not both.");
271
+ const env = Object.fromEntries(optionValues(options, "--env").map(parseEnvReference));
272
+ const headers = Object.fromEntries(optionValues(options, "--header-env").map(parseHeaderEnvReference));
273
+ const bearerEnv = optionValue(options, "--bearer-env");
274
+ if (bearerEnv)
275
+ headers.Authorization = { source: bearerEnv, prefix: "Bearer " };
276
+ const cwd = optionValue(options, "--cwd");
277
+ const command = commandTail[0] ?? stdioOption;
278
+ const commandArgs = commandTail.length ? commandTail.slice(1) : optionValues(options, "--arg");
279
+ if (!command && !remoteUrl)
280
+ throw new Error("MCP add requires `-- <command> [args...]`, --stdio <command>, or --url <url>.");
281
+ const server = await hub.addServer({
282
+ name: nameOrQuery,
283
+ enabled: true,
284
+ scope,
285
+ workspaceRoot: scope === "workspace" ? process.cwd() : undefined,
286
+ transport: remoteUrl ? "http" : "stdio",
287
+ command,
288
+ args: command ? commandArgs : undefined,
289
+ cwd,
290
+ env: Object.keys(env).length ? env : undefined,
291
+ url: remoteUrl,
292
+ headers: Object.keys(headers).length ? headers : undefined,
293
+ });
294
+ console.log(`✓ Installed MCP ${server.name} (${server.transport}, ${server.scope}).`);
295
+ if (!options.includes("--no-probe")) {
296
+ try {
297
+ const result = await hub.probe(server.name);
298
+ console.log(`✓ Probe passed: ${result.toolCount} tools cached for smart routing.`);
299
+ }
300
+ catch (error) {
301
+ console.warn(`! Installed, but probe failed: ${error instanceof Error ? error.message : String(error)}`);
302
+ console.warn(` Fix the MCP configuration/environment, then run: codelocal mcp probe ${server.name}`);
303
+ }
304
+ }
305
+ await hub.shutdown();
306
+ }
307
+ async function looksLikeProjectPath(value) {
308
+ if (!value || value.startsWith("-"))
309
+ return false;
310
+ if (value === "." || value === ".." || value.startsWith("./") || value.startsWith("../") || path.isAbsolute(value))
311
+ return true;
312
+ return fs.stat(path.resolve(value)).then((stat) => stat.isDirectory()).catch(() => false);
313
+ }
314
+ const [, , command, ...args] = process.argv;
315
+ try {
316
+ if (!command || command === "help" || command === "--help" || command === "-h")
317
+ usage();
318
+ else if (command === "doctor")
319
+ await doctor(args[0] ?? ".");
320
+ else if (command === "pair")
321
+ await pair(args[0] ?? process.env.CODELOCAL_SERVER ?? "");
322
+ else if (command === "start")
323
+ await start(args[0] ?? ".", args[1]);
324
+ else if (command === "status")
325
+ await status();
326
+ else if (command === "rotate")
327
+ await rotate(args[0] ?? process.env.CODELOCAL_SERVER ?? "");
328
+ else if (command === "revoke")
329
+ await revoke(args[0] ?? process.env.CODELOCAL_SERVER ?? "");
330
+ else if (command === "login")
331
+ await login(args[0] ?? process.env.CODELOCAL_SERVER ?? "");
332
+ else if (command === "mcp")
333
+ await mcpCommand(args);
334
+ else if (await looksLikeProjectPath(command))
335
+ await start(command, args[0]);
336
+ else {
337
+ usage();
338
+ process.exitCode = 1;
339
+ }
340
+ }
341
+ catch (error) {
342
+ console.error(error instanceof Error ? error.message : String(error));
343
+ process.exitCode = 1;
344
+ }
@@ -0,0 +1,22 @@
1
+ // Dev client bootstrap. Patch the Chokidar singleton before client-v2 loads so
2
+ // existing watcher consumers transparently use a native recursive backend.
3
+ // @parcel/watcher is optional; bounded Chokidar remains the safety fallback.
4
+ const { log } = await import("./log.js");
5
+ const fallbackDepth = Math.max(0, Number(process.env.CODELOCAL_WATCH_DEPTH ?? 2) || 0);
6
+ const chokidarModule = await import("chokidar");
7
+ const chokidar = chokidarModule.default;
8
+ const { installNativeWatcherAdapter } = await import("./native-watcher.js");
9
+ const watcher = installNativeWatcherAdapter(chokidar, {
10
+ fallbackDepth,
11
+ log(level, event, detail = {}) {
12
+ log(level, event, detail);
13
+ },
14
+ });
15
+ log("info", "workspace.watcher_configured", {
16
+ backend: watcher.preferredBackend,
17
+ recursive: true,
18
+ fallbackBackend: "chokidar-bounded",
19
+ fallbackDepth: watcher.fallbackDepth,
20
+ });
21
+ await import("./client-v2.js");
22
+ export {};