@kvell007/embed-labs-cli 0.1.0-alpha.5 → 0.1.0-alpha.51

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, currentLocalToolchain, installLocalToolchain, latestLocalToolchain, validateLocalToolchain } from "./local-toolchain.js";
11
+ import { buildTaishanPiQtSmoke, compileTaishanPiSingleFile, currentLocalToolchain, installLocalToolchain, latestLocalToolchain, listLocalToolchainEnvironments, uninstallLocalToolchain, 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,23 +97,33 @@ 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_LATEST_USAGE = "Usage: embed local toolchain latest [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--json]";
100
+ const LOCAL_TOOLCHAIN_LIST_USAGE = "Usage: embed local toolchain list [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]";
101
+ const LOCAL_TOOLCHAIN_INSTALLED_USAGE = "Usage: embed local toolchain installed [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]";
102
+ const LOCAL_TOOLCHAIN_LATEST_USAGE = "Usage: embed local toolchain latest [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--json]";
85
103
  const LOCAL_TOOLCHAIN_CURRENT_USAGE = "Usage: embed local toolchain current [--install-root <path>] [--json]";
86
- const LOCAL_TOOLCHAIN_INSTALL_USAGE = "Usage: embed local toolchain install [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--force] [--json]";
87
- const LOCAL_TOOLCHAIN_VALIDATE_USAGE = "Usage: embed local toolchain validate [--release-root <path>] [--json]";
104
+ const LOCAL_TOOLCHAIN_INSTALL_USAGE = "Usage: embed local toolchain install [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-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_UNINSTALL_USAGE = "Usage: embed local toolchain uninstall --board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor [--install-root <path>] [--json]";
106
+ const LOCAL_TOOLCHAIN_VALIDATE_USAGE = "Usage: embed local toolchain validate [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--release-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--json]";
88
107
  const LOCAL_COMPILE_TAISHANPI_USAGE = "Usage: embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]";
89
108
  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]";
109
+ const AUTH_DEVICE_STATUS_USAGE = "Usage: embed auth device status [--json]";
110
+ const AUTH_DEVICE_LIST_USAGE = "Usage: embed auth device list [--json]";
111
+ const AUTH_DEVICE_REVOKE_USAGE = "Usage: embed auth device revoke <device_id> [--json]";
112
+ const AUTH_DEVICE_RENAME_USAGE = "Usage: embed auth device rename <device_id> --label <name> [--json]";
90
113
  const BOARD_REGISTRY_LIST_USAGE = "Usage: embed board registry list [--json]";
91
114
  const BOARD_REGISTRY_SHOW_USAGE = "Usage: embed board registry show <template_id> [--json]";
92
115
  const BOARD_METHODS_USAGE = "Usage: embed board methods <template_id> [--json]";
93
116
  const BOARD_KNOWLEDGE_USAGE = "Usage: embed board knowledge <template_id> [--json]";
117
+ const BOARD_KNOWLEDGE_SEARCH_USAGE = "Usage: embed board knowledge search <template_id> --query <text> [--source board_pack|build_template|registry] [--limit 5] [--json]";
94
118
  const BOARD_KNOWLEDGE_FILE_USAGE = "Usage: embed board knowledge file <template_id> --source board_pack|build_template|registry --path <relative_path> [--output <local_path>] [--json]";
95
119
  const MODEL_LIST_USAGE = "Usage: embed model list [--json]";
96
120
  const MODEL_DEFAULT_USAGE = "Usage: embed model default [--json]";
97
121
  const SERVICE_MODES_USAGE = "Usage: embed service modes [--json]";
98
122
  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]";
123
+ let cachedLocalHardwareFingerprint;
99
124
  const TOOL_LIST_USAGE = "Usage: embed tool list [--json]";
100
125
  const TOOL_CALL_USAGE = "Usage: embed tool call <capability_id> [--input-json '<json>'] [--approve] [--json]";
126
+ 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]";
101
127
  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]";
102
128
  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]";
103
129
  const TASK_STATES = new Set([
@@ -147,11 +173,7 @@ async function main(argv) {
147
173
  return output(parsed, await bridgePost("/v1/board/taishanpi/deploy", request), renderBoardDeployResult);
148
174
  }
149
175
  if (area === "bridge" && action === "start") {
150
- startServer({
151
- host: stringFlag(parsed, "host"),
152
- port: numberFlag(parsed, "port")
153
- });
154
- return await waitForever();
176
+ return await runBridgeStart(parsed);
155
177
  }
156
178
  if (area === "bridge" && action === "status") {
157
179
  return output(parsed, await bridgeGet("/healthz"), renderBridgeStatus);
@@ -216,6 +238,17 @@ async function main(argv) {
216
238
  return output(parsed, await cloudGet(`/v1/board-registry/templates/${encodeURIComponent(idResult.value)}/methods`), renderBoardMethods);
217
239
  }
218
240
  if (action === "knowledge") {
241
+ if (parsed.command[2] === "search") {
242
+ const request = boardKnowledgeSearchRequest(parsed);
243
+ if (typeof request === "string") {
244
+ return output(parsed, fail("invalid_args", request), undefined, 2);
245
+ }
246
+ const params = new URLSearchParams({ q: request.query, limit: String(request.limit) });
247
+ if (request.source) {
248
+ params.set("source", request.source);
249
+ }
250
+ return output(parsed, await cloudGet(`/v1/board-registry/templates/${encodeURIComponent(request.templateId)}/knowledge-search?${params.toString()}`), renderBoardKnowledgeSearch);
251
+ }
219
252
  if (parsed.command[2] === "file") {
220
253
  const request = boardKnowledgeFileRequest(parsed);
221
254
  if (typeof request === "string") {
@@ -241,6 +274,7 @@ async function main(argv) {
241
274
  BOARD_REGISTRY_SHOW_USAGE,
242
275
  BOARD_METHODS_USAGE,
243
276
  BOARD_KNOWLEDGE_USAGE,
277
+ BOARD_KNOWLEDGE_SEARCH_USAGE,
244
278
  BOARD_KNOWLEDGE_FILE_USAGE
245
279
  ].join("\n")), undefined, 2);
246
280
  }
@@ -279,7 +313,18 @@ async function main(argv) {
279
313
  const result = await pluginInstall(parsed);
280
314
  return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
281
315
  }
282
- return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE].join("\n")), undefined, 2);
316
+ if (action === "update") {
317
+ if (parsed.command[2] === "check") {
318
+ const result = await pluginUpdateCheck(parsed);
319
+ return output(parsed, result, renderPluginUpdateCheck, result.ok ? 0 : 2);
320
+ }
321
+ if (["codex", "opencode", "all"].includes(parsed.command[2] ?? "")) {
322
+ const result = await pluginUpdate(parsed);
323
+ return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
324
+ }
325
+ return output(parsed, fail("invalid_args", [PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
326
+ }
327
+ return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE, PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
283
328
  }
284
329
  if (area === "auth" && action === "login") {
285
330
  const result = await authLogin(parsed);
@@ -288,6 +333,31 @@ async function main(argv) {
288
333
  if (area === "auth" && action === "status") {
289
334
  return output(parsed, ok(await authStatus()), renderAuthStatus);
290
335
  }
336
+ if (area === "auth" && action === "device") {
337
+ const deviceAction = parsed.command[2] ?? "status";
338
+ if (deviceAction === "status") {
339
+ const result = await authDeviceStatus(parsed);
340
+ return output(parsed, result, renderAuthDeviceStatus, result.ok ? 0 : 2);
341
+ }
342
+ if (deviceAction === "list") {
343
+ const result = await authDeviceList(parsed);
344
+ return output(parsed, result, renderAuthDeviceList, result.ok ? 0 : 2);
345
+ }
346
+ if (deviceAction === "revoke") {
347
+ const result = await authDeviceRevoke(parsed);
348
+ return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
349
+ }
350
+ if (deviceAction === "rename") {
351
+ const result = await authDeviceRename(parsed);
352
+ return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
353
+ }
354
+ return output(parsed, fail("invalid_args", [
355
+ AUTH_DEVICE_STATUS_USAGE,
356
+ AUTH_DEVICE_LIST_USAGE,
357
+ AUTH_DEVICE_REVOKE_USAGE,
358
+ AUTH_DEVICE_RENAME_USAGE
359
+ ].join("\n")), undefined, 2);
360
+ }
291
361
  if (area === "auth" && action === "logout") {
292
362
  await rm(DEFAULT_AUTH_FILE, { force: true });
293
363
  return output(parsed, ok(await authStatus()), renderAuthStatus);
@@ -367,6 +437,16 @@ async function main(argv) {
367
437
  USAGE_EVENTS_USAGE
368
438
  ].join("\n")), undefined, 2);
369
439
  }
440
+ if (area === "mcp") {
441
+ if (action === "log" || action === "tool-event") {
442
+ const body = mcpToolEventBody(parsed);
443
+ if (typeof body === "string") {
444
+ return output(parsed, fail("invalid_args", body), undefined, 2);
445
+ }
446
+ return output(parsed, await cloudPost("/v1/mcp/tool-events", body), renderMcpToolEvent);
447
+ }
448
+ return output(parsed, fail("invalid_args", MCP_TOOL_EVENT_USAGE), undefined, 2);
449
+ }
370
450
  if (area === "billing") {
371
451
  if (action === "statement") {
372
452
  const request = billingStatementRequest(parsed);
@@ -506,6 +586,20 @@ async function main(argv) {
506
586
  return output(parsed, fail("invalid_args", [IMAGE_BOOT_LOGO_COMPOSE_USAGE, IMAGE_DTB_COMPOSE_USAGE].join("\n")), undefined, 2);
507
587
  }
508
588
  if (area === "local") {
589
+ if (action === "toolchain" && parsed.command[2] === "list") {
590
+ const request = localToolchainListRequest(parsed);
591
+ if (typeof request === "string") {
592
+ return output(parsed, fail("invalid_args", request), undefined, 2);
593
+ }
594
+ return output(parsed, ok(await listLocalToolchainEnvironments(request)), renderLocalToolchainList);
595
+ }
596
+ if (action === "toolchain" && parsed.command[2] === "installed") {
597
+ const request = localToolchainListRequest(parsed, LOCAL_TOOLCHAIN_INSTALLED_USAGE);
598
+ if (typeof request === "string") {
599
+ return output(parsed, fail("invalid_args", request), undefined, 2);
600
+ }
601
+ return output(parsed, ok(await listLocalToolchainEnvironments({ ...request, installedOnly: true })), renderLocalToolchainList);
602
+ }
509
603
  if (action === "toolchain" && parsed.command[2] === "latest") {
510
604
  const request = localToolchainLatestRequest(parsed);
511
605
  if (typeof request === "string") {
@@ -527,12 +621,19 @@ async function main(argv) {
527
621
  }
528
622
  return output(parsed, ok(await installLocalToolchain(request)), renderLocalToolchainInstall);
529
623
  }
624
+ if (action === "toolchain" && (parsed.command[2] === "uninstall" || parsed.command[2] === "remove")) {
625
+ const request = localToolchainUninstallRequest(parsed);
626
+ if (typeof request === "string") {
627
+ return output(parsed, fail("invalid_args", request), undefined, 2);
628
+ }
629
+ return output(parsed, ok(await uninstallLocalToolchain(request)), renderLocalToolchainUninstall);
630
+ }
530
631
  if (action === "toolchain" && parsed.command[2] === "validate") {
531
632
  const request = localToolchainValidateRequest(parsed);
532
633
  if (typeof request === "string") {
533
634
  return output(parsed, fail("invalid_args", request), undefined, 2);
534
635
  }
535
- return output(parsed, ok(await validateLocalToolchain(request.releaseRoot)), renderLocalToolchainValidation);
636
+ return output(parsed, ok(await validateLocalToolchain(request)), renderLocalToolchainValidation);
536
637
  }
537
638
  if (action === "compile" && parsed.command[2] === "taishanpi") {
538
639
  const request = localCompileTaishanPiRequest(parsed, await authStatus());
@@ -549,9 +650,12 @@ async function main(argv) {
549
650
  return output(parsed, ok(await buildTaishanPiQtSmoke(request)), renderLocalCompileResult);
550
651
  }
551
652
  return output(parsed, fail("invalid_args", [
653
+ LOCAL_TOOLCHAIN_LIST_USAGE,
654
+ LOCAL_TOOLCHAIN_INSTALLED_USAGE,
552
655
  LOCAL_TOOLCHAIN_LATEST_USAGE,
553
656
  LOCAL_TOOLCHAIN_CURRENT_USAGE,
554
657
  LOCAL_TOOLCHAIN_INSTALL_USAGE,
658
+ LOCAL_TOOLCHAIN_UNINSTALL_USAGE,
555
659
  LOCAL_TOOLCHAIN_VALIDATE_USAGE,
556
660
  LOCAL_COMPILE_TAISHANPI_USAGE,
557
661
  LOCAL_BUILD_QT_SMOKE_USAGE
@@ -1126,11 +1230,11 @@ async function doctor() {
1126
1230
  const bridgeHealth = await apiDoctorCheck("bridge_health", "Local Bridge health", `${bridgeBaseUrl}/healthz`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderHealthSummary, healthStatus);
1127
1231
  checks.push(bridgeHealth);
1128
1232
  if (isUsableDoctorCheck(bridgeHealth)) {
1129
- checks.push(await apiDoctorCheck("device_scan", "Device scan", `${bridgeBaseUrl}/v1/devices`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDeviceScanSummary, warningIfWarnings));
1233
+ checks.push(await apiDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDeviceScanSummary, warningIfWarnings));
1130
1234
  checks.push(await apiDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDebugToolScanSummary, warningIfWarnings));
1131
1235
  }
1132
1236
  else {
1133
- checks.push(dependentDoctorCheck("device_scan", "Device scan", `${bridgeBaseUrl}/v1/devices`, "Device scan requires a reachable Local Bridge."));
1237
+ checks.push(dependentDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "Device inventory requires a reachable Local Bridge."));
1134
1238
  checks.push(dependentDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "Debug tool scan requires a reachable Local Bridge."));
1135
1239
  }
1136
1240
  checks.push(await apiDoctorCheck("cloud_api_health", "Cloud API health", `${cloudBaseUrl}/healthz`, "cloud_api_unreachable", `Cloud API is unreachable at ${cloudBaseUrl}.`, renderHealthSummary, healthStatus));
@@ -1189,7 +1293,8 @@ function authDoctorCheck(status) {
1189
1293
  : {
1190
1294
  code: "auth_not_ready",
1191
1295
  message: "No CLI auth token is configured.",
1192
- remediation: "Run: embed auth login --token <token>"
1296
+ remediation: cloudAuthSetupRemediation(),
1297
+ details: cloudAuthSetupDetails()
1193
1298
  }
1194
1299
  };
1195
1300
  }
@@ -1322,7 +1427,7 @@ function warningIfWarnings(data) {
1322
1427
  }
1323
1428
  function renderDeviceScanSummary(result) {
1324
1429
  const warningText = result.warnings?.length ? ` ${result.warnings.length} warning(s).` : "";
1325
- return `Device scan completed: ${result.devices.length} device(s), ${result.usb.length} USB item(s), ${result.serial.length} serial port(s).${warningText}`;
1430
+ return `Device inventory snapshot: ${result.devices.length} device(s), ${result.usb.length} USB item(s), ${result.serial.length} serial port(s).${warningText}`;
1326
1431
  }
1327
1432
  function renderDebugToolScanSummary(result) {
1328
1433
  const available = result.tools.filter((tool) => tool.available).length;
@@ -1340,16 +1445,142 @@ function isApiResponse(value) {
1340
1445
  return isJsonObject(error) && typeof error.code === "string" && typeof error.message === "string";
1341
1446
  }
1342
1447
  async function bridgeGet(path) {
1343
- const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`);
1344
- return await response.json();
1448
+ return await bridgeRequest("GET", path);
1345
1449
  }
1346
1450
  async function bridgePost(path, body) {
1347
- const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
1348
- method: "POST",
1349
- headers: { "content-type": "application/json" },
1350
- body: JSON.stringify(body)
1451
+ return await bridgeRequest("POST", path, body);
1452
+ }
1453
+ async function bridgeRequest(method, path, body) {
1454
+ const bodyText = body === undefined ? "" : JSON.stringify(body);
1455
+ const makeRequest = async () => {
1456
+ const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
1457
+ method,
1458
+ headers: bridgeHeaders(method, path, method === "POST" ? bodyText : "", method === "POST" ? { "content-type": "application/json" } : {}),
1459
+ body: method === "POST" ? bodyText : undefined
1460
+ });
1461
+ return await response.json();
1462
+ };
1463
+ try {
1464
+ return await makeRequest();
1465
+ }
1466
+ catch (error) {
1467
+ if (!isBridgeConnectionFailure(error)) {
1468
+ throw error;
1469
+ }
1470
+ const started = await ensureBridgeStartedForRequest();
1471
+ if (!started.ok) {
1472
+ return started;
1473
+ }
1474
+ return await makeRequest();
1475
+ }
1476
+ }
1477
+ function bridgeHeaders(method, path, bodyText, base = {}) {
1478
+ const token = process.env.EMBED_BRIDGE_TOKEN?.trim();
1479
+ if (!token) {
1480
+ return base;
1481
+ }
1482
+ const headers = {
1483
+ ...base,
1484
+ authorization: `Bearer ${token}`
1485
+ };
1486
+ addBridgeRequestSignature(headers, method, path, bodyText, token);
1487
+ return headers;
1488
+ }
1489
+ function addBridgeRequestSignature(headers, method, pathWithQuery, bodyText, token) {
1490
+ if (process.env.EMBED_BRIDGE_SIGNING === "0") {
1491
+ return;
1492
+ }
1493
+ const timestamp = String(Math.floor(Date.now() / 1000));
1494
+ const nonce = randomBytes(16).toString("hex");
1495
+ const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
1496
+ const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
1497
+ const canonical = cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1498
+ headers["x-embed-key-id"] = keyId;
1499
+ headers["x-embed-timestamp"] = timestamp;
1500
+ headers["x-embed-nonce"] = nonce;
1501
+ headers["x-embed-body-sha256"] = bodySha256;
1502
+ headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
1503
+ }
1504
+ function isBridgeConnectionFailure(error) {
1505
+ const message = error instanceof Error ? error.message : String(error);
1506
+ return message.includes("fetch failed") ||
1507
+ message.includes("ECONNREFUSED") ||
1508
+ message.includes("ECONNRESET") ||
1509
+ message.includes("UND_ERR_SOCKET");
1510
+ }
1511
+ async function ensureBridgeStartedForRequest() {
1512
+ if (process.env.EMBED_BRIDGE_AUTO_START === "0") {
1513
+ return fail("bridge_unavailable", `embed-local-bridge is not running at ${DEFAULT_BRIDGE_URL}.`, {
1514
+ remediation: `Start it with: embed bridge start`
1515
+ });
1516
+ }
1517
+ let bridgeURL;
1518
+ try {
1519
+ bridgeURL = new URL(DEFAULT_BRIDGE_URL);
1520
+ }
1521
+ catch {
1522
+ return fail("bridge_url_invalid", `EMBED_BRIDGE_URL is not a valid URL: ${DEFAULT_BRIDGE_URL}`);
1523
+ }
1524
+ if (!isLocalBridgeURL(bridgeURL)) {
1525
+ return fail("bridge_unavailable", `embed-local-bridge is not reachable at ${DEFAULT_BRIDGE_URL}.`, {
1526
+ remediation: `Start the bridge for that host, or set EMBED_BRIDGE_URL to a local bridge URL.`
1527
+ });
1528
+ }
1529
+ const launcher = await resolveBridgeLauncher();
1530
+ const host = bridgeURL.hostname === "::1" ? "::1" : bridgeURL.hostname || "127.0.0.1";
1531
+ const port = bridgeURL.port || "18083";
1532
+ const env = {
1533
+ ...process.env,
1534
+ EMBED_BRIDGE_HOST: host,
1535
+ EMBED_BRIDGE_PORT: port
1536
+ };
1537
+ const child = spawn(launcher.command, [...launcher.args, "--host", host, "--port", port], {
1538
+ cwd: process.cwd(),
1539
+ detached: true,
1540
+ stdio: "ignore",
1541
+ env
1542
+ });
1543
+ child.unref();
1544
+ const ready = await waitForBridgeHealth(bridgeURL, 8000);
1545
+ if (!ready.ok) {
1546
+ return ready;
1547
+ }
1548
+ return ok({
1549
+ started: true,
1550
+ bridge_url: DEFAULT_BRIDGE_URL,
1551
+ command: launcher.command
1552
+ });
1553
+ }
1554
+ function isLocalBridgeURL(url) {
1555
+ const host = url.hostname.toLowerCase();
1556
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
1557
+ }
1558
+ async function waitForBridgeHealth(bridgeURL, timeoutMs) {
1559
+ const deadline = Date.now() + timeoutMs;
1560
+ let lastError = "";
1561
+ while (Date.now() < deadline) {
1562
+ try {
1563
+ const response = await fetch(new URL("/healthz", bridgeURL), {
1564
+ headers: bridgeHeaders("GET", "/healthz", "")
1565
+ });
1566
+ const parsed = await response.json();
1567
+ if (parsed.ok) {
1568
+ return parsed;
1569
+ }
1570
+ lastError = parsed.error?.message ?? `HTTP ${response.status}`;
1571
+ }
1572
+ catch (error) {
1573
+ lastError = error instanceof Error ? error.message : String(error);
1574
+ }
1575
+ await delay(100);
1576
+ }
1577
+ return fail("bridge_start_failed", `embed-local-bridge did not become healthy at ${DEFAULT_BRIDGE_URL}.`, {
1578
+ remediation: `Run embed bridge start in a separate terminal and retry.`,
1579
+ details: { last_error: lastError }
1351
1580
  });
1352
- return await response.json();
1581
+ }
1582
+ function delay(ms) {
1583
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
1353
1584
  }
1354
1585
  async function cloudGet(path) {
1355
1586
  return await cloudRequest("GET", path);
@@ -1360,16 +1591,23 @@ async function cloudPost(path, body) {
1360
1591
  async function cloudDownloadArtifact(artifactId, outputPath) {
1361
1592
  try {
1362
1593
  const headers = {};
1363
- const token = await cloudAuthToken();
1364
- if (token) {
1365
- headers.authorization = `Bearer ${token}`;
1594
+ const auth = await cloudAuthConfig();
1595
+ if (auth.token) {
1596
+ if (auth.device) {
1597
+ const integrity = await validateLocalDeviceIntegrity(auth.device);
1598
+ if (!integrity.ok) {
1599
+ return integrity;
1600
+ }
1601
+ }
1602
+ headers.authorization = `Bearer ${auth.token}`;
1603
+ addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", auth.token, auth.device);
1366
1604
  }
1367
1605
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/artifacts/${encodeURIComponent(artifactId)}/download`, {
1368
1606
  headers: Object.keys(headers).length > 0 ? headers : undefined
1369
1607
  });
1370
1608
  if (!response.ok) {
1371
1609
  const parsed = await parseErrorResponse(response);
1372
- return parsed ?? fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
1610
+ return parsed ? enrichCloudAuthFailure(parsed, Boolean(auth.token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
1373
1611
  }
1374
1612
  const bytes = Buffer.from(await response.arrayBuffer());
1375
1613
  const expectedSha256 = response.headers.get("x-embed-artifact-sha256")?.trim();
@@ -1398,19 +1636,28 @@ async function cloudDownloadArtifact(artifactId, outputPath) {
1398
1636
  async function cloudRequest(method, path, body) {
1399
1637
  try {
1400
1638
  const headers = {};
1401
- if (body !== undefined) {
1639
+ const bodyText = body === undefined ? "" : JSON.stringify(body);
1640
+ if (bodyText) {
1402
1641
  headers["content-type"] = "application/json";
1403
1642
  }
1404
- const token = await cloudAuthToken();
1405
- if (token) {
1406
- headers.authorization = `Bearer ${token}`;
1643
+ const auth = await cloudAuthConfig();
1644
+ if (auth.token) {
1645
+ if (auth.device) {
1646
+ const integrity = await validateLocalDeviceIntegrity(auth.device);
1647
+ if (!integrity.ok) {
1648
+ return integrity;
1649
+ }
1650
+ }
1651
+ headers.authorization = `Bearer ${auth.token}`;
1652
+ addCloudRequestSignature(headers, method, path, bodyText, auth.token, auth.device);
1407
1653
  }
1408
1654
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}${path}`, {
1409
1655
  method,
1410
1656
  headers: Object.keys(headers).length > 0 ? headers : undefined,
1411
- body: body === undefined ? undefined : JSON.stringify(body)
1657
+ body: body === undefined ? undefined : bodyText
1412
1658
  });
1413
- return await response.json();
1659
+ const parsed = await response.json();
1660
+ return enrichCloudAuthFailure(parsed, Boolean(auth.token));
1414
1661
  }
1415
1662
  catch (error) {
1416
1663
  return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
@@ -1418,6 +1665,129 @@ async function cloudRequest(method, path, body) {
1418
1665
  });
1419
1666
  }
1420
1667
  }
1668
+ async function validateLocalDeviceIntegrity(device) {
1669
+ const current = await localHardwareFingerprint();
1670
+ if (current.fingerprint_hash === device.fingerprint_hash) {
1671
+ return ok(undefined);
1672
+ }
1673
+ return fail("tool_integrity_check_failed", TOOL_INTEGRITY_RELOGIN_MESSAGE, {
1674
+ remediation: [
1675
+ "当前 Embed Labs CLI/插件配置绑定的电脑与本机硬件唯一码不一致。",
1676
+ TOOL_INTEGRITY_RELOGIN_MESSAGE,
1677
+ "如果账号设备数量已达上限,请先在原电脑或用户后台撤销旧设备。"
1678
+ ].join("\n"),
1679
+ details: {
1680
+ expected_fingerprint_hash: device.fingerprint_hash,
1681
+ current_fingerprint_hash: current.fingerprint_hash,
1682
+ platform: current.platform,
1683
+ arch: current.arch,
1684
+ fingerprint_source: current.source
1685
+ }
1686
+ });
1687
+ }
1688
+ function enrichCloudAuthFailure(response, hadToken) {
1689
+ if (response.ok) {
1690
+ return response;
1691
+ }
1692
+ if (response.error.code.startsWith("device_") || response.error.code.startsWith("request_signature_")) {
1693
+ return fail(response.error.code, response.error.message, {
1694
+ remediation: [
1695
+ "This computer is not fully registered for the configured Embed Labs API Token.",
1696
+ "Run: embedlabs auth login --token <your_token>",
1697
+ "Then verify with: embedlabs auth device status",
1698
+ "If the account already has too many devices, revoke one with: embedlabs auth device revoke <device_id>"
1699
+ ].join("\n"),
1700
+ details: response.error.details
1701
+ });
1702
+ }
1703
+ if (response.error.code !== "unauthorized") {
1704
+ return response;
1705
+ }
1706
+ if (!hadToken) {
1707
+ 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.", {
1708
+ remediation: cloudAuthSetupRemediation(),
1709
+ details: cloudAuthSetupDetails()
1710
+ });
1711
+ }
1712
+ 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.", {
1713
+ remediation: cloudAuthSetupRemediation(),
1714
+ details: cloudAuthSetupDetails()
1715
+ });
1716
+ }
1717
+ function cloudAuthSetupRemediation() {
1718
+ return [
1719
+ `1. Open ${DEFAULT_DASHBOARD_URL} and register or sign in.`,
1720
+ "2. Create or copy your Embed Labs API Token from the user dashboard.",
1721
+ "3. Run: embedlabs auth login --token <your_token>",
1722
+ "4. For automation, set: EMBED_API_TOKEN=<your_token>",
1723
+ "5. Verify with: embedlabs auth status"
1724
+ ].join("\n");
1725
+ }
1726
+ function cloudAuthSetupDetails() {
1727
+ return {
1728
+ dashboard_url: DEFAULT_DASHBOARD_URL,
1729
+ login_command: "embedlabs auth login --token <your_token>",
1730
+ env_var: "EMBED_API_TOKEN",
1731
+ auth_status_command: "embedlabs auth status",
1732
+ auth_file: DEFAULT_AUTH_FILE
1733
+ };
1734
+ }
1735
+ function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token, device) {
1736
+ if (process.env.EMBED_CLOUD_API_SIGNING === "0") {
1737
+ return;
1738
+ }
1739
+ const timestamp = String(Math.floor(Date.now() / 1000));
1740
+ const nonce = randomBytes(16).toString("hex");
1741
+ const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
1742
+ const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
1743
+ const canonical = device
1744
+ ? cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, EMBED_CLIENT_NAME, EMBED_CLIENT_VERSION)
1745
+ : cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1746
+ headers["x-embed-key-id"] = keyId;
1747
+ headers["x-embed-timestamp"] = timestamp;
1748
+ headers["x-embed-nonce"] = nonce;
1749
+ headers["x-embed-body-sha256"] = bodySha256;
1750
+ if (device) {
1751
+ headers["x-embed-signature-version"] = "v2";
1752
+ headers["x-embed-device-id"] = device.device_id;
1753
+ headers["x-embed-device-fingerprint-sha256"] = device.fingerprint_hash;
1754
+ headers["x-embed-client-name"] = EMBED_CLIENT_NAME;
1755
+ headers["x-embed-client-version"] = EMBED_CLIENT_VERSION;
1756
+ headers["x-embed-device-signature"] = signCrypto(null, Buffer.from(canonical), device.private_key_pem).toString("base64url");
1757
+ }
1758
+ headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
1759
+ }
1760
+ function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256) {
1761
+ return [
1762
+ method.toUpperCase(),
1763
+ normalizeCloudPathForSignature(pathWithQuery),
1764
+ timestamp,
1765
+ nonce,
1766
+ bodySha256
1767
+ ].join("\n");
1768
+ }
1769
+ function cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, clientName, clientVersion) {
1770
+ return [
1771
+ method.toUpperCase(),
1772
+ normalizeCloudPathForSignature(pathWithQuery),
1773
+ timestamp,
1774
+ nonce,
1775
+ bodySha256,
1776
+ device.device_id,
1777
+ device.fingerprint_hash,
1778
+ clientName,
1779
+ clientVersion
1780
+ ].join("\n");
1781
+ }
1782
+ function normalizeCloudPathForSignature(pathWithQuery) {
1783
+ try {
1784
+ const parsed = new URL(pathWithQuery, "http://embed.local");
1785
+ return `${parsed.pathname}${parsed.search}`;
1786
+ }
1787
+ catch {
1788
+ return pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`;
1789
+ }
1790
+ }
1421
1791
  async function pluginList(parsed) {
1422
1792
  const releaseDir = stringFlag(parsed, "release-dir");
1423
1793
  const manifest = releaseDir ? await readPluginReleaseManifest(releaseDir) : undefined;
@@ -1517,13 +1887,112 @@ async function pluginInstall(parsed) {
1517
1887
  await rm(tempDir, { recursive: true, force: true });
1518
1888
  }
1519
1889
  }
1890
+ async function pluginUpdateCheck(parsed) {
1891
+ const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
1892
+ if (unknownFlag) {
1893
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
1894
+ }
1895
+ const unexpected = parsed.command.slice(3);
1896
+ if (unexpected.length > 0) {
1897
+ return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
1898
+ }
1899
+ const remoteManifest = await fetchRemotePluginManifest(parsed);
1900
+ if (!remoteManifest.ok) {
1901
+ return remoteManifest;
1902
+ }
1903
+ const manifest = remoteManifest.data;
1904
+ const codexPackage = manifest.packages?.find((item) => item.id === "codex-embed-labs");
1905
+ const opencodePackage = manifest.packages?.find((item) => item.id === "opencode-embed-labs");
1906
+ const codexTarget = join(codexPluginTargetRoot(parsed, true), CODEX_PLUGIN_NAME);
1907
+ const openCodeTarget = openCodePluginTargetRoot(parsed, true);
1908
+ return ok({
1909
+ release_url: pluginReleaseBaseUrl(parsed),
1910
+ latest_version: manifest.version,
1911
+ release_notes: normalizedReleaseNotes(manifest.release_notes),
1912
+ plugins: [
1913
+ await pluginUpdateItem({
1914
+ id: "codex",
1915
+ displayName: "Embed Labs Codex plugin",
1916
+ targetPath: codexTarget,
1917
+ installedVersion: await installedCodexPluginVersion(codexTarget),
1918
+ latestVersion: codexPackage?.version ?? manifest.version,
1919
+ releaseFile: codexPackage?.file,
1920
+ updateCommand: "embedlabs plugin update codex"
1921
+ }),
1922
+ await pluginUpdateItem({
1923
+ id: "opencode",
1924
+ displayName: "Embed Labs OpenCode plugin",
1925
+ targetPath: openCodeTarget,
1926
+ installedVersion: await installedOpenCodePluginVersion(openCodeTarget),
1927
+ latestVersion: opencodePackage?.version ?? manifest.version,
1928
+ releaseFile: opencodePackage?.file,
1929
+ updateCommand: "embedlabs plugin update opencode"
1930
+ })
1931
+ ]
1932
+ });
1933
+ }
1934
+ function normalizedReleaseNotes(notes) {
1935
+ if (!Array.isArray(notes)) {
1936
+ return [];
1937
+ }
1938
+ return notes
1939
+ .filter((item) => typeof item === "string" && item.trim().length > 0)
1940
+ .map((item) => item.trim());
1941
+ }
1942
+ async function pluginUpdate(parsed) {
1943
+ const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
1944
+ if (unknownFlag) {
1945
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_USAGE}`);
1946
+ }
1947
+ const target = parsed.command[2];
1948
+ if (!target || !["codex", "opencode", "all"].includes(target)) {
1949
+ return fail("invalid_args", PLUGIN_UPDATE_USAGE);
1950
+ }
1951
+ const unexpected = parsed.command.slice(3);
1952
+ if (unexpected.length > 0) {
1953
+ return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_USAGE}`);
1954
+ }
1955
+ const installParsed = {
1956
+ ...parsed,
1957
+ command: ["plugin", "install", target],
1958
+ flags: { ...parsed.flags, force: true }
1959
+ };
1960
+ return await pluginInstall(installParsed);
1961
+ }
1962
+ async function pluginUpdateItem(input) {
1963
+ const installed = !!input.installedVersion;
1964
+ const updateAvailable = !!input.latestVersion && input.installedVersion !== input.latestVersion;
1965
+ const notes = [];
1966
+ if (!installed) {
1967
+ notes.push("Plugin is not installed in the selected target.");
1968
+ }
1969
+ else if (updateAvailable) {
1970
+ notes.push("A newer plugin release is available. Run the update command, then restart Codex/OpenCode.");
1971
+ }
1972
+ else {
1973
+ notes.push("Plugin is up to date for the selected release channel.");
1974
+ }
1975
+ return {
1976
+ id: input.id,
1977
+ display_name: input.displayName,
1978
+ installed,
1979
+ installed_version: input.installedVersion,
1980
+ latest_version: input.latestVersion,
1981
+ update_available: updateAvailable,
1982
+ target_path: input.targetPath,
1983
+ release_file: input.releaseFile,
1984
+ update_command: input.updateCommand,
1985
+ notes
1986
+ };
1987
+ }
1520
1988
  async function installCodexPlugin(parsed, context) {
1521
1989
  const source = await resolveCodexPluginSource(context);
1522
1990
  if (!source.ok) {
1523
1991
  return source;
1524
1992
  }
1525
1993
  const targetRoot = codexPluginTargetRoot(parsed, context.installingAll);
1526
- const targetPath = join(targetRoot, "embed-labs");
1994
+ const targetPath = join(targetRoot, CODEX_PLUGIN_NAME);
1995
+ const legacyCleanup = await cleanupLegacyCodexPluginRemnants(targetRoot);
1527
1996
  if (await pathExists(targetPath) && !booleanFlag(parsed, "force")) {
1528
1997
  return fail("plugin_already_installed", `Codex plugin already exists at ${targetPath}.`, {
1529
1998
  remediation: "Pass --force to replace it, or pass --codex-target/--target to install into a different directory."
@@ -1533,16 +2002,24 @@ async function installCodexPlugin(parsed, context) {
1533
2002
  await mkdir(targetRoot, { recursive: true });
1534
2003
  await cp(source.data.sourcePath, targetPath, { recursive: true });
1535
2004
  const mcpRegistration = await maybeRegisterCodexMcp(parsed, targetRoot, targetPath);
2005
+ const marketplaceRegistration = await maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath);
1536
2006
  return ok({
1537
2007
  id: "codex",
1538
2008
  target_path: targetPath,
1539
2009
  source: source.data.sourceLabel,
1540
2010
  version: source.data.version,
1541
2011
  command_hint: mcpRegistration.registered
1542
- ? "Codex MCP was registered. Start a new Codex session to reload tools."
2012
+ ? (marketplaceRegistration.registered
2013
+ ? "Codex MCP and plugin marketplace entry were registered. Fully restart Codex to reload @Embed Labs."
2014
+ : "Codex MCP was registered. Start a new Codex session to reload tools.")
1543
2015
  : mcpRegistration.hint,
2016
+ warning: legacyCodexCleanupWarning(legacyCleanup),
1544
2017
  mcp_registered: mcpRegistration.registered,
1545
- mcp_warning: mcpRegistration.warning
2018
+ mcp_warning: mcpRegistration.warning,
2019
+ marketplace_registered: marketplaceRegistration.registered,
2020
+ marketplace_path: marketplaceRegistration.marketplacePath,
2021
+ marketplace_warning: marketplaceRegistration.warning,
2022
+ cleanup: legacyCleanup
1546
2023
  });
1547
2024
  }
1548
2025
  async function installOpenCodePlugin(parsed, context) {
@@ -1551,8 +2028,10 @@ async function installOpenCodePlugin(parsed, context) {
1551
2028
  return source;
1552
2029
  }
1553
2030
  const targetRoot = openCodePluginTargetRoot(parsed, context.installingAll);
1554
- const wrapperPath = join(targetRoot, "plugins", "development-board-toolchain.js");
1555
- if (await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
2031
+ const globalInstall = isGlobalOpenCodeRoot(targetRoot);
2032
+ const wrapperPath = join(targetRoot, "plugins", "embed-labs.js");
2033
+ const legacyCleanup = await cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall);
2034
+ if (!globalInstall && await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
1556
2035
  return fail("plugin_already_installed", `OpenCode plugin wrapper already exists at ${wrapperPath}.`, {
1557
2036
  remediation: "Pass --force to replace it, or pass --opencode-target/--target to install into a different directory."
1558
2037
  });
@@ -1578,15 +2057,25 @@ async function installOpenCodePlugin(parsed, context) {
1578
2057
  });
1579
2058
  }
1580
2059
  await ensureOpenCodeInstallPackageJson(targetRoot);
1581
- await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
2060
+ if (globalInstall) {
2061
+ await rm(wrapperPath, { force: true });
2062
+ legacyCleanup.legacy_removed_config_entries?.push(...await ensureOpenCodeGlobalPluginConfig());
2063
+ }
2064
+ else {
2065
+ await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
2066
+ }
1582
2067
  const duplicateWarning = await openCodeDuplicatePluginWarning(targetRoot);
2068
+ const cleanupWarning = legacyOpenCodeCleanupWarning(legacyCleanup);
1583
2069
  return ok({
1584
2070
  id: "opencode",
1585
2071
  target_path: targetRoot,
1586
2072
  source: source.data.sourceLabel,
1587
2073
  version: source.data.version,
1588
- command_hint: "Start OpenCode from the project containing this .opencode directory.",
1589
- warning: duplicateWarning
2074
+ command_hint: globalInstall
2075
+ ? "Restart OpenCode so the global embed-labs package plugin is reloaded."
2076
+ : "Start OpenCode from the project containing this .opencode directory.",
2077
+ warning: combineWarnings(cleanupWarning, duplicateWarning),
2078
+ cleanup: legacyCleanup
1590
2079
  });
1591
2080
  }
1592
2081
  async function resolveCodexPluginSource(context) {
@@ -1800,7 +2289,458 @@ function openCodePluginTargetRoot(parsed, installingAll) {
1800
2289
  return resolve(target ?? defaultOpenCodeRoot());
1801
2290
  }
1802
2291
  function defaultCodexPluginRoot() {
1803
- return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "plugins");
2292
+ return join(defaultCodexHome(), "plugins");
2293
+ }
2294
+ function defaultCodexHome() {
2295
+ return resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
2296
+ }
2297
+ function codexConfigPath() {
2298
+ return join(defaultCodexHome(), "config.toml");
2299
+ }
2300
+ async function cleanupLegacyCodexPluginRemnants(targetRoot) {
2301
+ const removedPaths = [];
2302
+ const removedConfigTables = [];
2303
+ const warnings = [];
2304
+ const stoppedProcesses = await stopLegacyCodexPluginProcesses(warnings);
2305
+ const legacyPaths = [
2306
+ join(targetRoot, "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME)
2307
+ ];
2308
+ for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
2309
+ legacyPaths.push(join(targetRoot, "cache", marketplaceName, CODEX_PLUGIN_NAME));
2310
+ }
2311
+ for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
2312
+ legacyPaths.push(join(targetRoot, pluginName));
2313
+ legacyPaths.push(join(targetRoot, "cache", pluginName));
2314
+ for (const marketplaceName of [CODEX_MARKETPLACE_NAME, ...LEGACY_CODEX_MARKETPLACE_NAMES]) {
2315
+ legacyPaths.push(join(targetRoot, "cache", marketplaceName, pluginName));
2316
+ }
2317
+ }
2318
+ legacyPaths.push(...await discoverLegacyCodexCachePaths(targetRoot));
2319
+ if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
2320
+ 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"));
2321
+ legacyPaths.push(...legacyCodexLocalMarketplacePaths(), ...await discoverLegacyHomeAgentsMarketplacePaths(warnings), ...await discoverLegacyCodexProjectMarketplacePaths(warnings), ...legacyDevelopmentBoardRuntimePluginPaths());
2322
+ }
2323
+ for (const candidate of legacyPaths) {
2324
+ try {
2325
+ if (await pathExists(candidate)) {
2326
+ await rm(candidate, { recursive: true, force: true });
2327
+ removedPaths.push(candidate);
2328
+ }
2329
+ }
2330
+ catch (error) {
2331
+ warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2332
+ }
2333
+ }
2334
+ if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
2335
+ const configPath = codexConfigPath();
2336
+ try {
2337
+ if (await pathExists(configPath)) {
2338
+ const current = await readFile(configPath, "utf8");
2339
+ const updated = removeLegacyCodexConfigTables(current);
2340
+ if (updated.text !== current) {
2341
+ await writeFile(configPath, updated.text, "utf8");
2342
+ }
2343
+ removedConfigTables.push(...updated.removedTables);
2344
+ }
2345
+ }
2346
+ catch (error) {
2347
+ warnings.push(`Could not update ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
2348
+ }
2349
+ }
2350
+ const removedHistoryEntries = await cleanupLegacyCodexTextState(warnings);
2351
+ return {
2352
+ legacy_removed_paths: Array.from(new Set(removedPaths)),
2353
+ legacy_removed_config_tables: removedConfigTables,
2354
+ legacy_removed_history_entries: removedHistoryEntries,
2355
+ legacy_stopped_processes: stoppedProcesses,
2356
+ ...(warnings.length > 0 ? { warnings } : {})
2357
+ };
2358
+ }
2359
+ async function stopLegacyCodexPluginProcesses(warnings) {
2360
+ if (process.platform === "win32")
2361
+ return 0;
2362
+ try {
2363
+ const ps = await runLocalProcess("ps", ["-axo", "pid=,command="]);
2364
+ if (ps.code !== 0)
2365
+ return 0;
2366
+ let stopped = 0;
2367
+ for (const line of ps.stdout.split("\n")) {
2368
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
2369
+ if (!match)
2370
+ continue;
2371
+ const pid = Number(match[1]);
2372
+ const command = match[2] || "";
2373
+ if (!isLegacyCodexPluginProcess(command))
2374
+ continue;
2375
+ try {
2376
+ process.kill(pid, "SIGTERM");
2377
+ stopped += 1;
2378
+ }
2379
+ catch (error) {
2380
+ warnings.push(`Could not stop legacy Codex plugin process ${pid}: ${error instanceof Error ? error.message : String(error)}`);
2381
+ }
2382
+ }
2383
+ return stopped;
2384
+ }
2385
+ catch (error) {
2386
+ warnings.push(`Could not scan legacy Codex plugin processes: ${error instanceof Error ? error.message : String(error)}`);
2387
+ return 0;
2388
+ }
2389
+ }
2390
+ function isLegacyCodexPluginProcess(command) {
2391
+ const trimmed = command.trim();
2392
+ return /^\/.*\/dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
2393
+ || /^dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
2394
+ || legacyDevelopmentBoardRuntimeProcessPatterns().some((pattern) => pattern.test(trimmed));
2395
+ }
2396
+ function legacyDevelopmentBoardRuntimeProcessPatterns() {
2397
+ const home = escapeRegExp(homedir());
2398
+ return [
2399
+ new RegExp(`^${home}/Library/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
2400
+ new RegExp(`^${home}/Library/Application Support/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
2401
+ new RegExp(`^${home}/Library/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
2402
+ new RegExp(`^${home}/Library/Application Support/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
2403
+ new RegExp(`^${home}/.*?/DBT-Agent\\.app/Contents/MacOS/DBT-Agent(?:\\s|$)`)
2404
+ ];
2405
+ }
2406
+ function legacyDevelopmentBoardRuntimePluginPaths() {
2407
+ return [
2408
+ join(homedir(), "Library", "development-board-toolchain", "runtime", "editor_plugins"),
2409
+ join(homedir(), "Library", "development-board-toolchain", "runtime", "opencode_plugin"),
2410
+ join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "editor_plugins"),
2411
+ join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "opencode_plugin")
2412
+ ];
2413
+ }
2414
+ function legacyCodexLocalMarketplacePaths() {
2415
+ return Array.from(LEGACY_CODEX_MARKETPLACE_NAMES)
2416
+ .filter((name) => name !== CODEX_MARKETPLACE_NAME)
2417
+ .map((name) => join(defaultCodexHome(), "local-marketplaces", name));
2418
+ }
2419
+ async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
2420
+ const paths = [];
2421
+ const pluginRoot = join(homedir(), ".agents", "plugins");
2422
+ try {
2423
+ const entries = await readdir(pluginRoot, { withFileTypes: true });
2424
+ for (const entry of entries) {
2425
+ if (!entry.isFile() || !entry.name.startsWith("marketplace.json"))
2426
+ continue;
2427
+ const filePath = join(pluginRoot, entry.name);
2428
+ try {
2429
+ const current = await readFile(filePath, "utf8");
2430
+ if (isLegacyHomeAgentsMarketplace(current)) {
2431
+ paths.push(filePath);
2432
+ }
2433
+ }
2434
+ catch (error) {
2435
+ warnings.push(`Could not inspect legacy Codex home marketplace ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2436
+ }
2437
+ }
2438
+ }
2439
+ catch {
2440
+ return paths;
2441
+ }
2442
+ return paths;
2443
+ }
2444
+ function isLegacyHomeAgentsMarketplace(text) {
2445
+ return marketplaceTextHasLegacyCodexPlugin(text);
2446
+ }
2447
+ async function discoverLegacyCodexProjectMarketplacePaths(warnings) {
2448
+ const configPath = codexConfigPath();
2449
+ let text = "";
2450
+ try {
2451
+ text = await readFile(configPath, "utf8");
2452
+ }
2453
+ catch {
2454
+ return [];
2455
+ }
2456
+ const paths = new Set();
2457
+ for (const projectPath of legacyCodexProjectPathsFromConfig(text)) {
2458
+ for (const candidate of [
2459
+ join(projectPath, ".agents", "plugins", "marketplace.json"),
2460
+ join(projectPath, "platform_plugin", ".agents", "plugins", "marketplace.json"),
2461
+ join(projectPath, "platform_plugins", "codex_plugin", ".agents", "plugins", "marketplace.json")
2462
+ ]) {
2463
+ try {
2464
+ if (!await pathExists(candidate))
2465
+ continue;
2466
+ const current = await readFile(candidate, "utf8");
2467
+ if (marketplaceTextHasLegacyCodexPlugin(current)) {
2468
+ paths.add(candidate);
2469
+ }
2470
+ }
2471
+ catch (error) {
2472
+ warnings.push(`Could not inspect legacy Codex project marketplace ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2473
+ }
2474
+ }
2475
+ }
2476
+ return Array.from(paths);
2477
+ }
2478
+ function legacyCodexProjectPathsFromConfig(text) {
2479
+ const paths = [];
2480
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2481
+ for (const line of lines) {
2482
+ const table = parseTomlTableHeader(line);
2483
+ const match = table ? /^projects\."([^"]+)"$/.exec(table) : undefined;
2484
+ if (match?.[1] && /DBT-Agent-Project|development-board-toolchain|dbt-agent/i.test(match[1])) {
2485
+ paths.push(match[1].replace(/\\"/g, '"'));
2486
+ }
2487
+ }
2488
+ return paths;
2489
+ }
2490
+ function marketplaceTextHasLegacyCodexPlugin(text) {
2491
+ try {
2492
+ const parsed = JSON.parse(text);
2493
+ const marketplaceName = typeof parsed.name === "string" ? parsed.name : "";
2494
+ const marketplaceDisplayName = typeof parsed.interface?.displayName === "string" ? parsed.interface.displayName : "";
2495
+ const marketplaceLooksLegacy = legacyTextHasCodexPluginResidue(marketplaceName)
2496
+ || legacyTextHasCodexPluginResidue(marketplaceDisplayName)
2497
+ || LEGACY_CODEX_MARKETPLACE_NAMES.has(marketplaceName);
2498
+ return (parsed.plugins ?? []).some((plugin) => {
2499
+ const values = [
2500
+ plugin.name,
2501
+ plugin.category,
2502
+ plugin.source?.path,
2503
+ plugin.interface?.displayName
2504
+ ].filter((value) => typeof value === "string");
2505
+ return values.some(legacyTextHasCodexPluginResidue) || marketplaceLooksLegacy && values.some((value) => /embed-labs|dbt|development-board/i.test(value));
2506
+ });
2507
+ }
2508
+ catch {
2509
+ return legacyTextHasCodexPluginResidue(text);
2510
+ }
2511
+ }
2512
+ async function discoverLegacyCodexCachePaths(targetRoot) {
2513
+ const paths = [];
2514
+ const cacheRoot = join(targetRoot, "cache");
2515
+ try {
2516
+ const marketplaces = await readdir(cacheRoot, { withFileTypes: true });
2517
+ for (const entry of marketplaces) {
2518
+ if (!entry.isDirectory())
2519
+ continue;
2520
+ if (LEGACY_CODEX_MARKETPLACE_NAMES.has(entry.name)) {
2521
+ paths.push(join(cacheRoot, entry.name, CODEX_PLUGIN_NAME));
2522
+ }
2523
+ for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
2524
+ paths.push(join(cacheRoot, entry.name, pluginName));
2525
+ }
2526
+ }
2527
+ }
2528
+ catch {
2529
+ return paths;
2530
+ }
2531
+ return paths;
2532
+ }
2533
+ async function cleanupLegacyCodexTextState(warnings) {
2534
+ let removed = 0;
2535
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "history.jsonl"), warnings);
2536
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "session_index.jsonl"), warnings);
2537
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "rules", "default.rules"), warnings);
2538
+ return removed;
2539
+ }
2540
+ async function cleanupLegacyCodexTextFile(filePath, warnings) {
2541
+ try {
2542
+ if (!await pathExists(filePath))
2543
+ return 0;
2544
+ const current = await readFile(filePath, "utf8");
2545
+ const lines = current.split("\n");
2546
+ let removed = 0;
2547
+ const kept = lines.filter((line) => {
2548
+ if (!line)
2549
+ return true;
2550
+ if (isLegacyCodexHistoryMention(line)) {
2551
+ removed += 1;
2552
+ return false;
2553
+ }
2554
+ return true;
2555
+ });
2556
+ if (removed > 0) {
2557
+ await writeFile(filePath, kept.join("\n"), "utf8");
2558
+ }
2559
+ return removed;
2560
+ }
2561
+ catch (error) {
2562
+ warnings.push(`Could not clean Codex legacy text state ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2563
+ return 0;
2564
+ }
2565
+ }
2566
+ function isLegacyCodexHistoryMention(line) {
2567
+ return line.includes("plugin://dbt-agent@plugins")
2568
+ || line.includes("plugin://dbt-agent@embed-labs")
2569
+ || line.includes("development-board-toolchain-dev")
2570
+ || line.includes("development-board-toolchain")
2571
+ || /plugin:\/\/Dbt Agent@/i.test(line)
2572
+ || /plugin:\/\/deve@/i.test(line)
2573
+ || /dbt-agent/i.test(line);
2574
+ }
2575
+ function removeLegacyCodexConfigTables(text) {
2576
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2577
+ const output = [];
2578
+ const removedTables = [];
2579
+ let skipping = false;
2580
+ for (const line of lines) {
2581
+ const table = parseTomlTableHeader(line);
2582
+ if (table) {
2583
+ skipping = isLegacyCodexConfigTable(table);
2584
+ if (skipping) {
2585
+ removedTables.push(table);
2586
+ continue;
2587
+ }
2588
+ }
2589
+ if (!skipping) {
2590
+ output.push(line);
2591
+ }
2592
+ }
2593
+ return { text: output.join("").replace(/\n{3,}/g, "\n\n"), removedTables };
2594
+ }
2595
+ function parseTomlTableHeader(line) {
2596
+ const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line);
2597
+ return match?.[1]?.trim();
2598
+ }
2599
+ function isLegacyCodexConfigTable(table) {
2600
+ return /^plugins\."dbt-agent@[^"]+"$/.test(table)
2601
+ || /^plugins\."Dbt Agent@[^"]+"$/i.test(table)
2602
+ || /^plugins\."deve@[^"]+"$/i.test(table)
2603
+ || isLegacyEmbedLabsCodexMarketplaceConfigTable(table)
2604
+ || table === "mcp_servers.dbt-agent"
2605
+ || table.startsWith("mcp_servers.dbt-agent.")
2606
+ || table === 'mcp_servers."dbt-agent"'
2607
+ || table.startsWith('mcp_servers."dbt-agent".')
2608
+ || table === "mcp_servers.deve"
2609
+ || table.startsWith("mcp_servers.deve.")
2610
+ || /^projects\."[^"]*\/DBT-Agent-Project(?:\/[^"]*)?"$/.test(table);
2611
+ }
2612
+ function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
2613
+ for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
2614
+ if (table === `marketplaces.${marketplaceName}`) {
2615
+ return true;
2616
+ }
2617
+ if (table === `plugins."${CODEX_PLUGIN_NAME}@${marketplaceName}"`) {
2618
+ return true;
2619
+ }
2620
+ }
2621
+ return false;
2622
+ }
2623
+ function legacyTextHasCodexPluginResidue(value) {
2624
+ return /dbt-agent|Dbt Agent|development-board-toolchain|development-board-toolchain-dev/i.test(value)
2625
+ || value === "deve"
2626
+ || value.replace(/\\/g, "/").includes("/plugins/deve")
2627
+ || value.includes("plugin://deve@");
2628
+ }
2629
+ function legacyCodexCleanupWarning(cleanup) {
2630
+ const parts = [];
2631
+ if (cleanup.legacy_removed_paths.length > 0) {
2632
+ parts.push(`removed ${cleanup.legacy_removed_paths.length} stale/legacy Codex plugin path(s)`);
2633
+ }
2634
+ if (cleanup.legacy_removed_config_tables?.length) {
2635
+ parts.push(`removed ${cleanup.legacy_removed_config_tables.length} legacy Codex config table(s)`);
2636
+ }
2637
+ if (cleanup.legacy_removed_history_entries) {
2638
+ parts.push(`removed ${cleanup.legacy_removed_history_entries} legacy Codex text-state mention(s)`);
2639
+ }
2640
+ if (cleanup.legacy_stopped_processes) {
2641
+ parts.push(`stopped ${cleanup.legacy_stopped_processes} legacy Codex plugin process(es)`);
2642
+ }
2643
+ if (cleanup.warnings?.length) {
2644
+ parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
2645
+ }
2646
+ return parts.length > 0 ? `Codex plugin cleanup: ${parts.join(", ")}.` : undefined;
2647
+ }
2648
+ async function cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall) {
2649
+ const removedPaths = [];
2650
+ const warnings = [];
2651
+ const legacyPaths = [
2652
+ join(targetRoot, "plugins", "development-board-toolchain.js"),
2653
+ join(targetRoot, "plugins", "development-board-toolchain-dev.js"),
2654
+ join(targetRoot, "plugins", "dbt-agent.js"),
2655
+ join(targetRoot, "plugins", "Dbt Agent.js"),
2656
+ join(targetRoot, "plugins", "deve.js"),
2657
+ join(targetRoot, "plugins", "deve"),
2658
+ join(targetRoot, "node_modules", "development-board-toolchain"),
2659
+ join(targetRoot, "node_modules", "development-board-toolchain-dev"),
2660
+ join(targetRoot, "node_modules", "dbt-agent")
2661
+ ];
2662
+ if (globalInstall) {
2663
+ legacyPaths.push(join(targetRoot, "plugins", "embed-labs.js"));
2664
+ legacyPaths.push(...await discoverLegacyOpenCodeBackupPaths(targetRoot, warnings));
2665
+ legacyPaths.push(...await discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings));
2666
+ }
2667
+ for (const candidate of legacyPaths) {
2668
+ try {
2669
+ if (await pathExists(candidate)) {
2670
+ await rm(candidate, { recursive: true, force: true });
2671
+ removedPaths.push(candidate);
2672
+ }
2673
+ }
2674
+ catch (error) {
2675
+ warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2676
+ }
2677
+ }
2678
+ return {
2679
+ legacy_removed_paths: removedPaths,
2680
+ legacy_removed_config_entries: [],
2681
+ ...(warnings.length > 0 ? { warnings } : {})
2682
+ };
2683
+ }
2684
+ async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
2685
+ const paths = [];
2686
+ try {
2687
+ const entries = await readdir(targetRoot, { withFileTypes: true });
2688
+ for (const entry of entries) {
2689
+ if (!entry.isFile() || !entry.name.includes(".bak"))
2690
+ continue;
2691
+ const filePath = join(targetRoot, entry.name);
2692
+ try {
2693
+ const current = await readFile(filePath, "utf8");
2694
+ if (legacyTextHasCodexPluginResidue(current)) {
2695
+ paths.push(filePath);
2696
+ }
2697
+ }
2698
+ catch (error) {
2699
+ warnings.push(`Could not inspect legacy OpenCode backup ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2700
+ }
2701
+ }
2702
+ }
2703
+ catch {
2704
+ return paths;
2705
+ }
2706
+ return paths;
2707
+ }
2708
+ async function discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings) {
2709
+ const paths = [];
2710
+ const cacheRoot = join(targetRoot, ".embed-labs", "plugin-cache");
2711
+ try {
2712
+ const entries = await readdir(cacheRoot, { withFileTypes: true });
2713
+ for (const entry of entries) {
2714
+ if (!entry.isFile())
2715
+ continue;
2716
+ if (/^(embed-labs|embed-labs-opencode-plugin)-\d+\.\d+\.\d+\.tgz$/.test(entry.name)) {
2717
+ paths.push(join(cacheRoot, entry.name));
2718
+ }
2719
+ }
2720
+ }
2721
+ catch (error) {
2722
+ if (error.code !== "ENOENT") {
2723
+ warnings.push(`Could not inspect OpenCode plugin cache ${cacheRoot}: ${error instanceof Error ? error.message : String(error)}`);
2724
+ }
2725
+ }
2726
+ return paths;
2727
+ }
2728
+ function legacyOpenCodeCleanupWarning(cleanup) {
2729
+ const parts = [];
2730
+ if (cleanup.legacy_removed_paths.length > 0) {
2731
+ parts.push(`removed ${cleanup.legacy_removed_paths.length} legacy OpenCode plugin path(s)`);
2732
+ }
2733
+ if (cleanup.legacy_removed_config_entries?.length) {
2734
+ parts.push(`removed ${cleanup.legacy_removed_config_entries.length} legacy OpenCode config entry(s)`);
2735
+ }
2736
+ if (cleanup.warnings?.length) {
2737
+ parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
2738
+ }
2739
+ return parts.length > 0 ? `Legacy OpenCode cleanup: ${parts.join(", ")}.` : undefined;
2740
+ }
2741
+ function combineWarnings(...warnings) {
2742
+ const actual = warnings.filter((warning) => Boolean(warning));
2743
+ return actual.length > 0 ? actual.join(" ") : undefined;
1804
2744
  }
1805
2745
  async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
1806
2746
  const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
@@ -1861,6 +2801,115 @@ async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
1861
2801
  const warning = await upsertCodexMcpRuntimeConfig(bridgePath);
1862
2802
  return warning ? { registered: true, warning } : { registered: true };
1863
2803
  }
2804
+ async function maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath) {
2805
+ const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
2806
+ if (explicitTarget && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
2807
+ return {
2808
+ registered: false,
2809
+ warning: "Codex plugin marketplace entry was not registered because a custom target was used. Set EMBED_CODEX_MARKETPLACE_REGISTER=1 to register it anyway."
2810
+ };
2811
+ }
2812
+ if (resolve(targetRoot) !== resolve(defaultCodexPluginRoot()) && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
2813
+ return {
2814
+ registered: false,
2815
+ warning: "Codex plugin marketplace entry was not registered because the install target is not the default Codex plugin root."
2816
+ };
2817
+ }
2818
+ const marketplacePath = defaultCodexLocalMarketplaceRoot();
2819
+ const marketplacePluginPath = join(marketplacePath, "plugins", CODEX_PLUGIN_NAME);
2820
+ try {
2821
+ if (!await pathExists(join(targetPath, ".codex-plugin", "plugin.json"))) {
2822
+ return {
2823
+ registered: false,
2824
+ warning: `Codex plugin manifest was not found at ${join(targetPath, ".codex-plugin", "plugin.json")}.`
2825
+ };
2826
+ }
2827
+ await rm(marketplacePluginPath, { recursive: true, force: true });
2828
+ await mkdir(dirname(marketplacePluginPath), { recursive: true });
2829
+ await cp(targetPath, marketplacePluginPath, { recursive: true });
2830
+ await refreshCodexPluginCache(targetPath);
2831
+ await writeCodexLocalMarketplaceManifest(marketplacePath);
2832
+ const warning = await upsertCodexPluginMarketplaceConfig(marketplacePath);
2833
+ return warning ? { registered: true, marketplacePath, warning } : { registered: true, marketplacePath };
2834
+ }
2835
+ catch (error) {
2836
+ return {
2837
+ registered: false,
2838
+ marketplacePath,
2839
+ warning: `Could not register Codex plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`
2840
+ };
2841
+ }
2842
+ }
2843
+ function defaultCodexLocalMarketplaceRoot() {
2844
+ return join(defaultCodexHome(), "local-marketplaces", CODEX_PLUGIN_NAME);
2845
+ }
2846
+ async function refreshCodexPluginCache(targetPath) {
2847
+ const manifestPath = join(targetPath, ".codex-plugin", "plugin.json");
2848
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
2849
+ const version = typeof manifest.version === "string" && manifest.version.trim()
2850
+ ? manifest.version.trim()
2851
+ : "local";
2852
+ const cachePluginRoot = join(defaultCodexPluginRoot(), "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME);
2853
+ const cacheVersionPath = join(cachePluginRoot, version);
2854
+ await rm(cachePluginRoot, { recursive: true, force: true });
2855
+ await mkdir(dirname(cacheVersionPath), { recursive: true });
2856
+ await cp(targetPath, cacheVersionPath, { recursive: true });
2857
+ }
2858
+ async function writeCodexLocalMarketplaceManifest(marketplacePath) {
2859
+ const manifestPath = join(marketplacePath, ".agents", "plugins", "marketplace.json");
2860
+ const manifest = {
2861
+ name: CODEX_MARKETPLACE_NAME,
2862
+ interface: {
2863
+ displayName: "Embed Labs"
2864
+ },
2865
+ plugins: [
2866
+ {
2867
+ name: CODEX_PLUGIN_NAME,
2868
+ source: {
2869
+ source: "local",
2870
+ path: `./plugins/${CODEX_PLUGIN_NAME}`
2871
+ },
2872
+ policy: {
2873
+ installation: "AVAILABLE",
2874
+ authentication: "ON_USE"
2875
+ },
2876
+ category: "Developer Tools"
2877
+ }
2878
+ ]
2879
+ };
2880
+ await mkdir(dirname(manifestPath), { recursive: true });
2881
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
2882
+ }
2883
+ async function upsertCodexPluginMarketplaceConfig(marketplacePath) {
2884
+ const configPath = codexConfigPath();
2885
+ try {
2886
+ await mkdir(dirname(configPath), { recursive: true });
2887
+ let text = "";
2888
+ try {
2889
+ text = await readFile(configPath, "utf8");
2890
+ }
2891
+ catch {
2892
+ text = "";
2893
+ }
2894
+ const original = text;
2895
+ const cleaned = removeLegacyCodexConfigTables(text).text;
2896
+ let updated = upsertTomlTableKeys(cleaned, `marketplaces.${CODEX_MARKETPLACE_NAME}`, {
2897
+ source_type: tomlString("local"),
2898
+ source: tomlString(marketplacePath),
2899
+ last_updated: tomlString(new Date().toISOString().replace(/\.\d{3}Z$/, "Z"))
2900
+ });
2901
+ updated = upsertTomlTableKeys(updated, `plugins."${CODEX_PLUGIN_NAME}@${CODEX_MARKETPLACE_NAME}"`, {
2902
+ enabled: "true"
2903
+ });
2904
+ if (updated !== original) {
2905
+ await writeFile(configPath, updated, "utf8");
2906
+ }
2907
+ return undefined;
2908
+ }
2909
+ catch (error) {
2910
+ return `${configPath} could not be updated with the Embed Labs plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`;
2911
+ }
2912
+ }
1864
2913
  function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embedCliBin) {
1865
2914
  try {
1866
2915
  const parsed = JSON.parse(stdout);
@@ -1972,11 +3021,160 @@ async function resolveExecutableOnPath(name) {
1972
3021
  }
1973
3022
  return undefined;
1974
3023
  }
3024
+ async function runBridgeStart(parsed) {
3025
+ const host = stringFlag(parsed, "host");
3026
+ const port = numberFlag(parsed, "port");
3027
+ const bridge = await resolveBridgeLauncher();
3028
+ const args = [...bridge.args];
3029
+ if (host) {
3030
+ args.push("--host", host);
3031
+ }
3032
+ if (port !== undefined) {
3033
+ args.push("--port", String(port));
3034
+ }
3035
+ const env = {
3036
+ ...process.env,
3037
+ ...(host ? { EMBED_BRIDGE_HOST: host } : {}),
3038
+ ...(port !== undefined ? { EMBED_BRIDGE_PORT: String(port) } : {})
3039
+ };
3040
+ const child = spawn(bridge.command, args, {
3041
+ stdio: "inherit",
3042
+ env
3043
+ });
3044
+ const forwardSignal = (signal) => {
3045
+ if (!child.killed) {
3046
+ child.kill(signal);
3047
+ }
3048
+ };
3049
+ process.once("SIGINT", forwardSignal);
3050
+ process.once("SIGTERM", forwardSignal);
3051
+ return await new Promise((resolveCode) => {
3052
+ child.on("error", (error) => {
3053
+ process.off("SIGINT", forwardSignal);
3054
+ process.off("SIGTERM", forwardSignal);
3055
+ console.error(error instanceof Error ? error.message : String(error));
3056
+ resolveCode(1);
3057
+ });
3058
+ child.on("close", (code, signal) => {
3059
+ process.off("SIGINT", forwardSignal);
3060
+ process.off("SIGTERM", forwardSignal);
3061
+ if (signal === "SIGINT" || signal === "SIGTERM") {
3062
+ resolveCode(0);
3063
+ }
3064
+ else {
3065
+ resolveCode(code ?? 0);
3066
+ }
3067
+ });
3068
+ });
3069
+ }
3070
+ async function resolveBridgeLauncher() {
3071
+ const explicitBinary = process.env.EMBED_LOCAL_BRIDGE_BINARY?.trim();
3072
+ if (explicitBinary) {
3073
+ try {
3074
+ await access(explicitBinary, constants.X_OK);
3075
+ return { command: explicitBinary, args: [] };
3076
+ }
3077
+ catch {
3078
+ // Fall through so the package launcher can print its clearer repair message.
3079
+ }
3080
+ }
3081
+ const pathBinary = await resolveExecutableOnPath(process.platform === "win32" ? "embed-local-bridge.cmd" : "embed-local-bridge");
3082
+ if (pathBinary) {
3083
+ return { command: pathBinary, args: [] };
3084
+ }
3085
+ const packageLauncher = await resolveBridgePackageLauncher();
3086
+ if (packageLauncher) {
3087
+ return { command: process.execPath, args: [packageLauncher] };
3088
+ }
3089
+ return {
3090
+ command: process.execPath,
3091
+ args: [resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js")]
3092
+ };
3093
+ }
3094
+ async function resolveBridgePackageLauncher() {
3095
+ const candidates = [];
3096
+ try {
3097
+ const packageJson = require.resolve("@embed-labs/local-bridge/package.json");
3098
+ candidates.push(join(dirname(packageJson), "dist", "index.js"));
3099
+ }
3100
+ catch {
3101
+ // Source checkout fallback below.
3102
+ }
3103
+ candidates.push(resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js"));
3104
+ for (const candidate of candidates) {
3105
+ try {
3106
+ await access(candidate, constants.R_OK);
3107
+ return candidate;
3108
+ }
3109
+ catch {
3110
+ // Keep looking.
3111
+ }
3112
+ }
3113
+ return undefined;
3114
+ }
1975
3115
  function defaultOpenCodeRoot() {
1976
- return join(process.cwd(), ".opencode");
3116
+ return globalOpenCodeRoot();
3117
+ }
3118
+ function globalOpenCodeRoot() {
3119
+ return join(homedir(), ".config", "opencode");
3120
+ }
3121
+ function isGlobalOpenCodeRoot(targetRoot) {
3122
+ return resolve(targetRoot) === resolve(globalOpenCodeRoot());
3123
+ }
3124
+ async function ensureOpenCodeGlobalPluginConfig() {
3125
+ const configPath = join(globalOpenCodeRoot(), "opencode.json");
3126
+ let existing = {};
3127
+ try {
3128
+ const parsed = JSON.parse(await readFile(configPath, "utf8"));
3129
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3130
+ existing = parsed;
3131
+ }
3132
+ }
3133
+ catch {
3134
+ existing = {};
3135
+ }
3136
+ const configured = Array.isArray(existing.plugin)
3137
+ ? existing.plugin.filter((item) => typeof item === "string")
3138
+ : Array.isArray(existing.plugins)
3139
+ ? existing.plugins.filter((item) => typeof item === "string")
3140
+ : [];
3141
+ const removed = configured.filter(isLegacyOpenCodePluginConfigEntry);
3142
+ const cleaned = configured.filter((item) => !isLegacyOpenCodePluginConfigEntry(item));
3143
+ if (!cleaned.includes("embed-labs")) {
3144
+ cleaned.push("embed-labs");
3145
+ }
3146
+ await writeFile(configPath, `${JSON.stringify({
3147
+ ...existing,
3148
+ plugin: cleaned,
3149
+ plugins: undefined
3150
+ }, null, 2)}\n`, "utf8");
3151
+ return removed;
3152
+ }
3153
+ function isLegacyOpenCodePluginConfigEntry(item) {
3154
+ const normalized = item.trim().replace(/\\/g, "/");
3155
+ const pathOnly = normalized.split(/[?#]/, 1)[0] || normalized;
3156
+ return normalized === "dbt-agent"
3157
+ || normalized === "Dbt Agent"
3158
+ || normalized === "deve"
3159
+ || normalized === "development-board-toolchain"
3160
+ || normalized === "development-board-toolchain-dev"
3161
+ || normalized === "./plugins/deve"
3162
+ || normalized === "./plugins/deve.js"
3163
+ || /(?:^|\/)plugins\/deve(?:\.js)?$/.test(pathOnly)
3164
+ || /(?:^|\/)plugins\/dbt-agent(?:\.js)?$/.test(pathOnly)
3165
+ || normalized === "./plugins/development-board-toolchain"
3166
+ || normalized === "./plugins/development-board-toolchain.js"
3167
+ || normalized === "./plugins/development-board-toolchain-dev"
3168
+ || normalized === "./plugins/development-board-toolchain-dev.js"
3169
+ || pathOnly.endsWith("/plugins/development-board-toolchain")
3170
+ || pathOnly.endsWith("/plugins/development-board-toolchain.js")
3171
+ || pathOnly.endsWith("/plugins/development-board-toolchain-dev")
3172
+ || pathOnly.endsWith("/plugins/development-board-toolchain-dev.js")
3173
+ || normalized.includes("dbt-agent")
3174
+ || normalized.includes("development-board-toolchain");
1977
3175
  }
1978
3176
  async function openCodeDuplicatePluginWarning(targetRoot) {
1979
- const globalRoot = join(homedir(), ".config", "opencode");
3177
+ const globalRoot = globalOpenCodeRoot();
1980
3178
  if (resolve(targetRoot) === resolve(globalRoot))
1981
3179
  return undefined;
1982
3180
  const configPath = join(globalRoot, "opencode.json");
@@ -2008,6 +3206,21 @@ async function localPluginVersion(kind) {
2008
3206
  return undefined;
2009
3207
  }
2010
3208
  }
3209
+ async function installedCodexPluginVersion(pluginPath) {
3210
+ return await readPackageVersion(join(pluginPath, ".codex-plugin", "plugin.json"));
3211
+ }
3212
+ async function installedOpenCodePluginVersion(targetRoot) {
3213
+ return await readPackageVersion(join(targetRoot, "node_modules", "embed-labs", "package.json"));
3214
+ }
3215
+ async function readPackageVersion(filePath) {
3216
+ try {
3217
+ const parsed = JSON.parse(await readFile(filePath, "utf8"));
3218
+ return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : undefined;
3219
+ }
3220
+ catch {
3221
+ return undefined;
3222
+ }
3223
+ }
2011
3224
  async function localPluginSourcesAvailable() {
2012
3225
  return await pathExists(sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", ".codex-plugin", "plugin.json"))
2013
3226
  && await pathExists(sourceCheckoutPath("platform_plugins", "opencode_plugin", "package.json"));
@@ -2066,19 +3279,65 @@ async function parseErrorResponse(response) {
2066
3279
  return undefined;
2067
3280
  }
2068
3281
  async function cloudAuthToken() {
3282
+ return (await cloudAuthConfig()).token;
3283
+ }
3284
+ async function cloudAuthConfig() {
2069
3285
  const envToken = process.env.EMBED_API_TOKEN?.trim();
2070
3286
  if (envToken) {
2071
- return envToken;
3287
+ const fileConfig = await readLocalAuthFile();
3288
+ return {
3289
+ ...fileConfig,
3290
+ token: envToken,
3291
+ profile: process.env.EMBED_AUTH_PROFILE ?? fileConfig.profile ?? "default",
3292
+ source: "env"
3293
+ };
2072
3294
  }
3295
+ const fileConfig = await readLocalAuthFile();
3296
+ return {
3297
+ ...fileConfig,
3298
+ token: fileConfig.token?.trim() || undefined,
3299
+ profile: fileConfig.profile ?? "default",
3300
+ source: fileConfig.token ? "file" : undefined
3301
+ };
3302
+ }
3303
+ async function readLocalAuthFile() {
2073
3304
  try {
2074
3305
  const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
2075
- const fileToken = typeof parsed.token === "string" ? parsed.token.trim() : "";
2076
- return fileToken || undefined;
3306
+ return normalizeLocalAuthFile(parsed);
2077
3307
  }
2078
3308
  catch {
2079
- return undefined;
3309
+ return {};
2080
3310
  }
2081
3311
  }
3312
+ function normalizeLocalAuthFile(parsed) {
3313
+ const device = isJsonObject(parsed.device) ? parsed.device : undefined;
3314
+ const normalizedDevice = device && typeof device.device_id === "string" && typeof device.fingerprint_hash === "string" && typeof device.private_key_pem === "string"
3315
+ ? {
3316
+ device_id: device.device_id,
3317
+ fingerprint_hash: device.fingerprint_hash,
3318
+ private_key_pem: device.private_key_pem,
3319
+ public_key_pem: typeof device.public_key_pem === "string" ? device.public_key_pem : undefined,
3320
+ label: typeof device.label === "string" ? device.label : undefined,
3321
+ platform: typeof device.platform === "string" ? device.platform : undefined,
3322
+ arch: typeof device.arch === "string" ? device.arch : undefined,
3323
+ hostname_hash: typeof device.hostname_hash === "string" ? device.hostname_hash : undefined,
3324
+ registered_at: typeof device.registered_at === "string" ? device.registered_at : undefined
3325
+ }
3326
+ : undefined;
3327
+ return {
3328
+ profile: typeof parsed.profile === "string" ? parsed.profile : undefined,
3329
+ token: typeof parsed.token === "string" ? parsed.token.trim() : undefined,
3330
+ updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined,
3331
+ account_id: typeof parsed.account_id === "string" ? parsed.account_id : undefined,
3332
+ api_key_id: typeof parsed.api_key_id === "string" ? parsed.api_key_id : undefined,
3333
+ device: normalizedDevice
3334
+ };
3335
+ }
3336
+ async function writeLocalAuthFile(config) {
3337
+ await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
3338
+ await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify(config, null, 2)}\n`, "utf8");
3339
+ await chmod(DEFAULT_AUTH_FILE, 0o600).catch(() => undefined);
3340
+ }
2082
3341
  function serviceBaseUrl(url) {
2083
3342
  return url.replace(/\/+$/, "");
2084
3343
  }
@@ -3422,6 +4681,47 @@ function boardKnowledgeFileRequest(parsed) {
3422
4681
  outputPath: outputPath.value
3423
4682
  };
3424
4683
  }
4684
+ function boardKnowledgeSearchRequest(parsed) {
4685
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "query", "source", "limit"]);
4686
+ if (unknownFlag) {
4687
+ return `Unknown flag --${unknownFlag}. ${BOARD_KNOWLEDGE_SEARCH_USAGE}`;
4688
+ }
4689
+ const templateId = parsed.command[3]?.trim();
4690
+ if (!templateId) {
4691
+ return BOARD_KNOWLEDGE_SEARCH_USAGE;
4692
+ }
4693
+ const extra = parsed.command.slice(4);
4694
+ if (extra.length > 0) {
4695
+ return `Unexpected argument: ${extra[0]}. ${BOARD_KNOWLEDGE_SEARCH_USAGE}`;
4696
+ }
4697
+ const query = optionalTrimmedStringFlag(parsed, "query");
4698
+ if (query.error) {
4699
+ return query.error;
4700
+ }
4701
+ if (!query.value) {
4702
+ return BOARD_KNOWLEDGE_SEARCH_USAGE;
4703
+ }
4704
+ const source = optionalTrimmedStringFlag(parsed, "source");
4705
+ if (source.error) {
4706
+ return source.error;
4707
+ }
4708
+ if (source.value && !["board_pack", "build_template", "registry"].includes(source.value)) {
4709
+ return BOARD_KNOWLEDGE_SEARCH_USAGE;
4710
+ }
4711
+ const limit = optionalPositiveIntegerFlag(parsed, "limit");
4712
+ if (limit.error) {
4713
+ return limit.error;
4714
+ }
4715
+ if (limit.value !== undefined && limit.value > 10) {
4716
+ return "--limit must be between 1 and 10.";
4717
+ }
4718
+ return {
4719
+ templateId,
4720
+ query: query.value,
4721
+ source: source.value,
4722
+ limit: limit.value ?? 5
4723
+ };
4724
+ }
3425
4725
  function toolCallRequest(parsed) {
3426
4726
  const unknownFlag = firstUnknownFlag(parsed, ["json", "input-json", "approve"]);
3427
4727
  if (unknownFlag) {
@@ -3537,30 +4837,272 @@ async function authLogin(parsed) {
3537
4837
  return fail("invalid_args", "Usage: embed auth login --token <token> [--profile default] [--json]");
3538
4838
  }
3539
4839
  const updatedAt = new Date().toISOString();
3540
- await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
3541
- await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify({ profile, token, updated_at: updatedAt }, null, 2)}\n`, "utf8");
3542
- return ok({ authenticated: true, profile, source: "file", updated_at: updatedAt });
4840
+ const current = await readLocalAuthFile();
4841
+ const localDevice = await buildLocalDeviceAuth(parsed, current.device);
4842
+ const registration = await registerLocalDevice(token.trim(), localDevice.registration);
4843
+ if (!registration.ok) {
4844
+ return fail(registration.error.code, registration.error.message, {
4845
+ remediation: registration.error.remediation,
4846
+ details: registration.error.details
4847
+ });
4848
+ }
4849
+ const device = {
4850
+ device_id: registration.data.device.device_id,
4851
+ fingerprint_hash: localDevice.device.fingerprint_hash,
4852
+ private_key_pem: localDevice.device.private_key_pem,
4853
+ public_key_pem: localDevice.device.public_key_pem,
4854
+ label: registration.data.device.label ?? localDevice.device.label,
4855
+ platform: registration.data.device.platform ?? localDevice.device.platform,
4856
+ arch: registration.data.device.arch ?? localDevice.device.arch,
4857
+ hostname_hash: registration.data.device.hostname_hash ?? localDevice.device.hostname_hash,
4858
+ registered_at: registration.data.device.first_seen_at
4859
+ };
4860
+ await writeLocalAuthFile({
4861
+ profile,
4862
+ token: token.trim(),
4863
+ updated_at: updatedAt,
4864
+ account_id: registration.data.device.account_id,
4865
+ api_key_id: registration.data.device.api_key_id,
4866
+ device
4867
+ });
4868
+ return ok({
4869
+ authenticated: true,
4870
+ profile,
4871
+ source: "file",
4872
+ updated_at: updatedAt,
4873
+ account_id: registration.data.device.account_id,
4874
+ api_key_id: registration.data.device.api_key_id,
4875
+ device_id: device.device_id,
4876
+ device_fingerprint_hash: device.fingerprint_hash,
4877
+ device_label: device.label,
4878
+ device_registered_at: device.registered_at,
4879
+ device_private_key_configured: true
4880
+ });
3543
4881
  }
3544
4882
  async function authStatus() {
3545
- if (process.env.EMBED_API_TOKEN?.trim()) {
4883
+ const envToken = process.env.EMBED_API_TOKEN?.trim();
4884
+ const file = await readLocalAuthFile();
4885
+ const deviceIntegrity = file.device
4886
+ ? (await validateLocalDeviceIntegrity(file.device)).ok ? "ok" : "failed"
4887
+ : "unbound";
4888
+ if (envToken) {
3546
4889
  return {
3547
4890
  authenticated: true,
3548
4891
  profile: process.env.EMBED_AUTH_PROFILE ?? "default",
3549
- source: "env"
4892
+ source: "env",
4893
+ account_id: file.account_id,
4894
+ api_key_id: file.api_key_id,
4895
+ device_id: file.device?.device_id,
4896
+ device_fingerprint_hash: file.device?.fingerprint_hash,
4897
+ device_label: file.device?.label,
4898
+ device_registered_at: file.device?.registered_at,
4899
+ device_private_key_configured: Boolean(file.device?.private_key_pem),
4900
+ device_integrity: deviceIntegrity
3550
4901
  };
3551
4902
  }
4903
+ return {
4904
+ authenticated: Boolean(file.token?.trim()),
4905
+ profile: file.profile ?? "default",
4906
+ source: file.token ? "file" : undefined,
4907
+ updated_at: file.updated_at,
4908
+ account_id: file.account_id,
4909
+ api_key_id: file.api_key_id,
4910
+ device_id: file.device?.device_id,
4911
+ device_fingerprint_hash: file.device?.fingerprint_hash,
4912
+ device_label: file.device?.label,
4913
+ device_registered_at: file.device?.registered_at,
4914
+ device_private_key_configured: Boolean(file.device?.private_key_pem),
4915
+ device_integrity: deviceIntegrity
4916
+ };
4917
+ }
4918
+ async function authDeviceStatus(parsed) {
4919
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
4920
+ const unexpected = parsed.command.slice(3);
4921
+ if (unknownFlag || unexpected.length > 0) {
4922
+ return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_STATUS_USAGE}` : AUTH_DEVICE_STATUS_USAGE);
4923
+ }
4924
+ const local = await authStatus();
4925
+ const remote = local.authenticated ? await cloudGet("/v1/me/devices") : undefined;
4926
+ if (remote && !remote.ok) {
4927
+ return ok({ local });
4928
+ }
4929
+ return ok({ local, remote: remote?.data });
4930
+ }
4931
+ async function authDeviceList(parsed) {
4932
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
4933
+ const unexpected = parsed.command.slice(3);
4934
+ if (unknownFlag || unexpected.length > 0) {
4935
+ return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_LIST_USAGE}` : AUTH_DEVICE_LIST_USAGE);
4936
+ }
4937
+ return await cloudGet("/v1/me/devices");
4938
+ }
4939
+ async function authDeviceRevoke(parsed) {
4940
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
4941
+ if (unknownFlag) {
4942
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_REVOKE_USAGE}`);
4943
+ }
4944
+ const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_REVOKE_USAGE);
4945
+ if (!id.ok) {
4946
+ return fail("invalid_args", id.error);
4947
+ }
4948
+ return await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}/revoke`, {});
4949
+ }
4950
+ async function authDeviceRename(parsed) {
4951
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "label"]);
4952
+ if (unknownFlag) {
4953
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_RENAME_USAGE}`);
4954
+ }
4955
+ const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_RENAME_USAGE);
4956
+ if (!id.ok) {
4957
+ return fail("invalid_args", id.error);
4958
+ }
4959
+ const label = stringFlag(parsed, "label");
4960
+ if (!label?.trim()) {
4961
+ return fail("invalid_args", AUTH_DEVICE_RENAME_USAGE);
4962
+ }
4963
+ const updated = await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}`, { label: label.trim() });
4964
+ if (updated.ok) {
4965
+ const auth = await readLocalAuthFile();
4966
+ if (auth.device?.device_id === updated.data.device_id) {
4967
+ await writeLocalAuthFile({ ...auth, device: { ...auth.device, label: updated.data.label } });
4968
+ }
4969
+ }
4970
+ return updated;
4971
+ }
4972
+ async function buildLocalDeviceAuth(parsed, existing) {
4973
+ const fingerprint = await localHardwareFingerprint();
4974
+ const keyPair = existing?.fingerprint_hash === fingerprint.fingerprint_hash && existing.private_key_pem && existing.public_key_pem
4975
+ ? { privateKeyPem: existing.private_key_pem, publicKeyPem: existing.public_key_pem }
4976
+ : generateLocalDeviceKeyPair();
4977
+ const label = stringFlag(parsed, "label")?.trim()
4978
+ || existing?.label
4979
+ || `${fingerprint.platform} ${fingerprint.arch}`;
4980
+ const device = {
4981
+ device_id: existing?.fingerprint_hash === fingerprint.fingerprint_hash ? existing.device_id : "",
4982
+ fingerprint_hash: fingerprint.fingerprint_hash,
4983
+ private_key_pem: keyPair.privateKeyPem,
4984
+ public_key_pem: keyPair.publicKeyPem,
4985
+ label,
4986
+ platform: fingerprint.platform,
4987
+ arch: fingerprint.arch,
4988
+ hostname_hash: fingerprint.hostname_hash,
4989
+ registered_at: existing?.registered_at
4990
+ };
4991
+ return {
4992
+ device,
4993
+ registration: {
4994
+ fingerprint_hash: fingerprint.fingerprint_hash,
4995
+ public_key: keyPair.publicKeyPem,
4996
+ label,
4997
+ platform: fingerprint.platform,
4998
+ arch: fingerprint.arch,
4999
+ hostname_hash: fingerprint.hostname_hash,
5000
+ client_name: EMBED_CLIENT_NAME,
5001
+ client_version: EMBED_CLIENT_VERSION,
5002
+ metadata: {
5003
+ fingerprint_version: "v1",
5004
+ fingerprint_source: fingerprint.source
5005
+ }
5006
+ }
5007
+ };
5008
+ }
5009
+ function generateLocalDeviceKeyPair() {
5010
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
5011
+ return {
5012
+ privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
5013
+ publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString()
5014
+ };
5015
+ }
5016
+ async function registerLocalDevice(token, body) {
3552
5017
  try {
3553
- const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
3554
- return {
3555
- authenticated: typeof parsed.token === "string" && parsed.token.trim().length > 0,
3556
- profile: typeof parsed.profile === "string" ? parsed.profile : "default",
3557
- source: "file",
3558
- updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined
5018
+ const bodyText = JSON.stringify(body);
5019
+ const headers = {
5020
+ "content-type": "application/json",
5021
+ authorization: `Bearer ${token}`
3559
5022
  };
5023
+ addCloudRequestSignature(headers, "POST", "/v1/me/devices/register", bodyText, token);
5024
+ const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/me/devices/register`, {
5025
+ method: "POST",
5026
+ headers,
5027
+ body: bodyText
5028
+ });
5029
+ const parsed = await response.json();
5030
+ return enrichCloudAuthFailure(parsed, true);
5031
+ }
5032
+ catch (error) {
5033
+ return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
5034
+ remediation: `Check that embed cloud-api is running at ${DEFAULT_CLOUD_API_URL}. Start it with: npm run cloud-api`
5035
+ });
5036
+ }
5037
+ }
5038
+ async function localHardwareFingerprint() {
5039
+ cachedLocalHardwareFingerprint ??= localHardwareFingerprintUncached();
5040
+ return await cachedLocalHardwareFingerprint;
5041
+ }
5042
+ async function localHardwareFingerprintUncached() {
5043
+ const platformName = platform();
5044
+ const archName = arch();
5045
+ const raw = await localHardwareId(platformName);
5046
+ const fingerprintHash = createHash("sha256")
5047
+ .update(`embed-labs:device:v1:${platformName}:${raw.value}`)
5048
+ .digest("hex");
5049
+ const hostnameHash = createHash("sha256")
5050
+ .update(`embed-labs:hostname:v1:${hostname()}`)
5051
+ .digest("hex");
5052
+ return {
5053
+ fingerprint_hash: fingerprintHash,
5054
+ platform: platformName,
5055
+ arch: archName,
5056
+ hostname_hash: hostnameHash,
5057
+ source: raw.source
5058
+ };
5059
+ }
5060
+ async function localHardwareId(platformName) {
5061
+ if (platformName === "darwin") {
5062
+ const result = await runLocalProcess("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
5063
+ const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(result.stdout);
5064
+ if (match?.[1]) {
5065
+ return { value: match[1], source: "macos_ioplatformuuid" };
5066
+ }
5067
+ }
5068
+ if (platformName === "win32") {
5069
+ const result = await runLocalProcess("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
5070
+ const match = /MachineGuid\s+REG_\w+\s+([^\r\n]+)/.exec(result.stdout);
5071
+ if (match?.[1]?.trim()) {
5072
+ return { value: match[1].trim(), source: "windows_machineguid" };
5073
+ }
5074
+ }
5075
+ if (platformName === "linux") {
5076
+ for (const pathValue of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
5077
+ try {
5078
+ const value = (await readFile(pathValue, "utf8")).trim();
5079
+ if (value) {
5080
+ return { value, source: `linux:${pathValue}` };
5081
+ }
5082
+ }
5083
+ catch {
5084
+ // Try the next stable machine id location.
5085
+ }
5086
+ }
5087
+ }
5088
+ const generated = await localGeneratedInstallId();
5089
+ return { value: generated, source: "generated_install_id" };
5090
+ }
5091
+ async function localGeneratedInstallId() {
5092
+ try {
5093
+ const parsed = JSON.parse(await readFile(DEFAULT_DEVICE_FILE, "utf8"));
5094
+ if (typeof parsed.generated_install_id === "string" && parsed.generated_install_id.trim()) {
5095
+ return parsed.generated_install_id.trim();
5096
+ }
3560
5097
  }
3561
5098
  catch {
3562
- return { authenticated: false, profile: "default" };
5099
+ // Fall through and create a local-only fallback id.
3563
5100
  }
5101
+ const generated = `install_${randomBytes(24).toString("base64url")}`;
5102
+ await mkdir(dirname(DEFAULT_DEVICE_FILE), { recursive: true });
5103
+ await writeFile(DEFAULT_DEVICE_FILE, `${JSON.stringify({ generated_install_id: generated, created_at: new Date().toISOString() }, null, 2)}\n`, "utf8");
5104
+ await chmod(DEFAULT_DEVICE_FILE, 0o600).catch(() => undefined);
5105
+ return generated;
3564
5106
  }
3565
5107
  function accountCreateBody(parsed) {
3566
5108
  const unknownFlag = firstUnknownFlag(parsed, ["json", "email", "display-name"]);
@@ -3716,6 +5258,80 @@ function usageRecordBody(parsed) {
3716
5258
  created_at: createdAtResult.value
3717
5259
  });
3718
5260
  }
5261
+ function mcpToolEventBody(parsed) {
5262
+ const unknownFlag = firstUnknownFlag(parsed, [
5263
+ "json",
5264
+ "account",
5265
+ "account-id",
5266
+ "tool",
5267
+ "client",
5268
+ "mode",
5269
+ "server-model-used",
5270
+ "success",
5271
+ "local-device-id",
5272
+ "local_device_id",
5273
+ "request-id",
5274
+ "duration-ms",
5275
+ "input-summary",
5276
+ "output-summary"
5277
+ ]);
5278
+ if (unknownFlag) {
5279
+ return `Unknown flag --${unknownFlag}. ${MCP_TOOL_EVENT_USAGE}`;
5280
+ }
5281
+ const extra = parsed.command.slice(2);
5282
+ if (extra.length > 0) {
5283
+ return `Unexpected argument: ${extra[0]}. ${MCP_TOOL_EVENT_USAGE}`;
5284
+ }
5285
+ const toolResult = optionalTrimmedStringFlag(parsed, "tool");
5286
+ if (toolResult.error)
5287
+ return toolResult.error;
5288
+ if (!toolResult.value)
5289
+ return MCP_TOOL_EVENT_USAGE;
5290
+ const accountResult = optionalTrimmedStringAliasFlag(parsed, ["account", "account-id"], "account or account-id");
5291
+ if (accountResult.error)
5292
+ return accountResult.error;
5293
+ const clientResult = optionalTrimmedStringFlag(parsed, "client");
5294
+ if (clientResult.error)
5295
+ return clientResult.error;
5296
+ const modeResult = optionalTrimmedStringFlag(parsed, "mode");
5297
+ if (modeResult.error)
5298
+ return modeResult.error;
5299
+ const localDeviceResult = optionalTrimmedStringAliasFlag(parsed, ["local-device-id", "local_device_id"], "local-device-id");
5300
+ if (localDeviceResult.error)
5301
+ return localDeviceResult.error;
5302
+ const requestIdResult = optionalTrimmedStringFlag(parsed, "request-id");
5303
+ if (requestIdResult.error)
5304
+ return requestIdResult.error;
5305
+ const inputSummaryResult = optionalTrimmedStringFlag(parsed, "input-summary");
5306
+ if (inputSummaryResult.error)
5307
+ return inputSummaryResult.error;
5308
+ const outputSummaryResult = optionalTrimmedStringFlag(parsed, "output-summary");
5309
+ if (outputSummaryResult.error)
5310
+ return outputSummaryResult.error;
5311
+ const durationResult = optionalNonNegativeIntegerFlag(parsed, "duration-ms");
5312
+ if (durationResult.error)
5313
+ return durationResult.error;
5314
+ const serverModelUsed = optionalBooleanFlag(parsed, "server-model-used");
5315
+ if (typeof serverModelUsed === "string")
5316
+ return serverModelUsed;
5317
+ const success = optionalBooleanFlag(parsed, "success");
5318
+ if (typeof success === "string")
5319
+ return success;
5320
+ return compactBody({
5321
+ account_id: accountResult.value,
5322
+ tool_name: toolResult.value,
5323
+ client: clientResult.value,
5324
+ mode: modeResult.value,
5325
+ local_device_id: localDeviceResult.value,
5326
+ server_model_used: serverModelUsed,
5327
+ success,
5328
+ request_id: requestIdResult.value,
5329
+ duration_ms: durationResult.value,
5330
+ input_summary: inputSummaryResult.value,
5331
+ output_summary: outputSummaryResult.value,
5332
+ metadata: localDeviceResult.value ? { local_device_id: localDeviceResult.value } : undefined
5333
+ });
5334
+ }
3719
5335
  function usageSummaryRequest(parsed) {
3720
5336
  const unknownFlag = firstUnknownFlag(parsed, ["json", "account", "account-id", "api-key-id", "from", "to"]);
3721
5337
  if (unknownFlag) {
@@ -4051,6 +5667,29 @@ function billingSnapshotListRequest(parsed) {
4051
5667
  }
4052
5668
  return { path: `/v1/accounts/${encodeURIComponent(accountResult.value)}/billing/snapshots` };
4053
5669
  }
5670
+ function localToolchainListRequest(parsed, usage = LOCAL_TOOLCHAIN_LIST_USAGE) {
5671
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "install-root"]);
5672
+ if (unknownFlag) {
5673
+ return `Unknown flag --${unknownFlag}. ${usage}`;
5674
+ }
5675
+ const extra = parsed.command.slice(3);
5676
+ if (extra.length > 0) {
5677
+ return `Unexpected argument: ${extra[0]}. ${usage}`;
5678
+ }
5679
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
5680
+ if (board.error)
5681
+ return board.error;
5682
+ const channel = optionalTrimmedStringFlag(parsed, "channel");
5683
+ if (channel.error)
5684
+ return channel.error;
5685
+ const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
5686
+ if (metadataRoot.error)
5687
+ return metadataRoot.error;
5688
+ const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
5689
+ if (installRoot.error)
5690
+ return installRoot.error;
5691
+ return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value, installRoot: installRoot.value };
5692
+ }
4054
5693
  function localToolchainLatestRequest(parsed) {
4055
5694
  const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root"]);
4056
5695
  if (unknownFlag) {
@@ -4086,7 +5725,7 @@ function localToolchainCurrentRequest(parsed) {
4086
5725
  return { installRoot: installRoot.value };
4087
5726
  }
4088
5727
  function localToolchainInstallRequest(parsed) {
4089
- const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "force"]);
5728
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "mode", "force"]);
4090
5729
  if (unknownFlag) {
4091
5730
  return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_INSTALL_USAGE}`;
4092
5731
  }
@@ -4115,6 +5754,9 @@ function localToolchainInstallRequest(parsed) {
4115
5754
  const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
4116
5755
  if (installRoot.error)
4117
5756
  return installRoot.error;
5757
+ const mode = optionalTrimmedStringFlag(parsed, "mode");
5758
+ if (mode.error)
5759
+ return mode.error;
4118
5760
  return {
4119
5761
  boardId: board.value,
4120
5762
  channel: channel.value,
@@ -4122,11 +5764,32 @@ function localToolchainInstallRequest(parsed) {
4122
5764
  sourceUrl: sourceUrl.value,
4123
5765
  sourceReleaseRoot: sourceReleaseRoot.value,
4124
5766
  installRoot: installRoot.value,
5767
+ mode: mode.value,
4125
5768
  force: booleanFlag(parsed, "force")
4126
5769
  };
4127
5770
  }
5771
+ function localToolchainUninstallRequest(parsed) {
5772
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "install-root"]);
5773
+ if (unknownFlag) {
5774
+ return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_UNINSTALL_USAGE}`;
5775
+ }
5776
+ const extra = parsed.command.slice(3);
5777
+ if (extra.length > 0) {
5778
+ return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_UNINSTALL_USAGE}`;
5779
+ }
5780
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
5781
+ if (board.error)
5782
+ return board.error;
5783
+ if (!board.value) {
5784
+ return LOCAL_TOOLCHAIN_UNINSTALL_USAGE;
5785
+ }
5786
+ const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
5787
+ if (installRoot.error)
5788
+ return installRoot.error;
5789
+ return { boardId: board.value, installRoot: installRoot.value };
5790
+ }
4128
5791
  function localToolchainValidateRequest(parsed) {
4129
- const unknownFlag = firstUnknownFlag(parsed, ["json", "release-root"]);
5792
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "release-root", "mode"]);
4130
5793
  if (unknownFlag) {
4131
5794
  return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_VALIDATE_USAGE}`;
4132
5795
  }
@@ -4138,7 +5801,15 @@ function localToolchainValidateRequest(parsed) {
4138
5801
  if (releaseRoot.error) {
4139
5802
  return releaseRoot.error;
4140
5803
  }
4141
- return { releaseRoot: releaseRoot.value };
5804
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
5805
+ if (board.error) {
5806
+ return board.error;
5807
+ }
5808
+ const mode = optionalTrimmedStringFlag(parsed, "mode");
5809
+ if (mode.error) {
5810
+ return mode.error;
5811
+ }
5812
+ return { releaseRoot: releaseRoot.value, mode: mode.value, boardId: board.value };
4142
5813
  }
4143
5814
  function localCompileTaishanPiRequest(parsed, auth) {
4144
5815
  const unknownFlag = firstUnknownFlag(parsed, ["json", "source", "output", "release-root", "account", "account-id"]);
@@ -4223,7 +5894,9 @@ function localToolchainAuthContext(auth, accountId) {
4223
5894
  authenticated: auth.authenticated,
4224
5895
  profile: auth.profile,
4225
5896
  source: auth.source,
4226
- account_id: accountId
5897
+ account_id: accountId,
5898
+ api_key_id: auth.api_key_id,
5899
+ device_id: auth.device_id
4227
5900
  };
4228
5901
  }
4229
5902
  function usageEventsRequest(parsed) {
@@ -4633,6 +6306,35 @@ function renderPluginList(result) {
4633
6306
  `install="${plugin.install_command}"`
4634
6307
  ].filter(Boolean).join(" ")).join("\n");
4635
6308
  }
6309
+ function renderPluginUpdateCheck(result) {
6310
+ const lines = [
6311
+ `release_url=${result.release_url}`,
6312
+ result.latest_version ? `latest_version=${result.latest_version}` : ""
6313
+ ].filter(Boolean);
6314
+ if (result.release_notes.length > 0) {
6315
+ lines.push("release_notes:");
6316
+ for (const note of result.release_notes) {
6317
+ lines.push(` - ${note}`);
6318
+ }
6319
+ }
6320
+ for (const plugin of result.plugins) {
6321
+ lines.push("");
6322
+ lines.push(`${plugin.display_name} (${plugin.id})`);
6323
+ lines.push(` installed=${plugin.installed}`);
6324
+ lines.push(` installed_version=${plugin.installed_version ?? "none"}`);
6325
+ lines.push(` latest_version=${plugin.latest_version ?? "unknown"}`);
6326
+ lines.push(` update_available=${plugin.update_available}`);
6327
+ lines.push(` target=${plugin.target_path}`);
6328
+ lines.push(` update_command=${plugin.update_command}`);
6329
+ if (plugin.release_file) {
6330
+ lines.push(` release_file=${plugin.release_file}`);
6331
+ }
6332
+ for (const note of plugin.notes) {
6333
+ lines.push(` note=${note}`);
6334
+ }
6335
+ }
6336
+ return lines.join("\n");
6337
+ }
4636
6338
  function renderPluginInstall(result) {
4637
6339
  const lines = ["Installed plugins:"];
4638
6340
  for (const item of result.installed) {
@@ -4654,6 +6356,15 @@ function renderPluginInstall(result) {
4654
6356
  if (item.mcp_warning) {
4655
6357
  lines.push(` warning=${item.mcp_warning}`);
4656
6358
  }
6359
+ if (item.marketplace_registered !== undefined) {
6360
+ lines.push(` codex_marketplace_registered=${item.marketplace_registered}`);
6361
+ }
6362
+ if (item.marketplace_path) {
6363
+ lines.push(` codex_marketplace=${item.marketplace_path}`);
6364
+ }
6365
+ if (item.marketplace_warning) {
6366
+ lines.push(` warning=${item.marketplace_warning}`);
6367
+ }
4657
6368
  }
4658
6369
  return lines.join("\n");
4659
6370
  }
@@ -4685,9 +6396,53 @@ function renderAgentRunResult(result) {
4685
6396
  return lines.join("\n");
4686
6397
  }
4687
6398
  function renderAuthStatus(status) {
4688
- return status.authenticated
4689
- ? `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`
4690
- : `Not authenticated profile=${status.profile}`;
6399
+ if (!status.authenticated) {
6400
+ return `Not authenticated profile=${status.profile}`;
6401
+ }
6402
+ return [
6403
+ `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`,
6404
+ status.account_id ? `account=${status.account_id}` : "",
6405
+ status.api_key_id ? `api_key=${status.api_key_id}` : "",
6406
+ status.device_id ? `device=${status.device_id}` : "device=not_registered",
6407
+ status.device_label ? `device_label=${status.device_label}` : "",
6408
+ status.device_integrity ? `device_integrity=${status.device_integrity}` : "",
6409
+ status.device_private_key_configured === false ? "device_private_key=missing" : ""
6410
+ ].filter(Boolean).join("\n");
6411
+ }
6412
+ function renderAuthDeviceStatus(status) {
6413
+ const lines = [renderAuthStatus(status.local)];
6414
+ if (status.remote) {
6415
+ const activeCount = status.remote.devices.filter((device) => device.status === "active").length;
6416
+ lines.push(`remote_devices=${activeCount}/${status.remote.device_limit}`);
6417
+ const localDevice = status.local.device_id
6418
+ ? status.remote.devices.find((device) => device.device_id === status.local.device_id)
6419
+ : undefined;
6420
+ if (localDevice) {
6421
+ lines.push(`remote_current=${renderAuthDevice(localDevice)}`);
6422
+ }
6423
+ }
6424
+ return lines.join("\n");
6425
+ }
6426
+ function renderAuthDeviceList(result) {
6427
+ if (result.devices.length === 0) {
6428
+ return `No registered devices. device_limit=${result.device_limit}`;
6429
+ }
6430
+ return [
6431
+ `device_limit=${result.device_limit}`,
6432
+ ...result.devices.map(renderAuthDevice)
6433
+ ].join("\n");
6434
+ }
6435
+ function renderAuthDevice(device) {
6436
+ return [
6437
+ `${device.device_id} account=${device.account_id}`,
6438
+ device.api_key_id ? `api_key=${device.api_key_id}` : "",
6439
+ `status=${device.status}`,
6440
+ device.label ? `label=${device.label}` : "",
6441
+ device.platform ? `platform=${device.platform}` : "",
6442
+ device.arch ? `arch=${device.arch}` : "",
6443
+ `last_seen_at=${device.last_seen_at}`,
6444
+ device.revoked_at ? `revoked_at=${device.revoked_at}` : ""
6445
+ ].filter(Boolean).join(" ");
4691
6446
  }
4692
6447
  function renderAccount(account) {
4693
6448
  return [
@@ -4737,6 +6492,20 @@ function renderUsageRecord(record) {
4737
6492
  `created_at=${record.created_at}`
4738
6493
  ].filter(Boolean).join(" ");
4739
6494
  }
6495
+ function renderMcpToolEvent(event) {
6496
+ return [
6497
+ `${event.event_id} tool=${event.tool_name}`,
6498
+ event.account_id ? `account=${event.account_id}` : "",
6499
+ event.api_key_id ? `api_key=${event.api_key_id}` : "",
6500
+ `client=${event.client}`,
6501
+ `mode=${event.mode}`,
6502
+ `server_model_used=${event.server_model_used}`,
6503
+ `success=${event.success}`,
6504
+ event.request_id ? `request=${event.request_id}` : "",
6505
+ event.duration_ms !== undefined ? `duration_ms=${event.duration_ms}` : "",
6506
+ `created_at=${event.created_at}`
6507
+ ].filter(Boolean).join(" ");
6508
+ }
4740
6509
  function renderUsageSummary(summary) {
4741
6510
  const lines = [
4742
6511
  summary.account_id ? `account=${summary.account_id}` : "",
@@ -5048,6 +6817,20 @@ function renderBoardKnowledge(data) {
5048
6817
  `title=${file.title}`
5049
6818
  ].join(" ")).join("\n");
5050
6819
  }
6820
+ function renderBoardKnowledgeSearch(data) {
6821
+ const result = data;
6822
+ const matches = Array.isArray(result.matches) ? result.matches : [];
6823
+ if (matches.length === 0) {
6824
+ return "No matching board knowledge snippets.";
6825
+ }
6826
+ return matches.map((match, index) => [
6827
+ `#${index + 1}`,
6828
+ `${match.source}:${match.path}`,
6829
+ `score=${match.score}`,
6830
+ `title=${match.title}`,
6831
+ `excerpt=${match.excerpt.replace(/\s+/g, " ").trim()}`
6832
+ ].join(" ")).join("\n");
6833
+ }
5051
6834
  function renderBoardKnowledgeFile(file) {
5052
6835
  return [
5053
6836
  `template=${file.template_id}`,
@@ -5260,14 +7043,150 @@ function renderBuildWorkspaceSourcePatch(result) {
5260
7043
  }
5261
7044
  return lines.join("\n");
5262
7045
  }
7046
+ function renderLocalToolchainList(result) {
7047
+ const installedCount = result.environments.filter((environment) => !!environment.installed).length;
7048
+ const availableCount = result.environments.filter((environment) => environment.status === "available").length;
7049
+ const updateCount = result.environments.filter((environment) => environment.status === "update_available").length;
7050
+ const lines = [
7051
+ `Local development environments: ${result.environments.length}`,
7052
+ `installed=${installedCount} available=${availableCount} updates=${updateCount}`,
7053
+ `host=${result.host}`,
7054
+ `channel=${result.channel}`,
7055
+ result.metadata_source === "local_override" ? `metadata_override=${result.metadata_root}` : "metadata=production/built-in",
7056
+ `install_root=${result.install_root}`,
7057
+ `registry=${result.registry_path}`
7058
+ ];
7059
+ for (const environment of result.environments) {
7060
+ lines.push("");
7061
+ lines.push(`${environment.display_name} (${environment.board_id})`);
7062
+ lines.push(` status=${localToolchainStatusLabel(environment.status)}`);
7063
+ lines.push(` latest=${environment.latest.version}`);
7064
+ if (environment.installed) {
7065
+ lines.push(` installed=${environment.installed.version ?? "unknown"} mode=${environment.installed.mode ?? "unknown"}`);
7066
+ if (environment.installed.release_root) {
7067
+ lines.push(` release_root=${environment.installed.release_root}`);
7068
+ }
7069
+ }
7070
+ lines.push(` install_modes=${environment.install_modes.join(",")}`);
7071
+ lines.push(` install_command=${environment.install_command}`);
7072
+ if (environment.status === "update_available") {
7073
+ lines.push(` update_command=${environment.update_command}`);
7074
+ }
7075
+ if (environment.components?.length) {
7076
+ lines.push(` component_catalog=${localToolchainComponentSummary(environment.components)}`);
7077
+ if (environment.latest.default_mode) {
7078
+ lines.push(` default_mode_download=${environment.latest.default_mode}: ${localToolchainComponentSummaryForMode(environment.components, environment.latest.default_mode)}`);
7079
+ }
7080
+ if (environment.installed?.mode && environment.installed.mode !== environment.latest.default_mode) {
7081
+ lines.push(` installed_mode_components=${environment.installed.mode}: ${localToolchainComponentSummaryForMode(environment.components, environment.installed.mode)}`);
7082
+ }
7083
+ lines.push(` mode_downloads=${localToolchainModeSummaries(environment.install_modes, environment.components).join("; ")}`);
7084
+ lines.push(` package_groups=${localToolchainComponentGroups(environment.components).join(", ")}`);
7085
+ lines.push(` detail_command=embedlabs local toolchain latest --board ${environment.board_id}`);
7086
+ }
7087
+ if (environment.notes.length > 0) {
7088
+ for (const note of environment.notes) {
7089
+ lines.push(` note=${note}`);
7090
+ }
7091
+ }
7092
+ }
7093
+ return lines.join("\n");
7094
+ }
7095
+ function localToolchainStatusLabel(status) {
7096
+ if (status === "installed")
7097
+ return "installed/已安装";
7098
+ if (status === "available")
7099
+ return "available/可安装";
7100
+ if (status === "update_available")
7101
+ return "update_available/可更新";
7102
+ if (status === "unsupported_host")
7103
+ return "unsupported_host/当前系统暂不支持";
7104
+ return status;
7105
+ }
7106
+ function localToolchainComponentSummary(components) {
7107
+ const totalBytes = components.reduce((total, component) => total + component.size_bytes, 0);
7108
+ return `${components.length} components, ${formatByteSize(totalBytes)}`;
7109
+ }
7110
+ function localToolchainComponentSummaryForMode(components, mode) {
7111
+ return localToolchainComponentSummary(localToolchainComponentsForMode(components, mode));
7112
+ }
7113
+ function localToolchainModeSummaries(modes, components) {
7114
+ return modes.map((mode) => `${mode}=${localToolchainComponentSummaryForMode(components, mode)}`);
7115
+ }
7116
+ function localToolchainComponentsForMode(components, mode) {
7117
+ return components.filter((component) => {
7118
+ if (!component.install_modes?.length) {
7119
+ return true;
7120
+ }
7121
+ return component.install_modes.includes(mode);
7122
+ });
7123
+ }
7124
+ function localToolchainComponentGroups(components) {
7125
+ const groups = new Set();
7126
+ for (const component of components) {
7127
+ const text = `${component.id} ${component.role ?? ""}`.toLowerCase();
7128
+ if (text.includes("arm-none-eabi") || text.includes("bare-metal") || text.includes("compiler"))
7129
+ groups.add("compiler/ARM 裸机编译器");
7130
+ if (text.includes("pico-sdk") || text.includes("sdk-core"))
7131
+ groups.add("pico-sdk/C/C++ SDK");
7132
+ if (text.includes("sysroot") || text.includes("cross"))
7133
+ groups.add("sysroot/交叉运行库");
7134
+ if (text.includes("qt"))
7135
+ groups.add("qt/Qt 应用支持");
7136
+ if (text.includes("rockchip") || text.includes("boot") || text.includes("resource"))
7137
+ groups.add("boot-flash/启动与烧写工具");
7138
+ if (text.includes("image") || text.includes("rootfs"))
7139
+ groups.add("images/镜像资源");
7140
+ if (text.includes("initial-firmware"))
7141
+ groups.add("initial-firmware/初始化镜像");
7142
+ if (text.includes("rp2350-monitor"))
7143
+ groups.add("rp2350-monitor/可选硬件监控镜像");
7144
+ if (text.includes("meta"))
7145
+ groups.add("metadata/知识与脚本元数据");
7146
+ }
7147
+ return groups.size > 0 ? [...groups] : ["runtime/运行时工具"];
7148
+ }
7149
+ function formatByteSize(bytes) {
7150
+ if (!Number.isFinite(bytes) || bytes < 0) {
7151
+ return "unknown size";
7152
+ }
7153
+ const units = ["B", "KB", "MB", "GB", "TB"];
7154
+ let value = bytes;
7155
+ let unit = 0;
7156
+ while (value >= 1024 && unit < units.length - 1) {
7157
+ value /= 1024;
7158
+ unit += 1;
7159
+ }
7160
+ const fixed = unit === 0 || value >= 10 ? value.toFixed(0) : value.toFixed(1);
7161
+ return `${fixed} ${units[unit]}`;
7162
+ }
5263
7163
  function renderLocalToolchainLatest(result) {
5264
7164
  const lines = [
5265
7165
  `board=${result.board_id}`,
5266
7166
  `channel=${result.channel}`,
5267
7167
  `version=${result.version}`,
5268
7168
  `host=${result.host}`,
5269
- result.metadata_root ? `metadata_root=${result.metadata_root}` : "metadata=built-in"
5270
- ];
7169
+ result.metadata_root ? `metadata_root=${result.metadata_root}` : "metadata=built-in",
7170
+ result.download?.source_url ? `download=${result.download.mirror_kind}:${result.download.source_url}` : "",
7171
+ result.download?.archive ? `archive_sha256=${result.download.archive.sha256}` : "",
7172
+ result.download?.archive ? `archive_size_bytes=${result.download.archive.size_bytes}` : "",
7173
+ result.download?.components?.length ? `components=${result.download.components.length}` : "",
7174
+ result.download?.default_mode ? `default_mode=${result.download.default_mode}` : "",
7175
+ result.download_error ? `download_error=${result.download_error}` : ""
7176
+ ].filter(Boolean);
7177
+ if (result.download?.components?.length) {
7178
+ const modes = [...new Set(result.download.components.flatMap((component) => component.install_modes ?? ["all"]))].filter((mode) => mode !== "all");
7179
+ if (result.download.default_mode) {
7180
+ lines.push(`default_mode_download=${result.download.default_mode}: ${localToolchainDownloadComponentSummaryForMode(result.download.components, result.download.default_mode)}`);
7181
+ }
7182
+ if (modes.length > 0) {
7183
+ lines.push(`mode_downloads=${modes.map((mode) => `${mode}=${localToolchainDownloadComponentSummaryForMode(result.download?.components ?? [], mode)}`).join("; ")}`);
7184
+ }
7185
+ lines.push("download_components:");
7186
+ for (const component of result.download.components) {
7187
+ lines.push(` ${component.id}@${component.version} modes=${component.install_modes?.join(",") || "all"} size=${formatByteSize(component.archive.size_bytes)} file=${component.archive.file}`);
7188
+ }
7189
+ }
5271
7190
  if (result.packages.length > 0) {
5272
7191
  lines.push("packages:");
5273
7192
  for (const pkg of result.packages) {
@@ -5276,6 +7195,11 @@ function renderLocalToolchainLatest(result) {
5276
7195
  }
5277
7196
  return lines.join("\n");
5278
7197
  }
7198
+ function localToolchainDownloadComponentSummaryForMode(components, mode) {
7199
+ const selected = localToolchainComponentsForMode(components ?? [], mode);
7200
+ const totalBytes = selected.reduce((total, component) => total + component.archive.size_bytes, 0);
7201
+ return `${selected.length} components, ${formatByteSize(totalBytes)}`;
7202
+ }
5279
7203
  function renderLocalToolchainCurrent(result) {
5280
7204
  if (!result.installed) {
5281
7205
  return [
@@ -5290,6 +7214,7 @@ function renderLocalToolchainCurrent(result) {
5290
7214
  `board=${result.board_id}`,
5291
7215
  result.version ? `version=${result.version}` : "",
5292
7216
  result.channel ? `channel=${result.channel}` : "",
7217
+ result.mode ? `mode=${result.mode}` : "",
5293
7218
  result.release_root ? `release_root=${result.release_root}` : "",
5294
7219
  `install_root=${result.install_root}`,
5295
7220
  `registry=${result.registry_path}`
@@ -5302,13 +7227,21 @@ function renderLocalToolchainInstall(result) {
5302
7227
  `version=${result.version}`,
5303
7228
  `channel=${result.channel}`,
5304
7229
  `host=${result.host}`,
7230
+ `mode=${result.mode}`,
5305
7231
  `install_root=${result.install_root}`,
5306
7232
  `release_root=${result.release_root}`,
5307
7233
  `registry=${result.registry_path}`,
5308
7234
  `source=${result.source.kind}:${result.source.value}`,
5309
7235
  result.source.downloaded_path ? `downloaded=${result.source.downloaded_path}` : "",
7236
+ result.source.components?.length ? `components=${result.source.components.length}` : "",
5310
7237
  `validation=${result.validation.ok ? "ok" : "failed"}`
5311
7238
  ].filter(Boolean);
7239
+ if (result.source.components?.length) {
7240
+ lines.push("installed_components:");
7241
+ for (const component of result.source.components) {
7242
+ lines.push(` ${component.id}@${component.version} ${component.mirror_kind || ""} bytes=${component.size_bytes}`);
7243
+ }
7244
+ }
5312
7245
  if (result.installed_paths.length > 0) {
5313
7246
  lines.push("installed_paths:");
5314
7247
  for (const installedPath of result.installed_paths) {
@@ -5323,16 +7256,50 @@ function renderLocalToolchainInstall(result) {
5323
7256
  }
5324
7257
  return lines.join("\n");
5325
7258
  }
7259
+ function renderLocalToolchainUninstall(result) {
7260
+ const lines = [
7261
+ result.removed ? "Local toolchain uninstalled." : "Local toolchain was not installed.",
7262
+ `board=${result.board_id}`,
7263
+ `install_root=${result.install_root}`,
7264
+ `registry=${result.registry_path}`,
7265
+ `removed_registry_entry=${result.removed_registry_entry}`,
7266
+ `observed_at=${result.observed_at}`
7267
+ ];
7268
+ if (result.removed_paths.length > 0) {
7269
+ lines.push("removed_paths:");
7270
+ for (const removedPath of result.removed_paths) {
7271
+ lines.push(` ${removedPath}`);
7272
+ }
7273
+ }
7274
+ if (result.remaining_installed_boards.length > 0) {
7275
+ lines.push(`remaining_installed_boards=${result.remaining_installed_boards.join(",")}`);
7276
+ }
7277
+ return lines.join("\n");
7278
+ }
5326
7279
  function renderLocalToolchainValidation(result) {
5327
7280
  const lines = [
5328
7281
  result.ok ? "Local toolchain ready." : "Local toolchain not ready.",
5329
7282
  `board=${result.board_id}`,
7283
+ `mode=${result.mode}`,
5330
7284
  `host=${result.host.platform}/${result.host.arch}`,
5331
- `release_root=${result.release_root}`
7285
+ `release_root=${result.release_root}`,
7286
+ `summary=${result.summary_for_user}`
5332
7287
  ];
7288
+ if (!result.ok && result.missing_groups.length > 0) {
7289
+ lines.push(`missing_groups=${result.missing_groups.join(", ")}`);
7290
+ }
7291
+ if (result.repair_command) {
7292
+ lines.push(`repair_command=${result.repair_command}`);
7293
+ }
5333
7294
  for (const check of result.checked_paths) {
5334
7295
  lines.push(`${check.exists ? "ok" : "missing"} ${check.label}: ${check.path}`);
5335
7296
  }
7297
+ if (result.path_leaks.length > 0) {
7298
+ lines.push("path_leaks:");
7299
+ for (const leak of result.path_leaks) {
7300
+ lines.push(` ${leak.label}: ${leak.path} contains ${leak.forbidden}`);
7301
+ }
7302
+ }
5336
7303
  if (result.notes.length > 0) {
5337
7304
  lines.push("notes:");
5338
7305
  for (const note of result.notes) {
@@ -5781,6 +7748,22 @@ function stringFlag(parsed, name) {
5781
7748
  function booleanFlag(parsed, name) {
5782
7749
  return parsed.flags[name] === true;
5783
7750
  }
7751
+ function optionalBooleanFlag(parsed, name) {
7752
+ const values = flagValues(parsed, name);
7753
+ if (values.length === 0)
7754
+ return undefined;
7755
+ const value = values[values.length - 1];
7756
+ if (value === true)
7757
+ return true;
7758
+ if (typeof value !== "string")
7759
+ return `--${name} must be true or false.`;
7760
+ const normalized = value.trim().toLowerCase();
7761
+ if (["1", "true", "yes", "y", "on"].includes(normalized))
7762
+ return true;
7763
+ if (["0", "false", "no", "n", "off"].includes(normalized))
7764
+ return false;
7765
+ return `--${name} must be true or false.`;
7766
+ }
5784
7767
  function switchFlag(parsed, name) {
5785
7768
  const values = flagValues(parsed, name);
5786
7769
  for (const value of values) {
@@ -6079,41 +8062,34 @@ Main workflow:
6079
8062
  3. Inspect server model routing:
6080
8063
  embed plugin install codex
6081
8064
  embed plugin install opencode
8065
+ embed plugin update check
6082
8066
  embed service modes
6083
8067
  embed model list
6084
8068
  embed model default
6085
8069
  4. Run a natural-language local tool loop:
6086
8070
  embed agent run --prompt "验证开发板状态"
6087
8071
  5. Validate or use the local TaishanPi toolchain:
8072
+ embed local toolchain list
8073
+ embed local toolchain installed
6088
8074
  embed local toolchain latest
6089
- embed local toolchain install --source-url <toolchain.tar.gz>
8075
+ embed local toolchain install
8076
+ embed local toolchain uninstall --board pico2w-rp2350-monitor
6090
8077
  embed local toolchain validate
6091
8078
  embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
6092
8079
  embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
6093
- 6. Pick a cloud build template:
8080
+ 6. Query board knowledge and method metadata:
6094
8081
  embed board registry list
6095
8082
  embed board methods taishanpi-1m-rk3566
6096
8083
  embed board knowledge taishanpi-1m-rk3566
8084
+ embed board knowledge search taishanpi-1m-rk3566 --query "UART pinout"
6097
8085
  embed build template list
6098
8086
  embed build template show <template_id>
6099
- 7. Provision and populate a build workspace:
6100
- embed build workspace provision --account <account_id> --project <project_id> --template <template_id>
6101
- embed build resource lease create --workspace <workspace_id> --execution-mode cloud_worker
6102
- embed build workspace source put <workspace_id> --file ./main.c:src/main.c
6103
- embed build workspace source list <workspace_id>
6104
- embed build workspace source get <workspace_id> --path src/main.c --output ./main.c
6105
- embed build workspace source search <workspace_id> --query init --glob "**/*.c"
6106
- embed build workspace source patch <workspace_id> --patch ./fix.patch
6107
- embed build workspace release <workspace_id> --dry-run
6108
- 8. Generate application source on the server and follow artifacts:
6109
- embed build application generate --workspace <workspace_id> --prompt "Create a minimal Linux app" --provider bai --model gpt-5.2
6110
- embed build application compile --workspace <workspace_id> --source app/generated.c --execution-mode docker_worker
6111
- embed build image generate --workspace <workspace_id> --prompt "Generate a minimal TaishanPi image"
8087
+ 7. Generate small image packages and compose locally:
6112
8088
  embed build image boot-logo --logo ./logo.png --board taishanpi --variant 1M-RK3566 --output ./boot-logo-package.json
6113
8089
  embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
6114
8090
  embed cloud task artifacts <task_id>
6115
8091
  embed artifact download <artifact_id> --output ./artifact.bin
6116
- 9. Check credits or create a recharge QR:
8092
+ 8. Check credits or create a recharge QR:
6117
8093
  embed billing balance --account <account_id>
6118
8094
  embed billing tokens --account <account_id>
6119
8095
  embed billing ledger --account <account_id>
@@ -6127,14 +8103,18 @@ Local hardware:
6127
8103
  embed agent run --prompt "部署泰山派应用" --host 198.19.77.2 --artifact ./artifact.bin --remote-path /userdata/embed-labs/apps/app --approve --run
6128
8104
  embed agent run --prompt "部署生成的泰山派应用" --host 198.19.77.2 --artifact-task <task_id> --remote-path /userdata/embed-labs/apps/app --approve --run
6129
8105
  embed tool list
6130
- embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22,15301]}'
8106
+ embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22]}'
6131
8107
  embed tool call wifi.scan --input-json '{"host":"198.19.77.2","user":"root"}'
8108
+ embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve
6132
8109
  embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}'
6133
8110
  embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}'
6134
8111
  embed device list
8112
+ embed local toolchain list
8113
+ embed local toolchain installed
6135
8114
  embed local toolchain latest
6136
8115
  embed local toolchain current
6137
- embed local toolchain install --source-url <toolchain.tar.gz>
8116
+ embed local toolchain install
8117
+ embed local toolchain uninstall --board pico2w-rp2350-monitor
6138
8118
  embed local toolchain validate
6139
8119
  embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
6140
8120
  embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
@@ -6148,7 +8128,7 @@ Help:
6148
8128
 
6149
8129
  Environment:
6150
8130
  EMBED_BRIDGE_URL=http://127.0.0.1:18083
6151
- EMBED_CLOUD_API_URL=http://127.0.0.1:18100
8131
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
6152
8132
  EMBED_API_TOKEN=<token>
6153
8133
  CODEX_HOME=~/.codex
6154
8134
 
@@ -6203,6 +8183,8 @@ Install local AI client plugins explicitly:
6203
8183
  embed plugin list
6204
8184
  embed plugin install codex
6205
8185
  embed plugin install opencode
8186
+ embed plugin update check
8187
+ embed plugin update all
6206
8188
 
6207
8189
  Cloud build path:
6208
8190
 
@@ -6217,6 +8199,7 @@ Cloud build path:
6217
8199
  embed board registry show taishanpi-1m-rk3566
6218
8200
  embed board methods taishanpi-1m-rk3566
6219
8201
  embed board knowledge taishanpi-1m-rk3566
8202
+ embed board knowledge search taishanpi-1m-rk3566 --query "UART pinout"
6220
8203
  embed build template list
6221
8204
  embed build template show <template_id>
6222
8205
  embed build workspace provision --account <account_id> --project <project_id> --template <template_id>
@@ -6227,9 +8210,6 @@ Cloud build path:
6227
8210
  embed build workspace source search <workspace_id> --query init --glob "**/*.c"
6228
8211
  embed build workspace source patch <workspace_id> --patch ./fix.patch
6229
8212
  embed build workspace release <workspace_id> --dry-run
6230
- embed build application generate --workspace <workspace_id> --prompt "Create a minimal Linux app" --provider bai --model gpt-5.2
6231
- embed build application compile --workspace <workspace_id> --source app/generated.c --execution-mode docker_worker
6232
- embed build image generate --workspace <workspace_id> --prompt "Generate a minimal TaishanPi image"
6233
8213
  embed build image boot-logo --logo ./logo.png --board taishanpi --variant 1M-RK3566 --output ./boot-logo-package.json
6234
8214
  embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
6235
8215
  embed cloud task status <task_id>
@@ -6260,7 +8240,7 @@ Local hardware path:
6260
8240
  embed run "验证开发板状态"
6261
8241
  embed tool list
6262
8242
  embed tool call debug.tools.scan
6263
- embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22,15301]}'
8243
+ embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22]}'
6264
8244
  embed device list
6265
8245
  embed deploy taishanpi --host 198.19.77.2 --artifact ./artifact.bin --approve --run
6266
8246
  embed flash plan --board <rp2350|taishanpi> --artifact ./artifact.bin
@@ -6281,6 +8261,8 @@ Usage:
6281
8261
  embed auth logout [--json]
6282
8262
  embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]
6283
8263
  embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]
8264
+ embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
8265
+ embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
6284
8266
  embed service modes [--json]
6285
8267
  embed model list [--json]
6286
8268
  embed model default [--json]
@@ -6288,6 +8270,7 @@ Usage:
6288
8270
  embed board registry show <template_id> [--json]
6289
8271
  embed board methods <template_id> [--json]
6290
8272
  embed board knowledge <template_id> [--json]
8273
+ embed board knowledge search <template_id> --query <text> [--source board_pack|build_template|registry] [--limit 5] [--json]
6291
8274
  embed board knowledge file <template_id> --source board_pack|build_template|registry --path <relative_path> [--output <local_path>] [--json]
6292
8275
  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]
6293
8276
  embed run <natural language request> [--provider stub|openai|bai|cc|claude-code] [--approve] [--json]
@@ -6336,10 +8319,14 @@ Usage:
6336
8319
  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]
6337
8320
  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]
6338
8321
  embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]
6339
- embed local toolchain latest [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--json]
8322
+ embed local toolchain list [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
8323
+ embed local toolchain installed [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
8324
+ embed local toolchain latest [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--json]
6340
8325
  embed local toolchain current [--install-root <path>] [--json]
6341
- embed local toolchain install [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--force] [--json]
6342
- embed local toolchain validate [--release-root <path>] [--json]
8326
+ embed local toolchain install [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-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]
8327
+ Defaults to the production download channel at download.embedboard.com.
8328
+ embed local toolchain uninstall --board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor [--install-root <path>] [--json]
8329
+ embed local toolchain validate [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--release-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--json]
6343
8330
  embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]
6344
8331
  embed local build qt-smoke --build-dir <dir> [--source <qt-smoke-dir>] [--release-root <path>] [--account <account_id>] [--json]
6345
8332
  embed debug tools [--json]
@@ -6350,6 +8337,18 @@ Usage:
6350
8337
  embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}' [--json]
6351
8338
  embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
6352
8339
  embed tool call qml.runtime.start --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
8340
+ embed tool call rp2350.monitor.capabilities [--json]
8341
+ embed tool call rp2350.monitor.status [--json]
8342
+ embed tool call rp2350.monitor.gpio.read --input-json '{"pins":[16,17]}' --approve [--json]
8343
+ embed tool call rp2350.monitor.gpio.write --input-json '{"pin":16,"level":true}' --approve [--json]
8344
+ embed tool call rp2350.monitor.uart.write --input-json '{"baud":115200,"text":"hello","line_ending":"lf"}' --approve [--json]
8345
+ embed tool call rp2350.monitor.i2c.transfer --input-json '{"address":"0x50","write":"00","read_len":4}' --approve [--json]
8346
+ embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve [--json]
8347
+ embed tool call rp2350.monitor.logic.capture --input-json '{"pin_base":16,"pin_count":4,"sample_rate":1000000,"samples":4096}' --approve [--json]
8348
+ embed tool call rp2350.monitor.logic.decode --input-json '{"input_path":".embed-labs/rp2350-monitor/captures/logic.jsonl","decoder":"summary"}' [--json]
8349
+ embed tool call rp2350.monitor.wifi.manage --input-json '{"action":"scan"}' --approve [--json]
8350
+ embed tool call rp2350.monitor.probe.debug --input-json '{"action":"status"}' --approve [--json]
8351
+ embed tool call rp2350.monitor.operation --input-json '{"action":"logic.stop","params":{}}' --approve [--json]
6353
8352
  embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
6354
8353
  embed board deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
6355
8354
  embed device list [--json]
@@ -6383,7 +8382,7 @@ Usage:
6383
8382
 
6384
8383
  Environment:
6385
8384
  EMBED_BRIDGE_URL=http://127.0.0.1:18083
6386
- EMBED_CLOUD_API_URL=http://127.0.0.1:18100
8385
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
6387
8386
  EMBED_API_TOKEN=<token>
6388
8387
  CODEX_HOME=~/.codex
6389
8388
  `);