@kvell007/embed-labs-cli 0.1.0-alpha.3 → 0.1.0-alpha.31

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/index.js CHANGED
@@ -1,24 +1,28 @@
1
1
  #!/usr/bin/env node
2
- import { createHash } from "node:crypto";
2
+ import { createHash, createHmac, generateKeyPairSync, randomBytes, sign as signCrypto } from "node:crypto";
3
3
  import { constants } from "node:fs";
4
4
  import { spawn } from "node:child_process";
5
- import { access, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
5
+ import { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
6
6
  import { createRequire } from "node:module";
7
- import { homedir, tmpdir } from "node:os";
7
+ import { arch, hostname, homedir, platform, tmpdir } from "node:os";
8
8
  import { basename, delimiter, dirname, join, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
- import { startServer } from "@embed-labs/local-bridge";
11
10
  import { composeBootLogoPackage } from "./image-compose.js";
12
- import { buildTaishanPiQtSmoke, compileTaishanPiSingleFile, validateLocalToolchain } from "./local-toolchain.js";
11
+ import { buildTaishanPiQtSmoke, compileTaishanPiSingleFile, currentLocalToolchain, installLocalToolchain, latestLocalToolchain, listLocalToolchainEnvironments, validateLocalToolchain } from "./local-toolchain.js";
13
12
  import { fail, ok } from "@embed-labs/protocol";
14
13
  const require = createRequire(import.meta.url);
15
14
  const CLI_MODULE_DIR = dirname(fileURLToPath(import.meta.url));
16
15
  const SOURCE_CHECKOUT_ROOT = resolve(CLI_MODULE_DIR, "..", "..", "..");
17
16
  const qrcodeTerminal = require("qrcode-terminal");
18
17
  const DEFAULT_BRIDGE_URL = process.env.EMBED_BRIDGE_URL ?? "http://127.0.0.1:18083";
19
- const DEFAULT_CLOUD_API_URL = process.env.EMBED_CLOUD_API_URL ?? "http://127.0.0.1:18100";
18
+ const DEFAULT_CLOUD_API_URL = process.env.EMBED_CLOUD_API_URL ?? "https://api.embedboard.com";
20
19
  const DEFAULT_AUTH_FILE = process.env.EMBED_AUTH_FILE ?? ".embed-labs/auth.json";
20
+ const DEFAULT_DEVICE_FILE = process.env.EMBED_DEVICE_FILE ?? join(dirname(DEFAULT_AUTH_FILE), "device.json");
21
+ const DEFAULT_DASHBOARD_URL = process.env.EMBED_DASHBOARD_URL ?? "https://api.embedboard.com/dashboard";
22
+ const EMBED_CLIENT_NAME = "embedlabs-cli";
23
+ const EMBED_CLIENT_VERSION = process.env.EMBED_CLIENT_VERSION ?? "0.1.0";
21
24
  const DEFAULT_AGENT_ARTIFACT_DIR = process.env.EMBED_AGENT_ARTIFACT_DIR ?? ".embed-labs/artifacts";
25
+ const TOOL_INTEGRITY_RELOGIN_MESSAGE = "工具完整性校验失败,请在当前电脑重新执行 embedlabs auth login --token <key>";
22
26
  const DOCTOR_USAGE = "Usage: embed doctor [--json]";
23
27
  const DOCTOR_HTTP_TIMEOUT_MS = 8000;
24
28
  const MIN_NODE_MAJOR = 20;
@@ -27,7 +31,19 @@ const DEVICE_PROBE_USAGE = "Usage: embed device probe --host <host> --ports 22,1
27
31
  const QUERY_USAGE = "Usage: embed query <natural language request> [--account <account_id>] [--qr] [--json]";
28
32
  const DEFAULT_PLUGIN_RELEASE_URL = process.env.EMBED_PLUGIN_RELEASE_URL?.trim() || "https://api.embedboard.com/plugin-releases/agent-plugins/latest";
29
33
  const PLUGIN_LIST_USAGE = "Usage: embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]";
34
+ const CODEX_PLUGIN_NAME = "embed-labs";
35
+ const CODEX_MARKETPLACE_NAME = "embed-labs";
36
+ const LEGACY_CODEX_PLUGIN_NAMES = [
37
+ "dbt-agent",
38
+ "Dbt Agent",
39
+ "development-board-toolchain",
40
+ "development-board-toolchain-dev",
41
+ "deve"
42
+ ];
43
+ const LEGACY_CODEX_MARKETPLACE_NAMES = new Set(["embed-labs-plugins", "plugins", "Plugins", "deve"]);
30
44
  const PLUGIN_INSTALL_USAGE = "Usage: embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]";
45
+ const PLUGIN_UPDATE_CHECK_USAGE = "Usage: embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
46
+ const PLUGIN_UPDATE_USAGE = "Usage: embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
31
47
  const CLOUD_TASK_ARTIFACTS_USAGE = "Usage: embed cloud task artifacts <task_id> [--json]";
32
48
  const CLOUD_TASK_EVIDENCE_USAGE = "Usage: embed cloud task evidence <task_id> [--json]";
33
49
  const ARTIFACT_STATUS_USAGE = "Usage: embed artifact status <artifact_id> [--json]";
@@ -81,9 +97,18 @@ const BUILD_IMAGE_BOOT_LOGO_USAGE = "Usage: embed build image boot-logo --logo <
81
97
  const IMAGE_BOOT_LOGO_COMPOSE_USAGE = "Usage: embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]";
82
98
  const BUILD_IMAGE_DTB_USAGE = "Usage: embed build image dtb --dtb <local.dtb|local.dts> [--input-format auto|dtb|dts] [--account <account_id>] [--project <project_id>] [--board taishanpi] [--variant 1M-RK3566] [--output <package.json>] [--json]";
83
99
  const IMAGE_DTB_COMPOSE_USAGE = "Usage: embed image dtb compose --package <dtb-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]";
84
- const LOCAL_TOOLCHAIN_VALIDATE_USAGE = "Usage: embed local toolchain validate [--release-root <path>] [--json]";
100
+ const LOCAL_TOOLCHAIN_LIST_USAGE = "Usage: embed local toolchain list [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]";
101
+ const LOCAL_TOOLCHAIN_INSTALLED_USAGE = "Usage: embed local toolchain installed [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]";
102
+ const LOCAL_TOOLCHAIN_LATEST_USAGE = "Usage: embed local toolchain latest [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--json]";
103
+ const LOCAL_TOOLCHAIN_CURRENT_USAGE = "Usage: embed local toolchain current [--install-root <path>] [--json]";
104
+ const LOCAL_TOOLCHAIN_INSTALL_USAGE = "Usage: embed local toolchain install [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--force] [--json]\nDefault source: the production download channel at download.embedboard.com.";
105
+ const LOCAL_TOOLCHAIN_VALIDATE_USAGE = "Usage: embed local toolchain validate [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor] [--release-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--json]";
85
106
  const LOCAL_COMPILE_TAISHANPI_USAGE = "Usage: embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]";
86
107
  const LOCAL_BUILD_QT_SMOKE_USAGE = "Usage: embed local build qt-smoke --build-dir <dir> [--source <qt-smoke-dir>] [--release-root <path>] [--account <account_id>] [--json]";
108
+ const AUTH_DEVICE_STATUS_USAGE = "Usage: embed auth device status [--json]";
109
+ const AUTH_DEVICE_LIST_USAGE = "Usage: embed auth device list [--json]";
110
+ const AUTH_DEVICE_REVOKE_USAGE = "Usage: embed auth device revoke <device_id> [--json]";
111
+ const AUTH_DEVICE_RENAME_USAGE = "Usage: embed auth device rename <device_id> --label <name> [--json]";
87
112
  const BOARD_REGISTRY_LIST_USAGE = "Usage: embed board registry list [--json]";
88
113
  const BOARD_REGISTRY_SHOW_USAGE = "Usage: embed board registry show <template_id> [--json]";
89
114
  const BOARD_METHODS_USAGE = "Usage: embed board methods <template_id> [--json]";
@@ -93,8 +118,10 @@ const MODEL_LIST_USAGE = "Usage: embed model list [--json]";
93
118
  const MODEL_DEFAULT_USAGE = "Usage: embed model default [--json]";
94
119
  const SERVICE_MODES_USAGE = "Usage: embed service modes [--json]";
95
120
  const AGENT_RUN_USAGE = "Usage: embed agent run --prompt <request> [--account <account_id>] [--workspace <workspace_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--max-tool-calls 6] [--host <ip>] [--ports 22,15301] [--artifact <local_file>|--artifact-id <artifact_id>|--artifact-task <task_id>] [--artifact-output <path>] [--remote-path <path>] [--run] [--approve] [--json]";
121
+ let cachedLocalHardwareFingerprint;
96
122
  const TOOL_LIST_USAGE = "Usage: embed tool list [--json]";
97
123
  const TOOL_CALL_USAGE = "Usage: embed tool call <capability_id> [--input-json '<json>'] [--approve] [--json]";
124
+ const MCP_TOOL_EVENT_USAGE = "Usage: embed mcp log --tool <tool_name> [--client codex|opencode] [--mode local_ai|server_ai] [--local-device-id <id>] [--server-model-used true|false] [--success true|false] [--request-id <id>] [--duration-ms <ms>] [--input-summary <text>] [--output-summary <text>] [--json]";
98
125
  const BOARD_DEPLOY_TAISHANPI_USAGE = "Usage: embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--user root] [--remote-path /userdata/embed-labs/apps/app] [--run] [--timeout 30] [--json]";
99
126
  const CLOUD_TASK_EVENT_APPEND_USAGE = "Usage: embed cloud task event append <task_id> [--state <state>] [--progress-stage <stage>|--stage <stage>] [--progress-text <text>|--message <text>] [--progress-percent 0-100] [--severity info|warning|error] [--type <event_type>] [--artifact-json '<json>'] [--evidence-json '<json>'] [--json]";
100
127
  const TASK_STATES = new Set([
@@ -144,11 +171,7 @@ async function main(argv) {
144
171
  return output(parsed, await bridgePost("/v1/board/taishanpi/deploy", request), renderBoardDeployResult);
145
172
  }
146
173
  if (area === "bridge" && action === "start") {
147
- startServer({
148
- host: stringFlag(parsed, "host"),
149
- port: numberFlag(parsed, "port")
150
- });
151
- return await waitForever();
174
+ return await runBridgeStart(parsed);
152
175
  }
153
176
  if (area === "bridge" && action === "status") {
154
177
  return output(parsed, await bridgeGet("/healthz"), renderBridgeStatus);
@@ -276,7 +299,18 @@ async function main(argv) {
276
299
  const result = await pluginInstall(parsed);
277
300
  return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
278
301
  }
279
- return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE].join("\n")), undefined, 2);
302
+ if (action === "update") {
303
+ if (parsed.command[2] === "check") {
304
+ const result = await pluginUpdateCheck(parsed);
305
+ return output(parsed, result, renderPluginUpdateCheck, result.ok ? 0 : 2);
306
+ }
307
+ if (["codex", "opencode", "all"].includes(parsed.command[2] ?? "")) {
308
+ const result = await pluginUpdate(parsed);
309
+ return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
310
+ }
311
+ return output(parsed, fail("invalid_args", [PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
312
+ }
313
+ return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE, PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
280
314
  }
281
315
  if (area === "auth" && action === "login") {
282
316
  const result = await authLogin(parsed);
@@ -285,6 +319,31 @@ async function main(argv) {
285
319
  if (area === "auth" && action === "status") {
286
320
  return output(parsed, ok(await authStatus()), renderAuthStatus);
287
321
  }
322
+ if (area === "auth" && action === "device") {
323
+ const deviceAction = parsed.command[2] ?? "status";
324
+ if (deviceAction === "status") {
325
+ const result = await authDeviceStatus(parsed);
326
+ return output(parsed, result, renderAuthDeviceStatus, result.ok ? 0 : 2);
327
+ }
328
+ if (deviceAction === "list") {
329
+ const result = await authDeviceList(parsed);
330
+ return output(parsed, result, renderAuthDeviceList, result.ok ? 0 : 2);
331
+ }
332
+ if (deviceAction === "revoke") {
333
+ const result = await authDeviceRevoke(parsed);
334
+ return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
335
+ }
336
+ if (deviceAction === "rename") {
337
+ const result = await authDeviceRename(parsed);
338
+ return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
339
+ }
340
+ return output(parsed, fail("invalid_args", [
341
+ AUTH_DEVICE_STATUS_USAGE,
342
+ AUTH_DEVICE_LIST_USAGE,
343
+ AUTH_DEVICE_REVOKE_USAGE,
344
+ AUTH_DEVICE_RENAME_USAGE
345
+ ].join("\n")), undefined, 2);
346
+ }
288
347
  if (area === "auth" && action === "logout") {
289
348
  await rm(DEFAULT_AUTH_FILE, { force: true });
290
349
  return output(parsed, ok(await authStatus()), renderAuthStatus);
@@ -364,6 +423,16 @@ async function main(argv) {
364
423
  USAGE_EVENTS_USAGE
365
424
  ].join("\n")), undefined, 2);
366
425
  }
426
+ if (area === "mcp") {
427
+ if (action === "log" || action === "tool-event") {
428
+ const body = mcpToolEventBody(parsed);
429
+ if (typeof body === "string") {
430
+ return output(parsed, fail("invalid_args", body), undefined, 2);
431
+ }
432
+ return output(parsed, await cloudPost("/v1/mcp/tool-events", body), renderMcpToolEvent);
433
+ }
434
+ return output(parsed, fail("invalid_args", MCP_TOOL_EVENT_USAGE), undefined, 2);
435
+ }
367
436
  if (area === "billing") {
368
437
  if (action === "statement") {
369
438
  const request = billingStatementRequest(parsed);
@@ -503,12 +572,47 @@ async function main(argv) {
503
572
  return output(parsed, fail("invalid_args", [IMAGE_BOOT_LOGO_COMPOSE_USAGE, IMAGE_DTB_COMPOSE_USAGE].join("\n")), undefined, 2);
504
573
  }
505
574
  if (area === "local") {
575
+ if (action === "toolchain" && parsed.command[2] === "list") {
576
+ const request = localToolchainListRequest(parsed);
577
+ if (typeof request === "string") {
578
+ return output(parsed, fail("invalid_args", request), undefined, 2);
579
+ }
580
+ return output(parsed, ok(await listLocalToolchainEnvironments(request)), renderLocalToolchainList);
581
+ }
582
+ if (action === "toolchain" && parsed.command[2] === "installed") {
583
+ const request = localToolchainListRequest(parsed, LOCAL_TOOLCHAIN_INSTALLED_USAGE);
584
+ if (typeof request === "string") {
585
+ return output(parsed, fail("invalid_args", request), undefined, 2);
586
+ }
587
+ return output(parsed, ok(await listLocalToolchainEnvironments({ ...request, installedOnly: true })), renderLocalToolchainList);
588
+ }
589
+ if (action === "toolchain" && parsed.command[2] === "latest") {
590
+ const request = localToolchainLatestRequest(parsed);
591
+ if (typeof request === "string") {
592
+ return output(parsed, fail("invalid_args", request), undefined, 2);
593
+ }
594
+ return output(parsed, ok(await latestLocalToolchain(request)), renderLocalToolchainLatest);
595
+ }
596
+ if (action === "toolchain" && parsed.command[2] === "current") {
597
+ const request = localToolchainCurrentRequest(parsed);
598
+ if (typeof request === "string") {
599
+ return output(parsed, fail("invalid_args", request), undefined, 2);
600
+ }
601
+ return output(parsed, ok(await currentLocalToolchain(request.installRoot)), renderLocalToolchainCurrent);
602
+ }
603
+ if (action === "toolchain" && parsed.command[2] === "install") {
604
+ const request = localToolchainInstallRequest(parsed);
605
+ if (typeof request === "string") {
606
+ return output(parsed, fail("invalid_args", request), undefined, 2);
607
+ }
608
+ return output(parsed, ok(await installLocalToolchain(request)), renderLocalToolchainInstall);
609
+ }
506
610
  if (action === "toolchain" && parsed.command[2] === "validate") {
507
611
  const request = localToolchainValidateRequest(parsed);
508
612
  if (typeof request === "string") {
509
613
  return output(parsed, fail("invalid_args", request), undefined, 2);
510
614
  }
511
- return output(parsed, ok(await validateLocalToolchain(request.releaseRoot)), renderLocalToolchainValidation);
615
+ return output(parsed, ok(await validateLocalToolchain(request)), renderLocalToolchainValidation);
512
616
  }
513
617
  if (action === "compile" && parsed.command[2] === "taishanpi") {
514
618
  const request = localCompileTaishanPiRequest(parsed, await authStatus());
@@ -525,6 +629,11 @@ async function main(argv) {
525
629
  return output(parsed, ok(await buildTaishanPiQtSmoke(request)), renderLocalCompileResult);
526
630
  }
527
631
  return output(parsed, fail("invalid_args", [
632
+ LOCAL_TOOLCHAIN_LIST_USAGE,
633
+ LOCAL_TOOLCHAIN_INSTALLED_USAGE,
634
+ LOCAL_TOOLCHAIN_LATEST_USAGE,
635
+ LOCAL_TOOLCHAIN_CURRENT_USAGE,
636
+ LOCAL_TOOLCHAIN_INSTALL_USAGE,
528
637
  LOCAL_TOOLCHAIN_VALIDATE_USAGE,
529
638
  LOCAL_COMPILE_TAISHANPI_USAGE,
530
639
  LOCAL_BUILD_QT_SMOKE_USAGE
@@ -1099,11 +1208,11 @@ async function doctor() {
1099
1208
  const bridgeHealth = await apiDoctorCheck("bridge_health", "Local Bridge health", `${bridgeBaseUrl}/healthz`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderHealthSummary, healthStatus);
1100
1209
  checks.push(bridgeHealth);
1101
1210
  if (isUsableDoctorCheck(bridgeHealth)) {
1102
- checks.push(await apiDoctorCheck("device_scan", "Device scan", `${bridgeBaseUrl}/v1/devices`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDeviceScanSummary, warningIfWarnings));
1211
+ checks.push(await apiDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDeviceScanSummary, warningIfWarnings));
1103
1212
  checks.push(await apiDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDebugToolScanSummary, warningIfWarnings));
1104
1213
  }
1105
1214
  else {
1106
- checks.push(dependentDoctorCheck("device_scan", "Device scan", `${bridgeBaseUrl}/v1/devices`, "Device scan requires a reachable Local Bridge."));
1215
+ checks.push(dependentDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "Device inventory requires a reachable Local Bridge."));
1107
1216
  checks.push(dependentDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "Debug tool scan requires a reachable Local Bridge."));
1108
1217
  }
1109
1218
  checks.push(await apiDoctorCheck("cloud_api_health", "Cloud API health", `${cloudBaseUrl}/healthz`, "cloud_api_unreachable", `Cloud API is unreachable at ${cloudBaseUrl}.`, renderHealthSummary, healthStatus));
@@ -1162,7 +1271,8 @@ function authDoctorCheck(status) {
1162
1271
  : {
1163
1272
  code: "auth_not_ready",
1164
1273
  message: "No CLI auth token is configured.",
1165
- remediation: "Run: embed auth login --token <token>"
1274
+ remediation: cloudAuthSetupRemediation(),
1275
+ details: cloudAuthSetupDetails()
1166
1276
  }
1167
1277
  };
1168
1278
  }
@@ -1295,7 +1405,7 @@ function warningIfWarnings(data) {
1295
1405
  }
1296
1406
  function renderDeviceScanSummary(result) {
1297
1407
  const warningText = result.warnings?.length ? ` ${result.warnings.length} warning(s).` : "";
1298
- return `Device scan completed: ${result.devices.length} device(s), ${result.usb.length} USB item(s), ${result.serial.length} serial port(s).${warningText}`;
1408
+ return `Device inventory snapshot: ${result.devices.length} device(s), ${result.usb.length} USB item(s), ${result.serial.length} serial port(s).${warningText}`;
1299
1409
  }
1300
1410
  function renderDebugToolScanSummary(result) {
1301
1411
  const available = result.tools.filter((tool) => tool.available).length;
@@ -1313,16 +1423,142 @@ function isApiResponse(value) {
1313
1423
  return isJsonObject(error) && typeof error.code === "string" && typeof error.message === "string";
1314
1424
  }
1315
1425
  async function bridgeGet(path) {
1316
- const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`);
1317
- return await response.json();
1426
+ return await bridgeRequest("GET", path);
1318
1427
  }
1319
1428
  async function bridgePost(path, body) {
1320
- const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
1321
- method: "POST",
1322
- headers: { "content-type": "application/json" },
1323
- body: JSON.stringify(body)
1429
+ return await bridgeRequest("POST", path, body);
1430
+ }
1431
+ async function bridgeRequest(method, path, body) {
1432
+ const bodyText = body === undefined ? "" : JSON.stringify(body);
1433
+ const makeRequest = async () => {
1434
+ const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
1435
+ method,
1436
+ headers: bridgeHeaders(method, path, method === "POST" ? bodyText : "", method === "POST" ? { "content-type": "application/json" } : {}),
1437
+ body: method === "POST" ? bodyText : undefined
1438
+ });
1439
+ return await response.json();
1440
+ };
1441
+ try {
1442
+ return await makeRequest();
1443
+ }
1444
+ catch (error) {
1445
+ if (!isBridgeConnectionFailure(error)) {
1446
+ throw error;
1447
+ }
1448
+ const started = await ensureBridgeStartedForRequest();
1449
+ if (!started.ok) {
1450
+ return started;
1451
+ }
1452
+ return await makeRequest();
1453
+ }
1454
+ }
1455
+ function bridgeHeaders(method, path, bodyText, base = {}) {
1456
+ const token = process.env.EMBED_BRIDGE_TOKEN?.trim();
1457
+ if (!token) {
1458
+ return base;
1459
+ }
1460
+ const headers = {
1461
+ ...base,
1462
+ authorization: `Bearer ${token}`
1463
+ };
1464
+ addBridgeRequestSignature(headers, method, path, bodyText, token);
1465
+ return headers;
1466
+ }
1467
+ function addBridgeRequestSignature(headers, method, pathWithQuery, bodyText, token) {
1468
+ if (process.env.EMBED_BRIDGE_SIGNING === "0") {
1469
+ return;
1470
+ }
1471
+ const timestamp = String(Math.floor(Date.now() / 1000));
1472
+ const nonce = randomBytes(16).toString("hex");
1473
+ const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
1474
+ const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
1475
+ const canonical = cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1476
+ headers["x-embed-key-id"] = keyId;
1477
+ headers["x-embed-timestamp"] = timestamp;
1478
+ headers["x-embed-nonce"] = nonce;
1479
+ headers["x-embed-body-sha256"] = bodySha256;
1480
+ headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
1481
+ }
1482
+ function isBridgeConnectionFailure(error) {
1483
+ const message = error instanceof Error ? error.message : String(error);
1484
+ return message.includes("fetch failed") ||
1485
+ message.includes("ECONNREFUSED") ||
1486
+ message.includes("ECONNRESET") ||
1487
+ message.includes("UND_ERR_SOCKET");
1488
+ }
1489
+ async function ensureBridgeStartedForRequest() {
1490
+ if (process.env.EMBED_BRIDGE_AUTO_START === "0") {
1491
+ return fail("bridge_unavailable", `embed-local-bridge is not running at ${DEFAULT_BRIDGE_URL}.`, {
1492
+ remediation: `Start it with: embed bridge start`
1493
+ });
1494
+ }
1495
+ let bridgeURL;
1496
+ try {
1497
+ bridgeURL = new URL(DEFAULT_BRIDGE_URL);
1498
+ }
1499
+ catch {
1500
+ return fail("bridge_url_invalid", `EMBED_BRIDGE_URL is not a valid URL: ${DEFAULT_BRIDGE_URL}`);
1501
+ }
1502
+ if (!isLocalBridgeURL(bridgeURL)) {
1503
+ return fail("bridge_unavailable", `embed-local-bridge is not reachable at ${DEFAULT_BRIDGE_URL}.`, {
1504
+ remediation: `Start the bridge for that host, or set EMBED_BRIDGE_URL to a local bridge URL.`
1505
+ });
1506
+ }
1507
+ const launcher = await resolveBridgeLauncher();
1508
+ const host = bridgeURL.hostname === "::1" ? "::1" : bridgeURL.hostname || "127.0.0.1";
1509
+ const port = bridgeURL.port || "18083";
1510
+ const env = {
1511
+ ...process.env,
1512
+ EMBED_BRIDGE_HOST: host,
1513
+ EMBED_BRIDGE_PORT: port
1514
+ };
1515
+ const child = spawn(launcher.command, [...launcher.args, "--host", host, "--port", port], {
1516
+ cwd: process.cwd(),
1517
+ detached: true,
1518
+ stdio: "ignore",
1519
+ env
1324
1520
  });
1325
- return await response.json();
1521
+ child.unref();
1522
+ const ready = await waitForBridgeHealth(bridgeURL, 8000);
1523
+ if (!ready.ok) {
1524
+ return ready;
1525
+ }
1526
+ return ok({
1527
+ started: true,
1528
+ bridge_url: DEFAULT_BRIDGE_URL,
1529
+ command: launcher.command
1530
+ });
1531
+ }
1532
+ function isLocalBridgeURL(url) {
1533
+ const host = url.hostname.toLowerCase();
1534
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
1535
+ }
1536
+ async function waitForBridgeHealth(bridgeURL, timeoutMs) {
1537
+ const deadline = Date.now() + timeoutMs;
1538
+ let lastError = "";
1539
+ while (Date.now() < deadline) {
1540
+ try {
1541
+ const response = await fetch(new URL("/healthz", bridgeURL), {
1542
+ headers: bridgeHeaders("GET", "/healthz", "")
1543
+ });
1544
+ const parsed = await response.json();
1545
+ if (parsed.ok) {
1546
+ return parsed;
1547
+ }
1548
+ lastError = parsed.error?.message ?? `HTTP ${response.status}`;
1549
+ }
1550
+ catch (error) {
1551
+ lastError = error instanceof Error ? error.message : String(error);
1552
+ }
1553
+ await delay(100);
1554
+ }
1555
+ return fail("bridge_start_failed", `embed-local-bridge did not become healthy at ${DEFAULT_BRIDGE_URL}.`, {
1556
+ remediation: `Run embed bridge start in a separate terminal and retry.`,
1557
+ details: { last_error: lastError }
1558
+ });
1559
+ }
1560
+ function delay(ms) {
1561
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
1326
1562
  }
1327
1563
  async function cloudGet(path) {
1328
1564
  return await cloudRequest("GET", path);
@@ -1333,16 +1569,23 @@ async function cloudPost(path, body) {
1333
1569
  async function cloudDownloadArtifact(artifactId, outputPath) {
1334
1570
  try {
1335
1571
  const headers = {};
1336
- const token = await cloudAuthToken();
1337
- if (token) {
1338
- headers.authorization = `Bearer ${token}`;
1572
+ const auth = await cloudAuthConfig();
1573
+ if (auth.token) {
1574
+ if (auth.device) {
1575
+ const integrity = await validateLocalDeviceIntegrity(auth.device);
1576
+ if (!integrity.ok) {
1577
+ return integrity;
1578
+ }
1579
+ }
1580
+ headers.authorization = `Bearer ${auth.token}`;
1581
+ addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", auth.token, auth.device);
1339
1582
  }
1340
1583
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/artifacts/${encodeURIComponent(artifactId)}/download`, {
1341
1584
  headers: Object.keys(headers).length > 0 ? headers : undefined
1342
1585
  });
1343
1586
  if (!response.ok) {
1344
1587
  const parsed = await parseErrorResponse(response);
1345
- return parsed ?? fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
1588
+ return parsed ? enrichCloudAuthFailure(parsed, Boolean(auth.token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
1346
1589
  }
1347
1590
  const bytes = Buffer.from(await response.arrayBuffer());
1348
1591
  const expectedSha256 = response.headers.get("x-embed-artifact-sha256")?.trim();
@@ -1371,19 +1614,28 @@ async function cloudDownloadArtifact(artifactId, outputPath) {
1371
1614
  async function cloudRequest(method, path, body) {
1372
1615
  try {
1373
1616
  const headers = {};
1374
- if (body !== undefined) {
1617
+ const bodyText = body === undefined ? "" : JSON.stringify(body);
1618
+ if (bodyText) {
1375
1619
  headers["content-type"] = "application/json";
1376
1620
  }
1377
- const token = await cloudAuthToken();
1378
- if (token) {
1379
- headers.authorization = `Bearer ${token}`;
1621
+ const auth = await cloudAuthConfig();
1622
+ if (auth.token) {
1623
+ if (auth.device) {
1624
+ const integrity = await validateLocalDeviceIntegrity(auth.device);
1625
+ if (!integrity.ok) {
1626
+ return integrity;
1627
+ }
1628
+ }
1629
+ headers.authorization = `Bearer ${auth.token}`;
1630
+ addCloudRequestSignature(headers, method, path, bodyText, auth.token, auth.device);
1380
1631
  }
1381
1632
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}${path}`, {
1382
1633
  method,
1383
1634
  headers: Object.keys(headers).length > 0 ? headers : undefined,
1384
- body: body === undefined ? undefined : JSON.stringify(body)
1635
+ body: body === undefined ? undefined : bodyText
1385
1636
  });
1386
- return await response.json();
1637
+ const parsed = await response.json();
1638
+ return enrichCloudAuthFailure(parsed, Boolean(auth.token));
1387
1639
  }
1388
1640
  catch (error) {
1389
1641
  return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
@@ -1391,6 +1643,129 @@ async function cloudRequest(method, path, body) {
1391
1643
  });
1392
1644
  }
1393
1645
  }
1646
+ async function validateLocalDeviceIntegrity(device) {
1647
+ const current = await localHardwareFingerprint();
1648
+ if (current.fingerprint_hash === device.fingerprint_hash) {
1649
+ return ok(undefined);
1650
+ }
1651
+ return fail("tool_integrity_check_failed", TOOL_INTEGRITY_RELOGIN_MESSAGE, {
1652
+ remediation: [
1653
+ "当前 Embed Labs CLI/插件配置绑定的电脑与本机硬件唯一码不一致。",
1654
+ TOOL_INTEGRITY_RELOGIN_MESSAGE,
1655
+ "如果账号设备数量已达上限,请先在原电脑或用户后台撤销旧设备。"
1656
+ ].join("\n"),
1657
+ details: {
1658
+ expected_fingerprint_hash: device.fingerprint_hash,
1659
+ current_fingerprint_hash: current.fingerprint_hash,
1660
+ platform: current.platform,
1661
+ arch: current.arch,
1662
+ fingerprint_source: current.source
1663
+ }
1664
+ });
1665
+ }
1666
+ function enrichCloudAuthFailure(response, hadToken) {
1667
+ if (response.ok) {
1668
+ return response;
1669
+ }
1670
+ if (response.error.code.startsWith("device_") || response.error.code.startsWith("request_signature_")) {
1671
+ return fail(response.error.code, response.error.message, {
1672
+ remediation: [
1673
+ "This computer is not fully registered for the configured Embed Labs API Token.",
1674
+ "Run: embedlabs auth login --token <your_token>",
1675
+ "Then verify with: embedlabs auth device status",
1676
+ "If the account already has too many devices, revoke one with: embedlabs auth device revoke <device_id>"
1677
+ ].join("\n"),
1678
+ details: response.error.details
1679
+ });
1680
+ }
1681
+ if (response.error.code !== "unauthorized") {
1682
+ return response;
1683
+ }
1684
+ if (!hadToken) {
1685
+ return fail("auth_token_missing", "Embed Labs API Token is not configured. Register or sign in, create an API Token, then configure it locally before using cloud and plugin services.", {
1686
+ remediation: cloudAuthSetupRemediation(),
1687
+ details: cloudAuthSetupDetails()
1688
+ });
1689
+ }
1690
+ return fail("auth_token_rejected", "The configured Embed Labs API Token was rejected by the server. Recreate or copy a fresh token from the dashboard and sign in again.", {
1691
+ remediation: cloudAuthSetupRemediation(),
1692
+ details: cloudAuthSetupDetails()
1693
+ });
1694
+ }
1695
+ function cloudAuthSetupRemediation() {
1696
+ return [
1697
+ `1. Open ${DEFAULT_DASHBOARD_URL} and register or sign in.`,
1698
+ "2. Create or copy your Embed Labs API Token from the user dashboard.",
1699
+ "3. Run: embedlabs auth login --token <your_token>",
1700
+ "4. For automation, set: EMBED_API_TOKEN=<your_token>",
1701
+ "5. Verify with: embedlabs auth status"
1702
+ ].join("\n");
1703
+ }
1704
+ function cloudAuthSetupDetails() {
1705
+ return {
1706
+ dashboard_url: DEFAULT_DASHBOARD_URL,
1707
+ login_command: "embedlabs auth login --token <your_token>",
1708
+ env_var: "EMBED_API_TOKEN",
1709
+ auth_status_command: "embedlabs auth status",
1710
+ auth_file: DEFAULT_AUTH_FILE
1711
+ };
1712
+ }
1713
+ function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token, device) {
1714
+ if (process.env.EMBED_CLOUD_API_SIGNING === "0") {
1715
+ return;
1716
+ }
1717
+ const timestamp = String(Math.floor(Date.now() / 1000));
1718
+ const nonce = randomBytes(16).toString("hex");
1719
+ const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
1720
+ const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
1721
+ const canonical = device
1722
+ ? cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, EMBED_CLIENT_NAME, EMBED_CLIENT_VERSION)
1723
+ : cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1724
+ headers["x-embed-key-id"] = keyId;
1725
+ headers["x-embed-timestamp"] = timestamp;
1726
+ headers["x-embed-nonce"] = nonce;
1727
+ headers["x-embed-body-sha256"] = bodySha256;
1728
+ if (device) {
1729
+ headers["x-embed-signature-version"] = "v2";
1730
+ headers["x-embed-device-id"] = device.device_id;
1731
+ headers["x-embed-device-fingerprint-sha256"] = device.fingerprint_hash;
1732
+ headers["x-embed-client-name"] = EMBED_CLIENT_NAME;
1733
+ headers["x-embed-client-version"] = EMBED_CLIENT_VERSION;
1734
+ headers["x-embed-device-signature"] = signCrypto(null, Buffer.from(canonical), device.private_key_pem).toString("base64url");
1735
+ }
1736
+ headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
1737
+ }
1738
+ function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256) {
1739
+ return [
1740
+ method.toUpperCase(),
1741
+ normalizeCloudPathForSignature(pathWithQuery),
1742
+ timestamp,
1743
+ nonce,
1744
+ bodySha256
1745
+ ].join("\n");
1746
+ }
1747
+ function cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, clientName, clientVersion) {
1748
+ return [
1749
+ method.toUpperCase(),
1750
+ normalizeCloudPathForSignature(pathWithQuery),
1751
+ timestamp,
1752
+ nonce,
1753
+ bodySha256,
1754
+ device.device_id,
1755
+ device.fingerprint_hash,
1756
+ clientName,
1757
+ clientVersion
1758
+ ].join("\n");
1759
+ }
1760
+ function normalizeCloudPathForSignature(pathWithQuery) {
1761
+ try {
1762
+ const parsed = new URL(pathWithQuery, "http://embed.local");
1763
+ return `${parsed.pathname}${parsed.search}`;
1764
+ }
1765
+ catch {
1766
+ return pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`;
1767
+ }
1768
+ }
1394
1769
  async function pluginList(parsed) {
1395
1770
  const releaseDir = stringFlag(parsed, "release-dir");
1396
1771
  const manifest = releaseDir ? await readPluginReleaseManifest(releaseDir) : undefined;
@@ -1490,13 +1865,112 @@ async function pluginInstall(parsed) {
1490
1865
  await rm(tempDir, { recursive: true, force: true });
1491
1866
  }
1492
1867
  }
1868
+ async function pluginUpdateCheck(parsed) {
1869
+ const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
1870
+ if (unknownFlag) {
1871
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
1872
+ }
1873
+ const unexpected = parsed.command.slice(3);
1874
+ if (unexpected.length > 0) {
1875
+ return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
1876
+ }
1877
+ const remoteManifest = await fetchRemotePluginManifest(parsed);
1878
+ if (!remoteManifest.ok) {
1879
+ return remoteManifest;
1880
+ }
1881
+ const manifest = remoteManifest.data;
1882
+ const codexPackage = manifest.packages?.find((item) => item.id === "codex-embed-labs");
1883
+ const opencodePackage = manifest.packages?.find((item) => item.id === "opencode-embed-labs");
1884
+ const codexTarget = join(codexPluginTargetRoot(parsed, true), CODEX_PLUGIN_NAME);
1885
+ const openCodeTarget = openCodePluginTargetRoot(parsed, true);
1886
+ return ok({
1887
+ release_url: pluginReleaseBaseUrl(parsed),
1888
+ latest_version: manifest.version,
1889
+ release_notes: normalizedReleaseNotes(manifest.release_notes),
1890
+ plugins: [
1891
+ await pluginUpdateItem({
1892
+ id: "codex",
1893
+ displayName: "Embed Labs Codex plugin",
1894
+ targetPath: codexTarget,
1895
+ installedVersion: await installedCodexPluginVersion(codexTarget),
1896
+ latestVersion: codexPackage?.version ?? manifest.version,
1897
+ releaseFile: codexPackage?.file,
1898
+ updateCommand: "embedlabs plugin update codex"
1899
+ }),
1900
+ await pluginUpdateItem({
1901
+ id: "opencode",
1902
+ displayName: "Embed Labs OpenCode plugin",
1903
+ targetPath: openCodeTarget,
1904
+ installedVersion: await installedOpenCodePluginVersion(openCodeTarget),
1905
+ latestVersion: opencodePackage?.version ?? manifest.version,
1906
+ releaseFile: opencodePackage?.file,
1907
+ updateCommand: "embedlabs plugin update opencode"
1908
+ })
1909
+ ]
1910
+ });
1911
+ }
1912
+ function normalizedReleaseNotes(notes) {
1913
+ if (!Array.isArray(notes)) {
1914
+ return [];
1915
+ }
1916
+ return notes
1917
+ .filter((item) => typeof item === "string" && item.trim().length > 0)
1918
+ .map((item) => item.trim());
1919
+ }
1920
+ async function pluginUpdate(parsed) {
1921
+ const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
1922
+ if (unknownFlag) {
1923
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_USAGE}`);
1924
+ }
1925
+ const target = parsed.command[2];
1926
+ if (!target || !["codex", "opencode", "all"].includes(target)) {
1927
+ return fail("invalid_args", PLUGIN_UPDATE_USAGE);
1928
+ }
1929
+ const unexpected = parsed.command.slice(3);
1930
+ if (unexpected.length > 0) {
1931
+ return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_USAGE}`);
1932
+ }
1933
+ const installParsed = {
1934
+ ...parsed,
1935
+ command: ["plugin", "install", target],
1936
+ flags: { ...parsed.flags, force: true }
1937
+ };
1938
+ return await pluginInstall(installParsed);
1939
+ }
1940
+ async function pluginUpdateItem(input) {
1941
+ const installed = !!input.installedVersion;
1942
+ const updateAvailable = !!input.latestVersion && input.installedVersion !== input.latestVersion;
1943
+ const notes = [];
1944
+ if (!installed) {
1945
+ notes.push("Plugin is not installed in the selected target.");
1946
+ }
1947
+ else if (updateAvailable) {
1948
+ notes.push("A newer plugin release is available. Run the update command, then restart Codex/OpenCode.");
1949
+ }
1950
+ else {
1951
+ notes.push("Plugin is up to date for the selected release channel.");
1952
+ }
1953
+ return {
1954
+ id: input.id,
1955
+ display_name: input.displayName,
1956
+ installed,
1957
+ installed_version: input.installedVersion,
1958
+ latest_version: input.latestVersion,
1959
+ update_available: updateAvailable,
1960
+ target_path: input.targetPath,
1961
+ release_file: input.releaseFile,
1962
+ update_command: input.updateCommand,
1963
+ notes
1964
+ };
1965
+ }
1493
1966
  async function installCodexPlugin(parsed, context) {
1494
1967
  const source = await resolveCodexPluginSource(context);
1495
1968
  if (!source.ok) {
1496
1969
  return source;
1497
1970
  }
1498
1971
  const targetRoot = codexPluginTargetRoot(parsed, context.installingAll);
1499
- const targetPath = join(targetRoot, "embed-labs");
1972
+ const targetPath = join(targetRoot, CODEX_PLUGIN_NAME);
1973
+ const legacyCleanup = await cleanupLegacyCodexPluginRemnants(targetRoot);
1500
1974
  if (await pathExists(targetPath) && !booleanFlag(parsed, "force")) {
1501
1975
  return fail("plugin_already_installed", `Codex plugin already exists at ${targetPath}.`, {
1502
1976
  remediation: "Pass --force to replace it, or pass --codex-target/--target to install into a different directory."
@@ -1506,16 +1980,24 @@ async function installCodexPlugin(parsed, context) {
1506
1980
  await mkdir(targetRoot, { recursive: true });
1507
1981
  await cp(source.data.sourcePath, targetPath, { recursive: true });
1508
1982
  const mcpRegistration = await maybeRegisterCodexMcp(parsed, targetRoot, targetPath);
1983
+ const marketplaceRegistration = await maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath);
1509
1984
  return ok({
1510
1985
  id: "codex",
1511
1986
  target_path: targetPath,
1512
1987
  source: source.data.sourceLabel,
1513
1988
  version: source.data.version,
1514
1989
  command_hint: mcpRegistration.registered
1515
- ? "Codex MCP was registered. Start a new Codex session to reload tools."
1990
+ ? (marketplaceRegistration.registered
1991
+ ? "Codex MCP and plugin marketplace entry were registered. Fully restart Codex to reload @Embed Labs."
1992
+ : "Codex MCP was registered. Start a new Codex session to reload tools.")
1516
1993
  : mcpRegistration.hint,
1994
+ warning: legacyCodexCleanupWarning(legacyCleanup),
1517
1995
  mcp_registered: mcpRegistration.registered,
1518
- mcp_warning: mcpRegistration.warning
1996
+ mcp_warning: mcpRegistration.warning,
1997
+ marketplace_registered: marketplaceRegistration.registered,
1998
+ marketplace_path: marketplaceRegistration.marketplacePath,
1999
+ marketplace_warning: marketplaceRegistration.warning,
2000
+ cleanup: legacyCleanup
1519
2001
  });
1520
2002
  }
1521
2003
  async function installOpenCodePlugin(parsed, context) {
@@ -1524,8 +2006,10 @@ async function installOpenCodePlugin(parsed, context) {
1524
2006
  return source;
1525
2007
  }
1526
2008
  const targetRoot = openCodePluginTargetRoot(parsed, context.installingAll);
1527
- const wrapperPath = join(targetRoot, "plugins", "development-board-toolchain.js");
1528
- if (await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
2009
+ const globalInstall = isGlobalOpenCodeRoot(targetRoot);
2010
+ const wrapperPath = join(targetRoot, "plugins", "embed-labs.js");
2011
+ const legacyCleanup = await cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall);
2012
+ if (!globalInstall && await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
1529
2013
  return fail("plugin_already_installed", `OpenCode plugin wrapper already exists at ${wrapperPath}.`, {
1530
2014
  remediation: "Pass --force to replace it, or pass --opencode-target/--target to install into a different directory."
1531
2015
  });
@@ -1551,15 +2035,25 @@ async function installOpenCodePlugin(parsed, context) {
1551
2035
  });
1552
2036
  }
1553
2037
  await ensureOpenCodeInstallPackageJson(targetRoot);
1554
- await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
2038
+ if (globalInstall) {
2039
+ await rm(wrapperPath, { force: true });
2040
+ legacyCleanup.legacy_removed_config_entries?.push(...await ensureOpenCodeGlobalPluginConfig());
2041
+ }
2042
+ else {
2043
+ await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
2044
+ }
1555
2045
  const duplicateWarning = await openCodeDuplicatePluginWarning(targetRoot);
2046
+ const cleanupWarning = legacyOpenCodeCleanupWarning(legacyCleanup);
1556
2047
  return ok({
1557
2048
  id: "opencode",
1558
2049
  target_path: targetRoot,
1559
2050
  source: source.data.sourceLabel,
1560
2051
  version: source.data.version,
1561
- command_hint: "Start OpenCode from the project containing this .opencode directory.",
1562
- warning: duplicateWarning
2052
+ command_hint: globalInstall
2053
+ ? "Restart OpenCode so the global embed-labs package plugin is reloaded."
2054
+ : "Start OpenCode from the project containing this .opencode directory.",
2055
+ warning: combineWarnings(cleanupWarning, duplicateWarning),
2056
+ cleanup: legacyCleanup
1563
2057
  });
1564
2058
  }
1565
2059
  async function resolveCodexPluginSource(context) {
@@ -1773,7 +2267,458 @@ function openCodePluginTargetRoot(parsed, installingAll) {
1773
2267
  return resolve(target ?? defaultOpenCodeRoot());
1774
2268
  }
1775
2269
  function defaultCodexPluginRoot() {
1776
- return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "plugins");
2270
+ return join(defaultCodexHome(), "plugins");
2271
+ }
2272
+ function defaultCodexHome() {
2273
+ return resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
2274
+ }
2275
+ function codexConfigPath() {
2276
+ return join(defaultCodexHome(), "config.toml");
2277
+ }
2278
+ async function cleanupLegacyCodexPluginRemnants(targetRoot) {
2279
+ const removedPaths = [];
2280
+ const removedConfigTables = [];
2281
+ const warnings = [];
2282
+ const stoppedProcesses = await stopLegacyCodexPluginProcesses(warnings);
2283
+ const legacyPaths = [
2284
+ join(targetRoot, "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME)
2285
+ ];
2286
+ for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
2287
+ legacyPaths.push(join(targetRoot, "cache", marketplaceName, CODEX_PLUGIN_NAME));
2288
+ }
2289
+ for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
2290
+ legacyPaths.push(join(targetRoot, pluginName));
2291
+ legacyPaths.push(join(targetRoot, "cache", pluginName));
2292
+ for (const marketplaceName of [CODEX_MARKETPLACE_NAME, ...LEGACY_CODEX_MARKETPLACE_NAMES]) {
2293
+ legacyPaths.push(join(targetRoot, "cache", marketplaceName, pluginName));
2294
+ }
2295
+ }
2296
+ legacyPaths.push(...await discoverLegacyCodexCachePaths(targetRoot));
2297
+ if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
2298
+ legacyPaths.push(join(defaultCodexHome(), ".tmp", "plugins"), join(defaultCodexHome(), ".tmp", "plugins.sha"), join(defaultCodexHome(), "ambient-suggestions"), join(defaultCodexHome(), "skills", "dbt-agent"), join(defaultCodexHome(), "skills", "development-board-toolchain-dev"), join(defaultCodexHome(), "memories", "skills", "dbt-agent-live-board-ops"), join(defaultCodexHome(), "memories", "skills", "dbt-agent-platform-plugin-maintenance"));
2299
+ legacyPaths.push(...legacyCodexLocalMarketplacePaths(), ...await discoverLegacyHomeAgentsMarketplacePaths(warnings), ...await discoverLegacyCodexProjectMarketplacePaths(warnings), ...legacyDevelopmentBoardRuntimePluginPaths());
2300
+ }
2301
+ for (const candidate of legacyPaths) {
2302
+ try {
2303
+ if (await pathExists(candidate)) {
2304
+ await rm(candidate, { recursive: true, force: true });
2305
+ removedPaths.push(candidate);
2306
+ }
2307
+ }
2308
+ catch (error) {
2309
+ warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2310
+ }
2311
+ }
2312
+ if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
2313
+ const configPath = codexConfigPath();
2314
+ try {
2315
+ if (await pathExists(configPath)) {
2316
+ const current = await readFile(configPath, "utf8");
2317
+ const updated = removeLegacyCodexConfigTables(current);
2318
+ if (updated.text !== current) {
2319
+ await writeFile(configPath, updated.text, "utf8");
2320
+ }
2321
+ removedConfigTables.push(...updated.removedTables);
2322
+ }
2323
+ }
2324
+ catch (error) {
2325
+ warnings.push(`Could not update ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
2326
+ }
2327
+ }
2328
+ const removedHistoryEntries = await cleanupLegacyCodexTextState(warnings);
2329
+ return {
2330
+ legacy_removed_paths: Array.from(new Set(removedPaths)),
2331
+ legacy_removed_config_tables: removedConfigTables,
2332
+ legacy_removed_history_entries: removedHistoryEntries,
2333
+ legacy_stopped_processes: stoppedProcesses,
2334
+ ...(warnings.length > 0 ? { warnings } : {})
2335
+ };
2336
+ }
2337
+ async function stopLegacyCodexPluginProcesses(warnings) {
2338
+ if (process.platform === "win32")
2339
+ return 0;
2340
+ try {
2341
+ const ps = await runLocalProcess("ps", ["-axo", "pid=,command="]);
2342
+ if (ps.code !== 0)
2343
+ return 0;
2344
+ let stopped = 0;
2345
+ for (const line of ps.stdout.split("\n")) {
2346
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
2347
+ if (!match)
2348
+ continue;
2349
+ const pid = Number(match[1]);
2350
+ const command = match[2] || "";
2351
+ if (!isLegacyCodexPluginProcess(command))
2352
+ continue;
2353
+ try {
2354
+ process.kill(pid, "SIGTERM");
2355
+ stopped += 1;
2356
+ }
2357
+ catch (error) {
2358
+ warnings.push(`Could not stop legacy Codex plugin process ${pid}: ${error instanceof Error ? error.message : String(error)}`);
2359
+ }
2360
+ }
2361
+ return stopped;
2362
+ }
2363
+ catch (error) {
2364
+ warnings.push(`Could not scan legacy Codex plugin processes: ${error instanceof Error ? error.message : String(error)}`);
2365
+ return 0;
2366
+ }
2367
+ }
2368
+ function isLegacyCodexPluginProcess(command) {
2369
+ const trimmed = command.trim();
2370
+ return /^\/.*\/dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
2371
+ || /^dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
2372
+ || legacyDevelopmentBoardRuntimeProcessPatterns().some((pattern) => pattern.test(trimmed));
2373
+ }
2374
+ function legacyDevelopmentBoardRuntimeProcessPatterns() {
2375
+ const home = escapeRegExp(homedir());
2376
+ return [
2377
+ new RegExp(`^${home}/Library/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
2378
+ new RegExp(`^${home}/Library/Application Support/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
2379
+ new RegExp(`^${home}/Library/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
2380
+ new RegExp(`^${home}/Library/Application Support/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
2381
+ new RegExp(`^${home}/.*?/DBT-Agent\\.app/Contents/MacOS/DBT-Agent(?:\\s|$)`)
2382
+ ];
2383
+ }
2384
+ function legacyDevelopmentBoardRuntimePluginPaths() {
2385
+ return [
2386
+ join(homedir(), "Library", "development-board-toolchain", "runtime", "editor_plugins"),
2387
+ join(homedir(), "Library", "development-board-toolchain", "runtime", "opencode_plugin"),
2388
+ join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "editor_plugins"),
2389
+ join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "opencode_plugin")
2390
+ ];
2391
+ }
2392
+ function legacyCodexLocalMarketplacePaths() {
2393
+ return Array.from(LEGACY_CODEX_MARKETPLACE_NAMES)
2394
+ .filter((name) => name !== CODEX_MARKETPLACE_NAME)
2395
+ .map((name) => join(defaultCodexHome(), "local-marketplaces", name));
2396
+ }
2397
+ async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
2398
+ const paths = [];
2399
+ const pluginRoot = join(homedir(), ".agents", "plugins");
2400
+ try {
2401
+ const entries = await readdir(pluginRoot, { withFileTypes: true });
2402
+ for (const entry of entries) {
2403
+ if (!entry.isFile() || !entry.name.startsWith("marketplace.json"))
2404
+ continue;
2405
+ const filePath = join(pluginRoot, entry.name);
2406
+ try {
2407
+ const current = await readFile(filePath, "utf8");
2408
+ if (isLegacyHomeAgentsMarketplace(current)) {
2409
+ paths.push(filePath);
2410
+ }
2411
+ }
2412
+ catch (error) {
2413
+ warnings.push(`Could not inspect legacy Codex home marketplace ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2414
+ }
2415
+ }
2416
+ }
2417
+ catch {
2418
+ return paths;
2419
+ }
2420
+ return paths;
2421
+ }
2422
+ function isLegacyHomeAgentsMarketplace(text) {
2423
+ return marketplaceTextHasLegacyCodexPlugin(text);
2424
+ }
2425
+ async function discoverLegacyCodexProjectMarketplacePaths(warnings) {
2426
+ const configPath = codexConfigPath();
2427
+ let text = "";
2428
+ try {
2429
+ text = await readFile(configPath, "utf8");
2430
+ }
2431
+ catch {
2432
+ return [];
2433
+ }
2434
+ const paths = new Set();
2435
+ for (const projectPath of legacyCodexProjectPathsFromConfig(text)) {
2436
+ for (const candidate of [
2437
+ join(projectPath, ".agents", "plugins", "marketplace.json"),
2438
+ join(projectPath, "platform_plugin", ".agents", "plugins", "marketplace.json"),
2439
+ join(projectPath, "platform_plugins", "codex_plugin", ".agents", "plugins", "marketplace.json")
2440
+ ]) {
2441
+ try {
2442
+ if (!await pathExists(candidate))
2443
+ continue;
2444
+ const current = await readFile(candidate, "utf8");
2445
+ if (marketplaceTextHasLegacyCodexPlugin(current)) {
2446
+ paths.add(candidate);
2447
+ }
2448
+ }
2449
+ catch (error) {
2450
+ warnings.push(`Could not inspect legacy Codex project marketplace ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2451
+ }
2452
+ }
2453
+ }
2454
+ return Array.from(paths);
2455
+ }
2456
+ function legacyCodexProjectPathsFromConfig(text) {
2457
+ const paths = [];
2458
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2459
+ for (const line of lines) {
2460
+ const table = parseTomlTableHeader(line);
2461
+ const match = table ? /^projects\."([^"]+)"$/.exec(table) : undefined;
2462
+ if (match?.[1] && /DBT-Agent-Project|development-board-toolchain|dbt-agent/i.test(match[1])) {
2463
+ paths.push(match[1].replace(/\\"/g, '"'));
2464
+ }
2465
+ }
2466
+ return paths;
2467
+ }
2468
+ function marketplaceTextHasLegacyCodexPlugin(text) {
2469
+ try {
2470
+ const parsed = JSON.parse(text);
2471
+ const marketplaceName = typeof parsed.name === "string" ? parsed.name : "";
2472
+ const marketplaceDisplayName = typeof parsed.interface?.displayName === "string" ? parsed.interface.displayName : "";
2473
+ const marketplaceLooksLegacy = legacyTextHasCodexPluginResidue(marketplaceName)
2474
+ || legacyTextHasCodexPluginResidue(marketplaceDisplayName)
2475
+ || LEGACY_CODEX_MARKETPLACE_NAMES.has(marketplaceName);
2476
+ return (parsed.plugins ?? []).some((plugin) => {
2477
+ const values = [
2478
+ plugin.name,
2479
+ plugin.category,
2480
+ plugin.source?.path,
2481
+ plugin.interface?.displayName
2482
+ ].filter((value) => typeof value === "string");
2483
+ return values.some(legacyTextHasCodexPluginResidue) || marketplaceLooksLegacy && values.some((value) => /embed-labs|dbt|development-board/i.test(value));
2484
+ });
2485
+ }
2486
+ catch {
2487
+ return legacyTextHasCodexPluginResidue(text);
2488
+ }
2489
+ }
2490
+ async function discoverLegacyCodexCachePaths(targetRoot) {
2491
+ const paths = [];
2492
+ const cacheRoot = join(targetRoot, "cache");
2493
+ try {
2494
+ const marketplaces = await readdir(cacheRoot, { withFileTypes: true });
2495
+ for (const entry of marketplaces) {
2496
+ if (!entry.isDirectory())
2497
+ continue;
2498
+ if (LEGACY_CODEX_MARKETPLACE_NAMES.has(entry.name)) {
2499
+ paths.push(join(cacheRoot, entry.name, CODEX_PLUGIN_NAME));
2500
+ }
2501
+ for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
2502
+ paths.push(join(cacheRoot, entry.name, pluginName));
2503
+ }
2504
+ }
2505
+ }
2506
+ catch {
2507
+ return paths;
2508
+ }
2509
+ return paths;
2510
+ }
2511
+ async function cleanupLegacyCodexTextState(warnings) {
2512
+ let removed = 0;
2513
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "history.jsonl"), warnings);
2514
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "session_index.jsonl"), warnings);
2515
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "rules", "default.rules"), warnings);
2516
+ return removed;
2517
+ }
2518
+ async function cleanupLegacyCodexTextFile(filePath, warnings) {
2519
+ try {
2520
+ if (!await pathExists(filePath))
2521
+ return 0;
2522
+ const current = await readFile(filePath, "utf8");
2523
+ const lines = current.split("\n");
2524
+ let removed = 0;
2525
+ const kept = lines.filter((line) => {
2526
+ if (!line)
2527
+ return true;
2528
+ if (isLegacyCodexHistoryMention(line)) {
2529
+ removed += 1;
2530
+ return false;
2531
+ }
2532
+ return true;
2533
+ });
2534
+ if (removed > 0) {
2535
+ await writeFile(filePath, kept.join("\n"), "utf8");
2536
+ }
2537
+ return removed;
2538
+ }
2539
+ catch (error) {
2540
+ warnings.push(`Could not clean Codex legacy text state ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2541
+ return 0;
2542
+ }
2543
+ }
2544
+ function isLegacyCodexHistoryMention(line) {
2545
+ return line.includes("plugin://dbt-agent@plugins")
2546
+ || line.includes("plugin://dbt-agent@embed-labs")
2547
+ || line.includes("development-board-toolchain-dev")
2548
+ || line.includes("development-board-toolchain")
2549
+ || /plugin:\/\/Dbt Agent@/i.test(line)
2550
+ || /plugin:\/\/deve@/i.test(line)
2551
+ || /dbt-agent/i.test(line);
2552
+ }
2553
+ function removeLegacyCodexConfigTables(text) {
2554
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2555
+ const output = [];
2556
+ const removedTables = [];
2557
+ let skipping = false;
2558
+ for (const line of lines) {
2559
+ const table = parseTomlTableHeader(line);
2560
+ if (table) {
2561
+ skipping = isLegacyCodexConfigTable(table);
2562
+ if (skipping) {
2563
+ removedTables.push(table);
2564
+ continue;
2565
+ }
2566
+ }
2567
+ if (!skipping) {
2568
+ output.push(line);
2569
+ }
2570
+ }
2571
+ return { text: output.join("").replace(/\n{3,}/g, "\n\n"), removedTables };
2572
+ }
2573
+ function parseTomlTableHeader(line) {
2574
+ const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line);
2575
+ return match?.[1]?.trim();
2576
+ }
2577
+ function isLegacyCodexConfigTable(table) {
2578
+ return /^plugins\."dbt-agent@[^"]+"$/.test(table)
2579
+ || /^plugins\."Dbt Agent@[^"]+"$/i.test(table)
2580
+ || /^plugins\."deve@[^"]+"$/i.test(table)
2581
+ || isLegacyEmbedLabsCodexMarketplaceConfigTable(table)
2582
+ || table === "mcp_servers.dbt-agent"
2583
+ || table.startsWith("mcp_servers.dbt-agent.")
2584
+ || table === 'mcp_servers."dbt-agent"'
2585
+ || table.startsWith('mcp_servers."dbt-agent".')
2586
+ || table === "mcp_servers.deve"
2587
+ || table.startsWith("mcp_servers.deve.")
2588
+ || /^projects\."[^"]*\/DBT-Agent-Project(?:\/[^"]*)?"$/.test(table);
2589
+ }
2590
+ function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
2591
+ for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
2592
+ if (table === `marketplaces.${marketplaceName}`) {
2593
+ return true;
2594
+ }
2595
+ if (table === `plugins."${CODEX_PLUGIN_NAME}@${marketplaceName}"`) {
2596
+ return true;
2597
+ }
2598
+ }
2599
+ return false;
2600
+ }
2601
+ function legacyTextHasCodexPluginResidue(value) {
2602
+ return /dbt-agent|Dbt Agent|development-board-toolchain|development-board-toolchain-dev/i.test(value)
2603
+ || value === "deve"
2604
+ || value.replace(/\\/g, "/").includes("/plugins/deve")
2605
+ || value.includes("plugin://deve@");
2606
+ }
2607
+ function legacyCodexCleanupWarning(cleanup) {
2608
+ const parts = [];
2609
+ if (cleanup.legacy_removed_paths.length > 0) {
2610
+ parts.push(`removed ${cleanup.legacy_removed_paths.length} stale/legacy Codex plugin path(s)`);
2611
+ }
2612
+ if (cleanup.legacy_removed_config_tables?.length) {
2613
+ parts.push(`removed ${cleanup.legacy_removed_config_tables.length} legacy Codex config table(s)`);
2614
+ }
2615
+ if (cleanup.legacy_removed_history_entries) {
2616
+ parts.push(`removed ${cleanup.legacy_removed_history_entries} legacy Codex text-state mention(s)`);
2617
+ }
2618
+ if (cleanup.legacy_stopped_processes) {
2619
+ parts.push(`stopped ${cleanup.legacy_stopped_processes} legacy Codex plugin process(es)`);
2620
+ }
2621
+ if (cleanup.warnings?.length) {
2622
+ parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
2623
+ }
2624
+ return parts.length > 0 ? `Codex plugin cleanup: ${parts.join(", ")}.` : undefined;
2625
+ }
2626
+ async function cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall) {
2627
+ const removedPaths = [];
2628
+ const warnings = [];
2629
+ const legacyPaths = [
2630
+ join(targetRoot, "plugins", "development-board-toolchain.js"),
2631
+ join(targetRoot, "plugins", "development-board-toolchain-dev.js"),
2632
+ join(targetRoot, "plugins", "dbt-agent.js"),
2633
+ join(targetRoot, "plugins", "Dbt Agent.js"),
2634
+ join(targetRoot, "plugins", "deve.js"),
2635
+ join(targetRoot, "plugins", "deve"),
2636
+ join(targetRoot, "node_modules", "development-board-toolchain"),
2637
+ join(targetRoot, "node_modules", "development-board-toolchain-dev"),
2638
+ join(targetRoot, "node_modules", "dbt-agent")
2639
+ ];
2640
+ if (globalInstall) {
2641
+ legacyPaths.push(join(targetRoot, "plugins", "embed-labs.js"));
2642
+ legacyPaths.push(...await discoverLegacyOpenCodeBackupPaths(targetRoot, warnings));
2643
+ legacyPaths.push(...await discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings));
2644
+ }
2645
+ for (const candidate of legacyPaths) {
2646
+ try {
2647
+ if (await pathExists(candidate)) {
2648
+ await rm(candidate, { recursive: true, force: true });
2649
+ removedPaths.push(candidate);
2650
+ }
2651
+ }
2652
+ catch (error) {
2653
+ warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2654
+ }
2655
+ }
2656
+ return {
2657
+ legacy_removed_paths: removedPaths,
2658
+ legacy_removed_config_entries: [],
2659
+ ...(warnings.length > 0 ? { warnings } : {})
2660
+ };
2661
+ }
2662
+ async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
2663
+ const paths = [];
2664
+ try {
2665
+ const entries = await readdir(targetRoot, { withFileTypes: true });
2666
+ for (const entry of entries) {
2667
+ if (!entry.isFile() || !entry.name.includes(".bak"))
2668
+ continue;
2669
+ const filePath = join(targetRoot, entry.name);
2670
+ try {
2671
+ const current = await readFile(filePath, "utf8");
2672
+ if (legacyTextHasCodexPluginResidue(current)) {
2673
+ paths.push(filePath);
2674
+ }
2675
+ }
2676
+ catch (error) {
2677
+ warnings.push(`Could not inspect legacy OpenCode backup ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2678
+ }
2679
+ }
2680
+ }
2681
+ catch {
2682
+ return paths;
2683
+ }
2684
+ return paths;
2685
+ }
2686
+ async function discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings) {
2687
+ const paths = [];
2688
+ const cacheRoot = join(targetRoot, ".embed-labs", "plugin-cache");
2689
+ try {
2690
+ const entries = await readdir(cacheRoot, { withFileTypes: true });
2691
+ for (const entry of entries) {
2692
+ if (!entry.isFile())
2693
+ continue;
2694
+ if (/^(embed-labs|embed-labs-opencode-plugin)-\d+\.\d+\.\d+\.tgz$/.test(entry.name)) {
2695
+ paths.push(join(cacheRoot, entry.name));
2696
+ }
2697
+ }
2698
+ }
2699
+ catch (error) {
2700
+ if (error.code !== "ENOENT") {
2701
+ warnings.push(`Could not inspect OpenCode plugin cache ${cacheRoot}: ${error instanceof Error ? error.message : String(error)}`);
2702
+ }
2703
+ }
2704
+ return paths;
2705
+ }
2706
+ function legacyOpenCodeCleanupWarning(cleanup) {
2707
+ const parts = [];
2708
+ if (cleanup.legacy_removed_paths.length > 0) {
2709
+ parts.push(`removed ${cleanup.legacy_removed_paths.length} legacy OpenCode plugin path(s)`);
2710
+ }
2711
+ if (cleanup.legacy_removed_config_entries?.length) {
2712
+ parts.push(`removed ${cleanup.legacy_removed_config_entries.length} legacy OpenCode config entry(s)`);
2713
+ }
2714
+ if (cleanup.warnings?.length) {
2715
+ parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
2716
+ }
2717
+ return parts.length > 0 ? `Legacy OpenCode cleanup: ${parts.join(", ")}.` : undefined;
2718
+ }
2719
+ function combineWarnings(...warnings) {
2720
+ const actual = warnings.filter((warning) => Boolean(warning));
2721
+ return actual.length > 0 ? actual.join(" ") : undefined;
1777
2722
  }
1778
2723
  async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
1779
2724
  const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
@@ -1834,6 +2779,115 @@ async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
1834
2779
  const warning = await upsertCodexMcpRuntimeConfig(bridgePath);
1835
2780
  return warning ? { registered: true, warning } : { registered: true };
1836
2781
  }
2782
+ async function maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath) {
2783
+ const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
2784
+ if (explicitTarget && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
2785
+ return {
2786
+ registered: false,
2787
+ warning: "Codex plugin marketplace entry was not registered because a custom target was used. Set EMBED_CODEX_MARKETPLACE_REGISTER=1 to register it anyway."
2788
+ };
2789
+ }
2790
+ if (resolve(targetRoot) !== resolve(defaultCodexPluginRoot()) && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
2791
+ return {
2792
+ registered: false,
2793
+ warning: "Codex plugin marketplace entry was not registered because the install target is not the default Codex plugin root."
2794
+ };
2795
+ }
2796
+ const marketplacePath = defaultCodexLocalMarketplaceRoot();
2797
+ const marketplacePluginPath = join(marketplacePath, "plugins", CODEX_PLUGIN_NAME);
2798
+ try {
2799
+ if (!await pathExists(join(targetPath, ".codex-plugin", "plugin.json"))) {
2800
+ return {
2801
+ registered: false,
2802
+ warning: `Codex plugin manifest was not found at ${join(targetPath, ".codex-plugin", "plugin.json")}.`
2803
+ };
2804
+ }
2805
+ await rm(marketplacePluginPath, { recursive: true, force: true });
2806
+ await mkdir(dirname(marketplacePluginPath), { recursive: true });
2807
+ await cp(targetPath, marketplacePluginPath, { recursive: true });
2808
+ await refreshCodexPluginCache(targetPath);
2809
+ await writeCodexLocalMarketplaceManifest(marketplacePath);
2810
+ const warning = await upsertCodexPluginMarketplaceConfig(marketplacePath);
2811
+ return warning ? { registered: true, marketplacePath, warning } : { registered: true, marketplacePath };
2812
+ }
2813
+ catch (error) {
2814
+ return {
2815
+ registered: false,
2816
+ marketplacePath,
2817
+ warning: `Could not register Codex plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`
2818
+ };
2819
+ }
2820
+ }
2821
+ function defaultCodexLocalMarketplaceRoot() {
2822
+ return join(defaultCodexHome(), "local-marketplaces", CODEX_PLUGIN_NAME);
2823
+ }
2824
+ async function refreshCodexPluginCache(targetPath) {
2825
+ const manifestPath = join(targetPath, ".codex-plugin", "plugin.json");
2826
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
2827
+ const version = typeof manifest.version === "string" && manifest.version.trim()
2828
+ ? manifest.version.trim()
2829
+ : "local";
2830
+ const cachePluginRoot = join(defaultCodexPluginRoot(), "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME);
2831
+ const cacheVersionPath = join(cachePluginRoot, version);
2832
+ await rm(cachePluginRoot, { recursive: true, force: true });
2833
+ await mkdir(dirname(cacheVersionPath), { recursive: true });
2834
+ await cp(targetPath, cacheVersionPath, { recursive: true });
2835
+ }
2836
+ async function writeCodexLocalMarketplaceManifest(marketplacePath) {
2837
+ const manifestPath = join(marketplacePath, ".agents", "plugins", "marketplace.json");
2838
+ const manifest = {
2839
+ name: CODEX_MARKETPLACE_NAME,
2840
+ interface: {
2841
+ displayName: "Embed Labs"
2842
+ },
2843
+ plugins: [
2844
+ {
2845
+ name: CODEX_PLUGIN_NAME,
2846
+ source: {
2847
+ source: "local",
2848
+ path: `./plugins/${CODEX_PLUGIN_NAME}`
2849
+ },
2850
+ policy: {
2851
+ installation: "AVAILABLE",
2852
+ authentication: "ON_USE"
2853
+ },
2854
+ category: "Developer Tools"
2855
+ }
2856
+ ]
2857
+ };
2858
+ await mkdir(dirname(manifestPath), { recursive: true });
2859
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
2860
+ }
2861
+ async function upsertCodexPluginMarketplaceConfig(marketplacePath) {
2862
+ const configPath = codexConfigPath();
2863
+ try {
2864
+ await mkdir(dirname(configPath), { recursive: true });
2865
+ let text = "";
2866
+ try {
2867
+ text = await readFile(configPath, "utf8");
2868
+ }
2869
+ catch {
2870
+ text = "";
2871
+ }
2872
+ const original = text;
2873
+ const cleaned = removeLegacyCodexConfigTables(text).text;
2874
+ let updated = upsertTomlTableKeys(cleaned, `marketplaces.${CODEX_MARKETPLACE_NAME}`, {
2875
+ source_type: tomlString("local"),
2876
+ source: tomlString(marketplacePath),
2877
+ last_updated: tomlString(new Date().toISOString().replace(/\.\d{3}Z$/, "Z"))
2878
+ });
2879
+ updated = upsertTomlTableKeys(updated, `plugins."${CODEX_PLUGIN_NAME}@${CODEX_MARKETPLACE_NAME}"`, {
2880
+ enabled: "true"
2881
+ });
2882
+ if (updated !== original) {
2883
+ await writeFile(configPath, updated, "utf8");
2884
+ }
2885
+ return undefined;
2886
+ }
2887
+ catch (error) {
2888
+ return `${configPath} could not be updated with the Embed Labs plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`;
2889
+ }
2890
+ }
1837
2891
  function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embedCliBin) {
1838
2892
  try {
1839
2893
  const parsed = JSON.parse(stdout);
@@ -1922,34 +2976,183 @@ function pluginMcpCloudApiUrl(parsed) {
1922
2976
  return DEFAULT_CLOUD_API_URL.replace(/\/+$/, "");
1923
2977
  }
1924
2978
  }
1925
- async function resolveExecutableOnPath(name) {
1926
- if (name.includes("/")) {
2979
+ async function resolveExecutableOnPath(name) {
2980
+ if (name.includes("/")) {
2981
+ try {
2982
+ await access(name, constants.X_OK);
2983
+ return name;
2984
+ }
2985
+ catch {
2986
+ return undefined;
2987
+ }
2988
+ }
2989
+ const pathEntries = (process.env.PATH || "").split(delimiter).filter(Boolean);
2990
+ for (const entry of pathEntries) {
2991
+ const candidate = join(entry, name);
2992
+ try {
2993
+ await access(candidate, constants.X_OK);
2994
+ return candidate;
2995
+ }
2996
+ catch {
2997
+ // Keep searching PATH.
2998
+ }
2999
+ }
3000
+ return undefined;
3001
+ }
3002
+ async function runBridgeStart(parsed) {
3003
+ const host = stringFlag(parsed, "host");
3004
+ const port = numberFlag(parsed, "port");
3005
+ const bridge = await resolveBridgeLauncher();
3006
+ const args = [...bridge.args];
3007
+ if (host) {
3008
+ args.push("--host", host);
3009
+ }
3010
+ if (port !== undefined) {
3011
+ args.push("--port", String(port));
3012
+ }
3013
+ const env = {
3014
+ ...process.env,
3015
+ ...(host ? { EMBED_BRIDGE_HOST: host } : {}),
3016
+ ...(port !== undefined ? { EMBED_BRIDGE_PORT: String(port) } : {})
3017
+ };
3018
+ const child = spawn(bridge.command, args, {
3019
+ stdio: "inherit",
3020
+ env
3021
+ });
3022
+ const forwardSignal = (signal) => {
3023
+ if (!child.killed) {
3024
+ child.kill(signal);
3025
+ }
3026
+ };
3027
+ process.once("SIGINT", forwardSignal);
3028
+ process.once("SIGTERM", forwardSignal);
3029
+ return await new Promise((resolveCode) => {
3030
+ child.on("error", (error) => {
3031
+ process.off("SIGINT", forwardSignal);
3032
+ process.off("SIGTERM", forwardSignal);
3033
+ console.error(error instanceof Error ? error.message : String(error));
3034
+ resolveCode(1);
3035
+ });
3036
+ child.on("close", (code, signal) => {
3037
+ process.off("SIGINT", forwardSignal);
3038
+ process.off("SIGTERM", forwardSignal);
3039
+ if (signal === "SIGINT" || signal === "SIGTERM") {
3040
+ resolveCode(0);
3041
+ }
3042
+ else {
3043
+ resolveCode(code ?? 0);
3044
+ }
3045
+ });
3046
+ });
3047
+ }
3048
+ async function resolveBridgeLauncher() {
3049
+ const explicitBinary = process.env.EMBED_LOCAL_BRIDGE_BINARY?.trim();
3050
+ if (explicitBinary) {
1927
3051
  try {
1928
- await access(name, constants.X_OK);
1929
- return name;
3052
+ await access(explicitBinary, constants.X_OK);
3053
+ return { command: explicitBinary, args: [] };
1930
3054
  }
1931
3055
  catch {
1932
- return undefined;
3056
+ // Fall through so the package launcher can print its clearer repair message.
1933
3057
  }
1934
3058
  }
1935
- const pathEntries = (process.env.PATH || "").split(delimiter).filter(Boolean);
1936
- for (const entry of pathEntries) {
1937
- const candidate = join(entry, name);
3059
+ const pathBinary = await resolveExecutableOnPath(process.platform === "win32" ? "embed-local-bridge.cmd" : "embed-local-bridge");
3060
+ if (pathBinary) {
3061
+ return { command: pathBinary, args: [] };
3062
+ }
3063
+ const packageLauncher = await resolveBridgePackageLauncher();
3064
+ if (packageLauncher) {
3065
+ return { command: process.execPath, args: [packageLauncher] };
3066
+ }
3067
+ return {
3068
+ command: process.execPath,
3069
+ args: [resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js")]
3070
+ };
3071
+ }
3072
+ async function resolveBridgePackageLauncher() {
3073
+ const candidates = [];
3074
+ try {
3075
+ const packageJson = require.resolve("@embed-labs/local-bridge/package.json");
3076
+ candidates.push(join(dirname(packageJson), "dist", "index.js"));
3077
+ }
3078
+ catch {
3079
+ // Source checkout fallback below.
3080
+ }
3081
+ candidates.push(resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js"));
3082
+ for (const candidate of candidates) {
1938
3083
  try {
1939
- await access(candidate, constants.X_OK);
3084
+ await access(candidate, constants.R_OK);
1940
3085
  return candidate;
1941
3086
  }
1942
3087
  catch {
1943
- // Keep searching PATH.
3088
+ // Keep looking.
1944
3089
  }
1945
3090
  }
1946
3091
  return undefined;
1947
3092
  }
1948
3093
  function defaultOpenCodeRoot() {
1949
- return join(process.cwd(), ".opencode");
3094
+ return globalOpenCodeRoot();
3095
+ }
3096
+ function globalOpenCodeRoot() {
3097
+ return join(homedir(), ".config", "opencode");
3098
+ }
3099
+ function isGlobalOpenCodeRoot(targetRoot) {
3100
+ return resolve(targetRoot) === resolve(globalOpenCodeRoot());
3101
+ }
3102
+ async function ensureOpenCodeGlobalPluginConfig() {
3103
+ const configPath = join(globalOpenCodeRoot(), "opencode.json");
3104
+ let existing = {};
3105
+ try {
3106
+ const parsed = JSON.parse(await readFile(configPath, "utf8"));
3107
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3108
+ existing = parsed;
3109
+ }
3110
+ }
3111
+ catch {
3112
+ existing = {};
3113
+ }
3114
+ const configured = Array.isArray(existing.plugin)
3115
+ ? existing.plugin.filter((item) => typeof item === "string")
3116
+ : Array.isArray(existing.plugins)
3117
+ ? existing.plugins.filter((item) => typeof item === "string")
3118
+ : [];
3119
+ const removed = configured.filter(isLegacyOpenCodePluginConfigEntry);
3120
+ const cleaned = configured.filter((item) => !isLegacyOpenCodePluginConfigEntry(item));
3121
+ if (!cleaned.includes("embed-labs")) {
3122
+ cleaned.push("embed-labs");
3123
+ }
3124
+ await writeFile(configPath, `${JSON.stringify({
3125
+ ...existing,
3126
+ plugin: cleaned,
3127
+ plugins: undefined
3128
+ }, null, 2)}\n`, "utf8");
3129
+ return removed;
3130
+ }
3131
+ function isLegacyOpenCodePluginConfigEntry(item) {
3132
+ const normalized = item.trim().replace(/\\/g, "/");
3133
+ const pathOnly = normalized.split(/[?#]/, 1)[0] || normalized;
3134
+ return normalized === "dbt-agent"
3135
+ || normalized === "Dbt Agent"
3136
+ || normalized === "deve"
3137
+ || normalized === "development-board-toolchain"
3138
+ || normalized === "development-board-toolchain-dev"
3139
+ || normalized === "./plugins/deve"
3140
+ || normalized === "./plugins/deve.js"
3141
+ || /(?:^|\/)plugins\/deve(?:\.js)?$/.test(pathOnly)
3142
+ || /(?:^|\/)plugins\/dbt-agent(?:\.js)?$/.test(pathOnly)
3143
+ || normalized === "./plugins/development-board-toolchain"
3144
+ || normalized === "./plugins/development-board-toolchain.js"
3145
+ || normalized === "./plugins/development-board-toolchain-dev"
3146
+ || normalized === "./plugins/development-board-toolchain-dev.js"
3147
+ || pathOnly.endsWith("/plugins/development-board-toolchain")
3148
+ || pathOnly.endsWith("/plugins/development-board-toolchain.js")
3149
+ || pathOnly.endsWith("/plugins/development-board-toolchain-dev")
3150
+ || pathOnly.endsWith("/plugins/development-board-toolchain-dev.js")
3151
+ || normalized.includes("dbt-agent")
3152
+ || normalized.includes("development-board-toolchain");
1950
3153
  }
1951
3154
  async function openCodeDuplicatePluginWarning(targetRoot) {
1952
- const globalRoot = join(homedir(), ".config", "opencode");
3155
+ const globalRoot = globalOpenCodeRoot();
1953
3156
  if (resolve(targetRoot) === resolve(globalRoot))
1954
3157
  return undefined;
1955
3158
  const configPath = join(globalRoot, "opencode.json");
@@ -1981,6 +3184,21 @@ async function localPluginVersion(kind) {
1981
3184
  return undefined;
1982
3185
  }
1983
3186
  }
3187
+ async function installedCodexPluginVersion(pluginPath) {
3188
+ return await readPackageVersion(join(pluginPath, ".codex-plugin", "plugin.json"));
3189
+ }
3190
+ async function installedOpenCodePluginVersion(targetRoot) {
3191
+ return await readPackageVersion(join(targetRoot, "node_modules", "embed-labs", "package.json"));
3192
+ }
3193
+ async function readPackageVersion(filePath) {
3194
+ try {
3195
+ const parsed = JSON.parse(await readFile(filePath, "utf8"));
3196
+ return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : undefined;
3197
+ }
3198
+ catch {
3199
+ return undefined;
3200
+ }
3201
+ }
1984
3202
  async function localPluginSourcesAvailable() {
1985
3203
  return await pathExists(sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", ".codex-plugin", "plugin.json"))
1986
3204
  && await pathExists(sourceCheckoutPath("platform_plugins", "opencode_plugin", "package.json"));
@@ -2039,19 +3257,65 @@ async function parseErrorResponse(response) {
2039
3257
  return undefined;
2040
3258
  }
2041
3259
  async function cloudAuthToken() {
3260
+ return (await cloudAuthConfig()).token;
3261
+ }
3262
+ async function cloudAuthConfig() {
2042
3263
  const envToken = process.env.EMBED_API_TOKEN?.trim();
2043
3264
  if (envToken) {
2044
- return envToken;
3265
+ const fileConfig = await readLocalAuthFile();
3266
+ return {
3267
+ ...fileConfig,
3268
+ token: envToken,
3269
+ profile: process.env.EMBED_AUTH_PROFILE ?? fileConfig.profile ?? "default",
3270
+ source: "env"
3271
+ };
2045
3272
  }
3273
+ const fileConfig = await readLocalAuthFile();
3274
+ return {
3275
+ ...fileConfig,
3276
+ token: fileConfig.token?.trim() || undefined,
3277
+ profile: fileConfig.profile ?? "default",
3278
+ source: fileConfig.token ? "file" : undefined
3279
+ };
3280
+ }
3281
+ async function readLocalAuthFile() {
2046
3282
  try {
2047
3283
  const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
2048
- const fileToken = typeof parsed.token === "string" ? parsed.token.trim() : "";
2049
- return fileToken || undefined;
3284
+ return normalizeLocalAuthFile(parsed);
2050
3285
  }
2051
3286
  catch {
2052
- return undefined;
3287
+ return {};
2053
3288
  }
2054
3289
  }
3290
+ function normalizeLocalAuthFile(parsed) {
3291
+ const device = isJsonObject(parsed.device) ? parsed.device : undefined;
3292
+ const normalizedDevice = device && typeof device.device_id === "string" && typeof device.fingerprint_hash === "string" && typeof device.private_key_pem === "string"
3293
+ ? {
3294
+ device_id: device.device_id,
3295
+ fingerprint_hash: device.fingerprint_hash,
3296
+ private_key_pem: device.private_key_pem,
3297
+ public_key_pem: typeof device.public_key_pem === "string" ? device.public_key_pem : undefined,
3298
+ label: typeof device.label === "string" ? device.label : undefined,
3299
+ platform: typeof device.platform === "string" ? device.platform : undefined,
3300
+ arch: typeof device.arch === "string" ? device.arch : undefined,
3301
+ hostname_hash: typeof device.hostname_hash === "string" ? device.hostname_hash : undefined,
3302
+ registered_at: typeof device.registered_at === "string" ? device.registered_at : undefined
3303
+ }
3304
+ : undefined;
3305
+ return {
3306
+ profile: typeof parsed.profile === "string" ? parsed.profile : undefined,
3307
+ token: typeof parsed.token === "string" ? parsed.token.trim() : undefined,
3308
+ updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined,
3309
+ account_id: typeof parsed.account_id === "string" ? parsed.account_id : undefined,
3310
+ api_key_id: typeof parsed.api_key_id === "string" ? parsed.api_key_id : undefined,
3311
+ device: normalizedDevice
3312
+ };
3313
+ }
3314
+ async function writeLocalAuthFile(config) {
3315
+ await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
3316
+ await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify(config, null, 2)}\n`, "utf8");
3317
+ await chmod(DEFAULT_AUTH_FILE, 0o600).catch(() => undefined);
3318
+ }
2055
3319
  function serviceBaseUrl(url) {
2056
3320
  return url.replace(/\/+$/, "");
2057
3321
  }
@@ -3510,30 +4774,272 @@ async function authLogin(parsed) {
3510
4774
  return fail("invalid_args", "Usage: embed auth login --token <token> [--profile default] [--json]");
3511
4775
  }
3512
4776
  const updatedAt = new Date().toISOString();
3513
- await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
3514
- await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify({ profile, token, updated_at: updatedAt }, null, 2)}\n`, "utf8");
3515
- return ok({ authenticated: true, profile, source: "file", updated_at: updatedAt });
4777
+ const current = await readLocalAuthFile();
4778
+ const localDevice = await buildLocalDeviceAuth(parsed, current.device);
4779
+ const registration = await registerLocalDevice(token.trim(), localDevice.registration);
4780
+ if (!registration.ok) {
4781
+ return fail(registration.error.code, registration.error.message, {
4782
+ remediation: registration.error.remediation,
4783
+ details: registration.error.details
4784
+ });
4785
+ }
4786
+ const device = {
4787
+ device_id: registration.data.device.device_id,
4788
+ fingerprint_hash: localDevice.device.fingerprint_hash,
4789
+ private_key_pem: localDevice.device.private_key_pem,
4790
+ public_key_pem: localDevice.device.public_key_pem,
4791
+ label: registration.data.device.label ?? localDevice.device.label,
4792
+ platform: registration.data.device.platform ?? localDevice.device.platform,
4793
+ arch: registration.data.device.arch ?? localDevice.device.arch,
4794
+ hostname_hash: registration.data.device.hostname_hash ?? localDevice.device.hostname_hash,
4795
+ registered_at: registration.data.device.first_seen_at
4796
+ };
4797
+ await writeLocalAuthFile({
4798
+ profile,
4799
+ token: token.trim(),
4800
+ updated_at: updatedAt,
4801
+ account_id: registration.data.device.account_id,
4802
+ api_key_id: registration.data.device.api_key_id,
4803
+ device
4804
+ });
4805
+ return ok({
4806
+ authenticated: true,
4807
+ profile,
4808
+ source: "file",
4809
+ updated_at: updatedAt,
4810
+ account_id: registration.data.device.account_id,
4811
+ api_key_id: registration.data.device.api_key_id,
4812
+ device_id: device.device_id,
4813
+ device_fingerprint_hash: device.fingerprint_hash,
4814
+ device_label: device.label,
4815
+ device_registered_at: device.registered_at,
4816
+ device_private_key_configured: true
4817
+ });
3516
4818
  }
3517
4819
  async function authStatus() {
3518
- if (process.env.EMBED_API_TOKEN?.trim()) {
4820
+ const envToken = process.env.EMBED_API_TOKEN?.trim();
4821
+ const file = await readLocalAuthFile();
4822
+ const deviceIntegrity = file.device
4823
+ ? (await validateLocalDeviceIntegrity(file.device)).ok ? "ok" : "failed"
4824
+ : "unbound";
4825
+ if (envToken) {
3519
4826
  return {
3520
4827
  authenticated: true,
3521
4828
  profile: process.env.EMBED_AUTH_PROFILE ?? "default",
3522
- source: "env"
4829
+ source: "env",
4830
+ account_id: file.account_id,
4831
+ api_key_id: file.api_key_id,
4832
+ device_id: file.device?.device_id,
4833
+ device_fingerprint_hash: file.device?.fingerprint_hash,
4834
+ device_label: file.device?.label,
4835
+ device_registered_at: file.device?.registered_at,
4836
+ device_private_key_configured: Boolean(file.device?.private_key_pem),
4837
+ device_integrity: deviceIntegrity
3523
4838
  };
3524
4839
  }
4840
+ return {
4841
+ authenticated: Boolean(file.token?.trim()),
4842
+ profile: file.profile ?? "default",
4843
+ source: file.token ? "file" : undefined,
4844
+ updated_at: file.updated_at,
4845
+ account_id: file.account_id,
4846
+ api_key_id: file.api_key_id,
4847
+ device_id: file.device?.device_id,
4848
+ device_fingerprint_hash: file.device?.fingerprint_hash,
4849
+ device_label: file.device?.label,
4850
+ device_registered_at: file.device?.registered_at,
4851
+ device_private_key_configured: Boolean(file.device?.private_key_pem),
4852
+ device_integrity: deviceIntegrity
4853
+ };
4854
+ }
4855
+ async function authDeviceStatus(parsed) {
4856
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
4857
+ const unexpected = parsed.command.slice(3);
4858
+ if (unknownFlag || unexpected.length > 0) {
4859
+ return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_STATUS_USAGE}` : AUTH_DEVICE_STATUS_USAGE);
4860
+ }
4861
+ const local = await authStatus();
4862
+ const remote = local.authenticated ? await cloudGet("/v1/me/devices") : undefined;
4863
+ if (remote && !remote.ok) {
4864
+ return ok({ local });
4865
+ }
4866
+ return ok({ local, remote: remote?.data });
4867
+ }
4868
+ async function authDeviceList(parsed) {
4869
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
4870
+ const unexpected = parsed.command.slice(3);
4871
+ if (unknownFlag || unexpected.length > 0) {
4872
+ return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_LIST_USAGE}` : AUTH_DEVICE_LIST_USAGE);
4873
+ }
4874
+ return await cloudGet("/v1/me/devices");
4875
+ }
4876
+ async function authDeviceRevoke(parsed) {
4877
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
4878
+ if (unknownFlag) {
4879
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_REVOKE_USAGE}`);
4880
+ }
4881
+ const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_REVOKE_USAGE);
4882
+ if (!id.ok) {
4883
+ return fail("invalid_args", id.error);
4884
+ }
4885
+ return await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}/revoke`, {});
4886
+ }
4887
+ async function authDeviceRename(parsed) {
4888
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "label"]);
4889
+ if (unknownFlag) {
4890
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_RENAME_USAGE}`);
4891
+ }
4892
+ const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_RENAME_USAGE);
4893
+ if (!id.ok) {
4894
+ return fail("invalid_args", id.error);
4895
+ }
4896
+ const label = stringFlag(parsed, "label");
4897
+ if (!label?.trim()) {
4898
+ return fail("invalid_args", AUTH_DEVICE_RENAME_USAGE);
4899
+ }
4900
+ const updated = await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}`, { label: label.trim() });
4901
+ if (updated.ok) {
4902
+ const auth = await readLocalAuthFile();
4903
+ if (auth.device?.device_id === updated.data.device_id) {
4904
+ await writeLocalAuthFile({ ...auth, device: { ...auth.device, label: updated.data.label } });
4905
+ }
4906
+ }
4907
+ return updated;
4908
+ }
4909
+ async function buildLocalDeviceAuth(parsed, existing) {
4910
+ const fingerprint = await localHardwareFingerprint();
4911
+ const keyPair = existing?.fingerprint_hash === fingerprint.fingerprint_hash && existing.private_key_pem && existing.public_key_pem
4912
+ ? { privateKeyPem: existing.private_key_pem, publicKeyPem: existing.public_key_pem }
4913
+ : generateLocalDeviceKeyPair();
4914
+ const label = stringFlag(parsed, "label")?.trim()
4915
+ || existing?.label
4916
+ || `${fingerprint.platform} ${fingerprint.arch}`;
4917
+ const device = {
4918
+ device_id: existing?.fingerprint_hash === fingerprint.fingerprint_hash ? existing.device_id : "",
4919
+ fingerprint_hash: fingerprint.fingerprint_hash,
4920
+ private_key_pem: keyPair.privateKeyPem,
4921
+ public_key_pem: keyPair.publicKeyPem,
4922
+ label,
4923
+ platform: fingerprint.platform,
4924
+ arch: fingerprint.arch,
4925
+ hostname_hash: fingerprint.hostname_hash,
4926
+ registered_at: existing?.registered_at
4927
+ };
4928
+ return {
4929
+ device,
4930
+ registration: {
4931
+ fingerprint_hash: fingerprint.fingerprint_hash,
4932
+ public_key: keyPair.publicKeyPem,
4933
+ label,
4934
+ platform: fingerprint.platform,
4935
+ arch: fingerprint.arch,
4936
+ hostname_hash: fingerprint.hostname_hash,
4937
+ client_name: EMBED_CLIENT_NAME,
4938
+ client_version: EMBED_CLIENT_VERSION,
4939
+ metadata: {
4940
+ fingerprint_version: "v1",
4941
+ fingerprint_source: fingerprint.source
4942
+ }
4943
+ }
4944
+ };
4945
+ }
4946
+ function generateLocalDeviceKeyPair() {
4947
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
4948
+ return {
4949
+ privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
4950
+ publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString()
4951
+ };
4952
+ }
4953
+ async function registerLocalDevice(token, body) {
3525
4954
  try {
3526
- const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
3527
- return {
3528
- authenticated: typeof parsed.token === "string" && parsed.token.trim().length > 0,
3529
- profile: typeof parsed.profile === "string" ? parsed.profile : "default",
3530
- source: "file",
3531
- updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined
4955
+ const bodyText = JSON.stringify(body);
4956
+ const headers = {
4957
+ "content-type": "application/json",
4958
+ authorization: `Bearer ${token}`
3532
4959
  };
4960
+ addCloudRequestSignature(headers, "POST", "/v1/me/devices/register", bodyText, token);
4961
+ const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/me/devices/register`, {
4962
+ method: "POST",
4963
+ headers,
4964
+ body: bodyText
4965
+ });
4966
+ const parsed = await response.json();
4967
+ return enrichCloudAuthFailure(parsed, true);
4968
+ }
4969
+ catch (error) {
4970
+ return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
4971
+ remediation: `Check that embed cloud-api is running at ${DEFAULT_CLOUD_API_URL}. Start it with: npm run cloud-api`
4972
+ });
4973
+ }
4974
+ }
4975
+ async function localHardwareFingerprint() {
4976
+ cachedLocalHardwareFingerprint ??= localHardwareFingerprintUncached();
4977
+ return await cachedLocalHardwareFingerprint;
4978
+ }
4979
+ async function localHardwareFingerprintUncached() {
4980
+ const platformName = platform();
4981
+ const archName = arch();
4982
+ const raw = await localHardwareId(platformName);
4983
+ const fingerprintHash = createHash("sha256")
4984
+ .update(`embed-labs:device:v1:${platformName}:${raw.value}`)
4985
+ .digest("hex");
4986
+ const hostnameHash = createHash("sha256")
4987
+ .update(`embed-labs:hostname:v1:${hostname()}`)
4988
+ .digest("hex");
4989
+ return {
4990
+ fingerprint_hash: fingerprintHash,
4991
+ platform: platformName,
4992
+ arch: archName,
4993
+ hostname_hash: hostnameHash,
4994
+ source: raw.source
4995
+ };
4996
+ }
4997
+ async function localHardwareId(platformName) {
4998
+ if (platformName === "darwin") {
4999
+ const result = await runLocalProcess("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
5000
+ const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(result.stdout);
5001
+ if (match?.[1]) {
5002
+ return { value: match[1], source: "macos_ioplatformuuid" };
5003
+ }
5004
+ }
5005
+ if (platformName === "win32") {
5006
+ const result = await runLocalProcess("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
5007
+ const match = /MachineGuid\s+REG_\w+\s+([^\r\n]+)/.exec(result.stdout);
5008
+ if (match?.[1]?.trim()) {
5009
+ return { value: match[1].trim(), source: "windows_machineguid" };
5010
+ }
5011
+ }
5012
+ if (platformName === "linux") {
5013
+ for (const pathValue of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
5014
+ try {
5015
+ const value = (await readFile(pathValue, "utf8")).trim();
5016
+ if (value) {
5017
+ return { value, source: `linux:${pathValue}` };
5018
+ }
5019
+ }
5020
+ catch {
5021
+ // Try the next stable machine id location.
5022
+ }
5023
+ }
5024
+ }
5025
+ const generated = await localGeneratedInstallId();
5026
+ return { value: generated, source: "generated_install_id" };
5027
+ }
5028
+ async function localGeneratedInstallId() {
5029
+ try {
5030
+ const parsed = JSON.parse(await readFile(DEFAULT_DEVICE_FILE, "utf8"));
5031
+ if (typeof parsed.generated_install_id === "string" && parsed.generated_install_id.trim()) {
5032
+ return parsed.generated_install_id.trim();
5033
+ }
3533
5034
  }
3534
5035
  catch {
3535
- return { authenticated: false, profile: "default" };
5036
+ // Fall through and create a local-only fallback id.
3536
5037
  }
5038
+ const generated = `install_${randomBytes(24).toString("base64url")}`;
5039
+ await mkdir(dirname(DEFAULT_DEVICE_FILE), { recursive: true });
5040
+ await writeFile(DEFAULT_DEVICE_FILE, `${JSON.stringify({ generated_install_id: generated, created_at: new Date().toISOString() }, null, 2)}\n`, "utf8");
5041
+ await chmod(DEFAULT_DEVICE_FILE, 0o600).catch(() => undefined);
5042
+ return generated;
3537
5043
  }
3538
5044
  function accountCreateBody(parsed) {
3539
5045
  const unknownFlag = firstUnknownFlag(parsed, ["json", "email", "display-name"]);
@@ -3689,6 +5195,80 @@ function usageRecordBody(parsed) {
3689
5195
  created_at: createdAtResult.value
3690
5196
  });
3691
5197
  }
5198
+ function mcpToolEventBody(parsed) {
5199
+ const unknownFlag = firstUnknownFlag(parsed, [
5200
+ "json",
5201
+ "account",
5202
+ "account-id",
5203
+ "tool",
5204
+ "client",
5205
+ "mode",
5206
+ "server-model-used",
5207
+ "success",
5208
+ "local-device-id",
5209
+ "local_device_id",
5210
+ "request-id",
5211
+ "duration-ms",
5212
+ "input-summary",
5213
+ "output-summary"
5214
+ ]);
5215
+ if (unknownFlag) {
5216
+ return `Unknown flag --${unknownFlag}. ${MCP_TOOL_EVENT_USAGE}`;
5217
+ }
5218
+ const extra = parsed.command.slice(2);
5219
+ if (extra.length > 0) {
5220
+ return `Unexpected argument: ${extra[0]}. ${MCP_TOOL_EVENT_USAGE}`;
5221
+ }
5222
+ const toolResult = optionalTrimmedStringFlag(parsed, "tool");
5223
+ if (toolResult.error)
5224
+ return toolResult.error;
5225
+ if (!toolResult.value)
5226
+ return MCP_TOOL_EVENT_USAGE;
5227
+ const accountResult = optionalTrimmedStringAliasFlag(parsed, ["account", "account-id"], "account or account-id");
5228
+ if (accountResult.error)
5229
+ return accountResult.error;
5230
+ const clientResult = optionalTrimmedStringFlag(parsed, "client");
5231
+ if (clientResult.error)
5232
+ return clientResult.error;
5233
+ const modeResult = optionalTrimmedStringFlag(parsed, "mode");
5234
+ if (modeResult.error)
5235
+ return modeResult.error;
5236
+ const localDeviceResult = optionalTrimmedStringAliasFlag(parsed, ["local-device-id", "local_device_id"], "local-device-id");
5237
+ if (localDeviceResult.error)
5238
+ return localDeviceResult.error;
5239
+ const requestIdResult = optionalTrimmedStringFlag(parsed, "request-id");
5240
+ if (requestIdResult.error)
5241
+ return requestIdResult.error;
5242
+ const inputSummaryResult = optionalTrimmedStringFlag(parsed, "input-summary");
5243
+ if (inputSummaryResult.error)
5244
+ return inputSummaryResult.error;
5245
+ const outputSummaryResult = optionalTrimmedStringFlag(parsed, "output-summary");
5246
+ if (outputSummaryResult.error)
5247
+ return outputSummaryResult.error;
5248
+ const durationResult = optionalNonNegativeIntegerFlag(parsed, "duration-ms");
5249
+ if (durationResult.error)
5250
+ return durationResult.error;
5251
+ const serverModelUsed = optionalBooleanFlag(parsed, "server-model-used");
5252
+ if (typeof serverModelUsed === "string")
5253
+ return serverModelUsed;
5254
+ const success = optionalBooleanFlag(parsed, "success");
5255
+ if (typeof success === "string")
5256
+ return success;
5257
+ return compactBody({
5258
+ account_id: accountResult.value,
5259
+ tool_name: toolResult.value,
5260
+ client: clientResult.value,
5261
+ mode: modeResult.value,
5262
+ local_device_id: localDeviceResult.value,
5263
+ server_model_used: serverModelUsed,
5264
+ success,
5265
+ request_id: requestIdResult.value,
5266
+ duration_ms: durationResult.value,
5267
+ input_summary: inputSummaryResult.value,
5268
+ output_summary: outputSummaryResult.value,
5269
+ metadata: localDeviceResult.value ? { local_device_id: localDeviceResult.value } : undefined
5270
+ });
5271
+ }
3692
5272
  function usageSummaryRequest(parsed) {
3693
5273
  const unknownFlag = firstUnknownFlag(parsed, ["json", "account", "account-id", "api-key-id", "from", "to"]);
3694
5274
  if (unknownFlag) {
@@ -4024,8 +5604,109 @@ function billingSnapshotListRequest(parsed) {
4024
5604
  }
4025
5605
  return { path: `/v1/accounts/${encodeURIComponent(accountResult.value)}/billing/snapshots` };
4026
5606
  }
5607
+ function localToolchainListRequest(parsed, usage = LOCAL_TOOLCHAIN_LIST_USAGE) {
5608
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "install-root"]);
5609
+ if (unknownFlag) {
5610
+ return `Unknown flag --${unknownFlag}. ${usage}`;
5611
+ }
5612
+ const extra = parsed.command.slice(3);
5613
+ if (extra.length > 0) {
5614
+ return `Unexpected argument: ${extra[0]}. ${usage}`;
5615
+ }
5616
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
5617
+ if (board.error)
5618
+ return board.error;
5619
+ const channel = optionalTrimmedStringFlag(parsed, "channel");
5620
+ if (channel.error)
5621
+ return channel.error;
5622
+ const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
5623
+ if (metadataRoot.error)
5624
+ return metadataRoot.error;
5625
+ const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
5626
+ if (installRoot.error)
5627
+ return installRoot.error;
5628
+ return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value, installRoot: installRoot.value };
5629
+ }
5630
+ function localToolchainLatestRequest(parsed) {
5631
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root"]);
5632
+ if (unknownFlag) {
5633
+ return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_LATEST_USAGE}`;
5634
+ }
5635
+ const extra = parsed.command.slice(3);
5636
+ if (extra.length > 0) {
5637
+ return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_LATEST_USAGE}`;
5638
+ }
5639
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
5640
+ if (board.error)
5641
+ return board.error;
5642
+ const channel = optionalTrimmedStringFlag(parsed, "channel");
5643
+ if (channel.error)
5644
+ return channel.error;
5645
+ const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
5646
+ if (metadataRoot.error)
5647
+ return metadataRoot.error;
5648
+ return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value };
5649
+ }
5650
+ function localToolchainCurrentRequest(parsed) {
5651
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "install-root"]);
5652
+ if (unknownFlag) {
5653
+ return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_CURRENT_USAGE}`;
5654
+ }
5655
+ const extra = parsed.command.slice(3);
5656
+ if (extra.length > 0) {
5657
+ return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_CURRENT_USAGE}`;
5658
+ }
5659
+ const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
5660
+ if (installRoot.error)
5661
+ return installRoot.error;
5662
+ return { installRoot: installRoot.value };
5663
+ }
5664
+ function localToolchainInstallRequest(parsed) {
5665
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "mode", "force"]);
5666
+ if (unknownFlag) {
5667
+ return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_INSTALL_USAGE}`;
5668
+ }
5669
+ const extra = parsed.command.slice(3);
5670
+ if (extra.length > 0) {
5671
+ return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_INSTALL_USAGE}`;
5672
+ }
5673
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
5674
+ if (board.error)
5675
+ return board.error;
5676
+ const channel = optionalTrimmedStringFlag(parsed, "channel");
5677
+ if (channel.error)
5678
+ return channel.error;
5679
+ const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
5680
+ if (metadataRoot.error)
5681
+ return metadataRoot.error;
5682
+ const sourceUrl = optionalTrimmedStringFlag(parsed, "source-url");
5683
+ if (sourceUrl.error)
5684
+ return sourceUrl.error;
5685
+ const sourceReleaseRoot = optionalTrimmedStringFlag(parsed, "source-release-root");
5686
+ if (sourceReleaseRoot.error)
5687
+ return sourceReleaseRoot.error;
5688
+ if (sourceUrl.value && sourceReleaseRoot.value) {
5689
+ return "Use only one of --source-url or --source-release-root.";
5690
+ }
5691
+ const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
5692
+ if (installRoot.error)
5693
+ return installRoot.error;
5694
+ const mode = optionalTrimmedStringFlag(parsed, "mode");
5695
+ if (mode.error)
5696
+ return mode.error;
5697
+ return {
5698
+ boardId: board.value,
5699
+ channel: channel.value,
5700
+ metadataRoot: metadataRoot.value,
5701
+ sourceUrl: sourceUrl.value,
5702
+ sourceReleaseRoot: sourceReleaseRoot.value,
5703
+ installRoot: installRoot.value,
5704
+ mode: mode.value,
5705
+ force: booleanFlag(parsed, "force")
5706
+ };
5707
+ }
4027
5708
  function localToolchainValidateRequest(parsed) {
4028
- const unknownFlag = firstUnknownFlag(parsed, ["json", "release-root"]);
5709
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "release-root", "mode"]);
4029
5710
  if (unknownFlag) {
4030
5711
  return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_VALIDATE_USAGE}`;
4031
5712
  }
@@ -4037,7 +5718,15 @@ function localToolchainValidateRequest(parsed) {
4037
5718
  if (releaseRoot.error) {
4038
5719
  return releaseRoot.error;
4039
5720
  }
4040
- return { releaseRoot: releaseRoot.value };
5721
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
5722
+ if (board.error) {
5723
+ return board.error;
5724
+ }
5725
+ const mode = optionalTrimmedStringFlag(parsed, "mode");
5726
+ if (mode.error) {
5727
+ return mode.error;
5728
+ }
5729
+ return { releaseRoot: releaseRoot.value, mode: mode.value, boardId: board.value };
4041
5730
  }
4042
5731
  function localCompileTaishanPiRequest(parsed, auth) {
4043
5732
  const unknownFlag = firstUnknownFlag(parsed, ["json", "source", "output", "release-root", "account", "account-id"]);
@@ -4122,7 +5811,9 @@ function localToolchainAuthContext(auth, accountId) {
4122
5811
  authenticated: auth.authenticated,
4123
5812
  profile: auth.profile,
4124
5813
  source: auth.source,
4125
- account_id: accountId
5814
+ account_id: accountId,
5815
+ api_key_id: auth.api_key_id,
5816
+ device_id: auth.device_id
4126
5817
  };
4127
5818
  }
4128
5819
  function usageEventsRequest(parsed) {
@@ -4532,6 +6223,35 @@ function renderPluginList(result) {
4532
6223
  `install="${plugin.install_command}"`
4533
6224
  ].filter(Boolean).join(" ")).join("\n");
4534
6225
  }
6226
+ function renderPluginUpdateCheck(result) {
6227
+ const lines = [
6228
+ `release_url=${result.release_url}`,
6229
+ result.latest_version ? `latest_version=${result.latest_version}` : ""
6230
+ ].filter(Boolean);
6231
+ if (result.release_notes.length > 0) {
6232
+ lines.push("release_notes:");
6233
+ for (const note of result.release_notes) {
6234
+ lines.push(` - ${note}`);
6235
+ }
6236
+ }
6237
+ for (const plugin of result.plugins) {
6238
+ lines.push("");
6239
+ lines.push(`${plugin.display_name} (${plugin.id})`);
6240
+ lines.push(` installed=${plugin.installed}`);
6241
+ lines.push(` installed_version=${plugin.installed_version ?? "none"}`);
6242
+ lines.push(` latest_version=${plugin.latest_version ?? "unknown"}`);
6243
+ lines.push(` update_available=${plugin.update_available}`);
6244
+ lines.push(` target=${plugin.target_path}`);
6245
+ lines.push(` update_command=${plugin.update_command}`);
6246
+ if (plugin.release_file) {
6247
+ lines.push(` release_file=${plugin.release_file}`);
6248
+ }
6249
+ for (const note of plugin.notes) {
6250
+ lines.push(` note=${note}`);
6251
+ }
6252
+ }
6253
+ return lines.join("\n");
6254
+ }
4535
6255
  function renderPluginInstall(result) {
4536
6256
  const lines = ["Installed plugins:"];
4537
6257
  for (const item of result.installed) {
@@ -4553,6 +6273,15 @@ function renderPluginInstall(result) {
4553
6273
  if (item.mcp_warning) {
4554
6274
  lines.push(` warning=${item.mcp_warning}`);
4555
6275
  }
6276
+ if (item.marketplace_registered !== undefined) {
6277
+ lines.push(` codex_marketplace_registered=${item.marketplace_registered}`);
6278
+ }
6279
+ if (item.marketplace_path) {
6280
+ lines.push(` codex_marketplace=${item.marketplace_path}`);
6281
+ }
6282
+ if (item.marketplace_warning) {
6283
+ lines.push(` warning=${item.marketplace_warning}`);
6284
+ }
4556
6285
  }
4557
6286
  return lines.join("\n");
4558
6287
  }
@@ -4584,9 +6313,53 @@ function renderAgentRunResult(result) {
4584
6313
  return lines.join("\n");
4585
6314
  }
4586
6315
  function renderAuthStatus(status) {
4587
- return status.authenticated
4588
- ? `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`
4589
- : `Not authenticated profile=${status.profile}`;
6316
+ if (!status.authenticated) {
6317
+ return `Not authenticated profile=${status.profile}`;
6318
+ }
6319
+ return [
6320
+ `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`,
6321
+ status.account_id ? `account=${status.account_id}` : "",
6322
+ status.api_key_id ? `api_key=${status.api_key_id}` : "",
6323
+ status.device_id ? `device=${status.device_id}` : "device=not_registered",
6324
+ status.device_label ? `device_label=${status.device_label}` : "",
6325
+ status.device_integrity ? `device_integrity=${status.device_integrity}` : "",
6326
+ status.device_private_key_configured === false ? "device_private_key=missing" : ""
6327
+ ].filter(Boolean).join("\n");
6328
+ }
6329
+ function renderAuthDeviceStatus(status) {
6330
+ const lines = [renderAuthStatus(status.local)];
6331
+ if (status.remote) {
6332
+ const activeCount = status.remote.devices.filter((device) => device.status === "active").length;
6333
+ lines.push(`remote_devices=${activeCount}/${status.remote.device_limit}`);
6334
+ const localDevice = status.local.device_id
6335
+ ? status.remote.devices.find((device) => device.device_id === status.local.device_id)
6336
+ : undefined;
6337
+ if (localDevice) {
6338
+ lines.push(`remote_current=${renderAuthDevice(localDevice)}`);
6339
+ }
6340
+ }
6341
+ return lines.join("\n");
6342
+ }
6343
+ function renderAuthDeviceList(result) {
6344
+ if (result.devices.length === 0) {
6345
+ return `No registered devices. device_limit=${result.device_limit}`;
6346
+ }
6347
+ return [
6348
+ `device_limit=${result.device_limit}`,
6349
+ ...result.devices.map(renderAuthDevice)
6350
+ ].join("\n");
6351
+ }
6352
+ function renderAuthDevice(device) {
6353
+ return [
6354
+ `${device.device_id} account=${device.account_id}`,
6355
+ device.api_key_id ? `api_key=${device.api_key_id}` : "",
6356
+ `status=${device.status}`,
6357
+ device.label ? `label=${device.label}` : "",
6358
+ device.platform ? `platform=${device.platform}` : "",
6359
+ device.arch ? `arch=${device.arch}` : "",
6360
+ `last_seen_at=${device.last_seen_at}`,
6361
+ device.revoked_at ? `revoked_at=${device.revoked_at}` : ""
6362
+ ].filter(Boolean).join(" ");
4590
6363
  }
4591
6364
  function renderAccount(account) {
4592
6365
  return [
@@ -4636,6 +6409,20 @@ function renderUsageRecord(record) {
4636
6409
  `created_at=${record.created_at}`
4637
6410
  ].filter(Boolean).join(" ");
4638
6411
  }
6412
+ function renderMcpToolEvent(event) {
6413
+ return [
6414
+ `${event.event_id} tool=${event.tool_name}`,
6415
+ event.account_id ? `account=${event.account_id}` : "",
6416
+ event.api_key_id ? `api_key=${event.api_key_id}` : "",
6417
+ `client=${event.client}`,
6418
+ `mode=${event.mode}`,
6419
+ `server_model_used=${event.server_model_used}`,
6420
+ `success=${event.success}`,
6421
+ event.request_id ? `request=${event.request_id}` : "",
6422
+ event.duration_ms !== undefined ? `duration_ms=${event.duration_ms}` : "",
6423
+ `created_at=${event.created_at}`
6424
+ ].filter(Boolean).join(" ");
6425
+ }
4639
6426
  function renderUsageSummary(summary) {
4640
6427
  const lines = [
4641
6428
  summary.account_id ? `account=${summary.account_id}` : "",
@@ -5159,10 +6946,187 @@ function renderBuildWorkspaceSourcePatch(result) {
5159
6946
  }
5160
6947
  return lines.join("\n");
5161
6948
  }
6949
+ function renderLocalToolchainList(result) {
6950
+ const installedCount = result.environments.filter((environment) => !!environment.installed).length;
6951
+ const availableCount = result.environments.filter((environment) => environment.status === "available").length;
6952
+ const updateCount = result.environments.filter((environment) => environment.status === "update_available").length;
6953
+ const lines = [
6954
+ `Local development environments: ${result.environments.length}`,
6955
+ `installed=${installedCount} available=${availableCount} updates=${updateCount}`,
6956
+ `host=${result.host}`,
6957
+ `channel=${result.channel}`,
6958
+ result.metadata_source === "local_override" ? `metadata_override=${result.metadata_root}` : "metadata=production/built-in",
6959
+ `install_root=${result.install_root}`,
6960
+ `registry=${result.registry_path}`
6961
+ ];
6962
+ for (const environment of result.environments) {
6963
+ lines.push("");
6964
+ lines.push(`${environment.display_name} (${environment.board_id})`);
6965
+ lines.push(` status=${localToolchainStatusLabel(environment.status)}`);
6966
+ lines.push(` latest=${environment.latest.version}`);
6967
+ if (environment.installed) {
6968
+ lines.push(` installed=${environment.installed.version ?? "unknown"} mode=${environment.installed.mode ?? "unknown"}`);
6969
+ if (environment.installed.release_root) {
6970
+ lines.push(` release_root=${environment.installed.release_root}`);
6971
+ }
6972
+ }
6973
+ lines.push(` install_modes=${environment.install_modes.join(",")}`);
6974
+ lines.push(` install_command=${environment.install_command}`);
6975
+ if (environment.status === "update_available") {
6976
+ lines.push(` update_command=${environment.update_command}`);
6977
+ }
6978
+ if (environment.components?.length) {
6979
+ lines.push(` package_summary=${localToolchainComponentSummary(environment.components)}`);
6980
+ lines.push(` package_groups=${localToolchainComponentGroups(environment.components).join(", ")}`);
6981
+ lines.push(` detail_command=embedlabs local toolchain latest --board ${environment.board_id}`);
6982
+ }
6983
+ if (environment.notes.length > 0) {
6984
+ for (const note of environment.notes) {
6985
+ lines.push(` note=${note}`);
6986
+ }
6987
+ }
6988
+ }
6989
+ return lines.join("\n");
6990
+ }
6991
+ function localToolchainStatusLabel(status) {
6992
+ if (status === "installed")
6993
+ return "installed/已安装";
6994
+ if (status === "available")
6995
+ return "available/可安装";
6996
+ if (status === "update_available")
6997
+ return "update_available/可更新";
6998
+ if (status === "unsupported_host")
6999
+ return "unsupported_host/当前系统暂不支持";
7000
+ return status;
7001
+ }
7002
+ function localToolchainComponentSummary(components) {
7003
+ const totalBytes = components.reduce((total, component) => total + component.size_bytes, 0);
7004
+ return `${components.length} packages, ${formatByteSize(totalBytes)}`;
7005
+ }
7006
+ function localToolchainComponentGroups(components) {
7007
+ const groups = new Set();
7008
+ for (const component of components) {
7009
+ const text = `${component.id} ${component.role ?? ""}`.toLowerCase();
7010
+ if (text.includes("llvm") || text.includes("compiler"))
7011
+ groups.add("compiler/编译器包装");
7012
+ if (text.includes("sysroot") || text.includes("cross"))
7013
+ groups.add("sysroot/交叉运行库");
7014
+ if (text.includes("qt"))
7015
+ groups.add("qt/Qt 应用支持");
7016
+ if (text.includes("rockchip") || text.includes("boot") || text.includes("resource"))
7017
+ groups.add("boot-flash/启动与烧写工具");
7018
+ if (text.includes("image") || text.includes("rootfs"))
7019
+ groups.add("images/镜像资源");
7020
+ if (text.includes("rp2350") || text.includes("pico"))
7021
+ groups.add("rp2350/Pico2 监控工具");
7022
+ if (text.includes("meta"))
7023
+ groups.add("metadata/知识与脚本元数据");
7024
+ }
7025
+ return groups.size > 0 ? [...groups] : ["runtime/运行时工具"];
7026
+ }
7027
+ function formatByteSize(bytes) {
7028
+ if (!Number.isFinite(bytes) || bytes < 0) {
7029
+ return "unknown size";
7030
+ }
7031
+ const units = ["B", "KB", "MB", "GB", "TB"];
7032
+ let value = bytes;
7033
+ let unit = 0;
7034
+ while (value >= 1024 && unit < units.length - 1) {
7035
+ value /= 1024;
7036
+ unit += 1;
7037
+ }
7038
+ const fixed = unit === 0 || value >= 10 ? value.toFixed(0) : value.toFixed(1);
7039
+ return `${fixed} ${units[unit]}`;
7040
+ }
7041
+ function renderLocalToolchainLatest(result) {
7042
+ const lines = [
7043
+ `board=${result.board_id}`,
7044
+ `channel=${result.channel}`,
7045
+ `version=${result.version}`,
7046
+ `host=${result.host}`,
7047
+ result.metadata_root ? `metadata_root=${result.metadata_root}` : "metadata=built-in",
7048
+ result.download?.source_url ? `download=${result.download.mirror_kind}:${result.download.source_url}` : "",
7049
+ result.download?.archive ? `archive_sha256=${result.download.archive.sha256}` : "",
7050
+ result.download?.archive ? `archive_size_bytes=${result.download.archive.size_bytes}` : "",
7051
+ result.download?.components?.length ? `components=${result.download.components.length}` : "",
7052
+ result.download?.default_mode ? `default_mode=${result.download.default_mode}` : "",
7053
+ result.download_error ? `download_error=${result.download_error}` : ""
7054
+ ].filter(Boolean);
7055
+ if (result.download?.components?.length) {
7056
+ lines.push("download_components:");
7057
+ for (const component of result.download.components) {
7058
+ lines.push(` ${component.id}@${component.version} modes=${component.install_modes?.join(",") || "all"} bytes=${component.archive.size_bytes}`);
7059
+ }
7060
+ }
7061
+ if (result.packages.length > 0) {
7062
+ lines.push("packages:");
7063
+ for (const pkg of result.packages) {
7064
+ lines.push(` ${pkg.id}@${pkg.version}`);
7065
+ }
7066
+ }
7067
+ return lines.join("\n");
7068
+ }
7069
+ function renderLocalToolchainCurrent(result) {
7070
+ if (!result.installed) {
7071
+ return [
7072
+ "No local toolchain installed.",
7073
+ `board=${result.board_id}`,
7074
+ `install_root=${result.install_root}`,
7075
+ `registry=${result.registry_path}`
7076
+ ].join("\n");
7077
+ }
7078
+ return [
7079
+ "Local toolchain installed.",
7080
+ `board=${result.board_id}`,
7081
+ result.version ? `version=${result.version}` : "",
7082
+ result.channel ? `channel=${result.channel}` : "",
7083
+ result.mode ? `mode=${result.mode}` : "",
7084
+ result.release_root ? `release_root=${result.release_root}` : "",
7085
+ `install_root=${result.install_root}`,
7086
+ `registry=${result.registry_path}`
7087
+ ].filter(Boolean).join("\n");
7088
+ }
7089
+ function renderLocalToolchainInstall(result) {
7090
+ const lines = [
7091
+ "Local toolchain installed.",
7092
+ `board=${result.board_id}`,
7093
+ `version=${result.version}`,
7094
+ `channel=${result.channel}`,
7095
+ `host=${result.host}`,
7096
+ `mode=${result.mode}`,
7097
+ `install_root=${result.install_root}`,
7098
+ `release_root=${result.release_root}`,
7099
+ `registry=${result.registry_path}`,
7100
+ `source=${result.source.kind}:${result.source.value}`,
7101
+ result.source.downloaded_path ? `downloaded=${result.source.downloaded_path}` : "",
7102
+ result.source.components?.length ? `components=${result.source.components.length}` : "",
7103
+ `validation=${result.validation.ok ? "ok" : "failed"}`
7104
+ ].filter(Boolean);
7105
+ if (result.source.components?.length) {
7106
+ lines.push("installed_components:");
7107
+ for (const component of result.source.components) {
7108
+ lines.push(` ${component.id}@${component.version} ${component.mirror_kind || ""} bytes=${component.size_bytes}`);
7109
+ }
7110
+ }
7111
+ if (result.installed_paths.length > 0) {
7112
+ lines.push("installed_paths:");
7113
+ for (const installedPath of result.installed_paths) {
7114
+ lines.push(` ${installedPath}`);
7115
+ }
7116
+ }
7117
+ if (result.packages.length > 0) {
7118
+ lines.push("packages:");
7119
+ for (const pkg of result.packages) {
7120
+ lines.push(` ${pkg.id}@${pkg.version}`);
7121
+ }
7122
+ }
7123
+ return lines.join("\n");
7124
+ }
5162
7125
  function renderLocalToolchainValidation(result) {
5163
7126
  const lines = [
5164
7127
  result.ok ? "Local toolchain ready." : "Local toolchain not ready.",
5165
7128
  `board=${result.board_id}`,
7129
+ `mode=${result.mode}`,
5166
7130
  `host=${result.host.platform}/${result.host.arch}`,
5167
7131
  `release_root=${result.release_root}`
5168
7132
  ];
@@ -5617,6 +7581,22 @@ function stringFlag(parsed, name) {
5617
7581
  function booleanFlag(parsed, name) {
5618
7582
  return parsed.flags[name] === true;
5619
7583
  }
7584
+ function optionalBooleanFlag(parsed, name) {
7585
+ const values = flagValues(parsed, name);
7586
+ if (values.length === 0)
7587
+ return undefined;
7588
+ const value = values[values.length - 1];
7589
+ if (value === true)
7590
+ return true;
7591
+ if (typeof value !== "string")
7592
+ return `--${name} must be true or false.`;
7593
+ const normalized = value.trim().toLowerCase();
7594
+ if (["1", "true", "yes", "y", "on"].includes(normalized))
7595
+ return true;
7596
+ if (["0", "false", "no", "n", "off"].includes(normalized))
7597
+ return false;
7598
+ return `--${name} must be true or false.`;
7599
+ }
5620
7600
  function switchFlag(parsed, name) {
5621
7601
  const values = flagValues(parsed, name);
5622
7602
  for (const value of values) {
@@ -5915,12 +7895,17 @@ Main workflow:
5915
7895
  3. Inspect server model routing:
5916
7896
  embed plugin install codex
5917
7897
  embed plugin install opencode
7898
+ embed plugin update check
5918
7899
  embed service modes
5919
7900
  embed model list
5920
7901
  embed model default
5921
7902
  4. Run a natural-language local tool loop:
5922
7903
  embed agent run --prompt "验证开发板状态"
5923
7904
  5. Validate or use the local TaishanPi toolchain:
7905
+ embed local toolchain list
7906
+ embed local toolchain installed
7907
+ embed local toolchain latest
7908
+ embed local toolchain install
5924
7909
  embed local toolchain validate
5925
7910
  embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
5926
7911
  embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
@@ -5963,9 +7948,15 @@ Local hardware:
5963
7948
  embed tool list
5964
7949
  embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22,15301]}'
5965
7950
  embed tool call wifi.scan --input-json '{"host":"198.19.77.2","user":"root"}'
7951
+ embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve
5966
7952
  embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}'
5967
7953
  embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}'
5968
7954
  embed device list
7955
+ embed local toolchain list
7956
+ embed local toolchain installed
7957
+ embed local toolchain latest
7958
+ embed local toolchain current
7959
+ embed local toolchain install
5969
7960
  embed local toolchain validate
5970
7961
  embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
5971
7962
  embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
@@ -5979,7 +7970,7 @@ Help:
5979
7970
 
5980
7971
  Environment:
5981
7972
  EMBED_BRIDGE_URL=http://127.0.0.1:18083
5982
- EMBED_CLOUD_API_URL=http://127.0.0.1:18100
7973
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
5983
7974
  EMBED_API_TOKEN=<token>
5984
7975
  CODEX_HOME=~/.codex
5985
7976
 
@@ -6034,6 +8025,8 @@ Install local AI client plugins explicitly:
6034
8025
  embed plugin list
6035
8026
  embed plugin install codex
6036
8027
  embed plugin install opencode
8028
+ embed plugin update check
8029
+ embed plugin update all
6037
8030
 
6038
8031
  Cloud build path:
6039
8032
 
@@ -6112,6 +8105,8 @@ Usage:
6112
8105
  embed auth logout [--json]
6113
8106
  embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]
6114
8107
  embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]
8108
+ embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
8109
+ embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
6115
8110
  embed service modes [--json]
6116
8111
  embed model list [--json]
6117
8112
  embed model default [--json]
@@ -6167,7 +8162,13 @@ Usage:
6167
8162
  embed build image generate --workspace <workspace_id> --prompt <request> [--account <account_id>] [--image-profile <profile_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--execution-mode cloud_worker|dry_run] [--worker-pool <pool>] [--json]
6168
8163
  embed build image boot-logo --logo <local_image> [--account <account_id>] [--project <project_id>] [--board taishanpi] [--variant 1M-RK3566] [--kernel-logo <local_image>] [--rotate -90] [--scale 100] [--output <package.json>] [--json]
6169
8164
  embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]
6170
- embed local toolchain validate [--release-root <path>] [--json]
8165
+ embed local toolchain list [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
8166
+ embed local toolchain installed [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
8167
+ embed local toolchain latest [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--json]
8168
+ embed local toolchain current [--install-root <path>] [--json]
8169
+ embed local toolchain install [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--force] [--json]
8170
+ Defaults to the production download channel at download.embedboard.com.
8171
+ embed local toolchain validate [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor] [--release-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--json]
6171
8172
  embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]
6172
8173
  embed local build qt-smoke --build-dir <dir> [--source <qt-smoke-dir>] [--release-root <path>] [--account <account_id>] [--json]
6173
8174
  embed debug tools [--json]
@@ -6178,6 +8179,18 @@ Usage:
6178
8179
  embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}' [--json]
6179
8180
  embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
6180
8181
  embed tool call qml.runtime.start --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
8182
+ embed tool call rp2350.monitor.capabilities [--json]
8183
+ embed tool call rp2350.monitor.status [--json]
8184
+ embed tool call rp2350.monitor.gpio.read --input-json '{"pins":[16,17]}' --approve [--json]
8185
+ embed tool call rp2350.monitor.gpio.write --input-json '{"pin":16,"level":true}' --approve [--json]
8186
+ embed tool call rp2350.monitor.uart.write --input-json '{"baud":115200,"text":"hello","line_ending":"lf"}' --approve [--json]
8187
+ embed tool call rp2350.monitor.i2c.transfer --input-json '{"address":"0x50","write":"00","read_len":4}' --approve [--json]
8188
+ embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve [--json]
8189
+ embed tool call rp2350.monitor.logic.capture --input-json '{"pin_base":16,"pin_count":4,"sample_rate":1000000,"samples":4096}' --approve [--json]
8190
+ embed tool call rp2350.monitor.logic.decode --input-json '{"input_path":".embed-labs/rp2350-monitor/captures/logic.jsonl","decoder":"summary"}' [--json]
8191
+ embed tool call rp2350.monitor.wifi.manage --input-json '{"action":"scan"}' --approve [--json]
8192
+ embed tool call rp2350.monitor.probe.debug --input-json '{"action":"status"}' --approve [--json]
8193
+ embed tool call rp2350.monitor.operation --input-json '{"action":"logic.stop","params":{}}' --approve [--json]
6181
8194
  embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
6182
8195
  embed board deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
6183
8196
  embed device list [--json]
@@ -6211,7 +8224,7 @@ Usage:
6211
8224
 
6212
8225
  Environment:
6213
8226
  EMBED_BRIDGE_URL=http://127.0.0.1:18083
6214
- EMBED_CLOUD_API_URL=http://127.0.0.1:18100
8227
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
6215
8228
  EMBED_API_TOKEN=<token>
6216
8229
  CODEX_HOME=~/.codex
6217
8230
  `);