@kvell007/embed-labs-cli 0.1.0-alpha.7 → 0.1.0-alpha.70

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, windowsWslInstall, windowsWslStatus } 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,20 @@ 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 MCP_START_USAGE = "Usage: embed mcp start [--bridge-path <path>]";
37
+ const LEGACY_CODEX_PLUGIN_NAMES = [
38
+ "dbt-agent",
39
+ "Dbt Agent",
40
+ "development-board-toolchain",
41
+ "development-board-toolchain-dev",
42
+ "deve"
43
+ ];
44
+ const LEGACY_CODEX_MARKETPLACE_NAMES = new Set(["embed-labs-plugins", "plugins", "Plugins", "deve"]);
30
45
  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]";
46
+ const PLUGIN_UPDATE_CHECK_USAGE = "Usage: embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
47
+ const PLUGIN_UPDATE_USAGE = "Usage: embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
31
48
  const CLOUD_TASK_ARTIFACTS_USAGE = "Usage: embed cloud task artifacts <task_id> [--json]";
32
49
  const CLOUD_TASK_EVIDENCE_USAGE = "Usage: embed cloud task evidence <task_id> [--json]";
33
50
  const ARTIFACT_STATUS_USAGE = "Usage: embed artifact status <artifact_id> [--json]";
@@ -81,24 +98,36 @@ const BUILD_IMAGE_BOOT_LOGO_USAGE = "Usage: embed build image boot-logo --logo <
81
98
  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
99
  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
100
  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]";
101
+ 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]";
102
+ 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]";
103
+ 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
104
  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]\nDefault source: the production download channel at download.embedboard.com.";
87
- const LOCAL_TOOLCHAIN_VALIDATE_USAGE = "Usage: embed local toolchain validate [--release-root <path>] [--json]";
105
+ 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.";
106
+ const LOCAL_TOOLCHAIN_UNINSTALL_USAGE = "Usage: embed local toolchain uninstall --board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor [--install-root <path>] [--yes|--force] [--json]";
107
+ 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]";
108
+ const LOCAL_WSL_STATUS_USAGE = "Usage: embed local wsl status [--json]";
109
+ const LOCAL_WSL_INSTALL_USAGE = "Usage: embed local wsl install [--distribution Ubuntu] [--no-launch true|false] [--web-download true|false] [--timeout-ms 600000] [--json]";
88
110
  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
- 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]";
111
+ const LOCAL_BUILD_QT_SMOKE_USAGE = "Usage: embed local build qt-smoke --build-dir <dir> [--source <qt-cmake-dir>] [--target-name <executable>] [--release-root <path>] [--account <account_id>] [--json]";
112
+ const AUTH_DEVICE_STATUS_USAGE = "Usage: embed auth device status [--json]";
113
+ const AUTH_DEVICE_LIST_USAGE = "Usage: embed auth device list [--json]";
114
+ const AUTH_DEVICE_REVOKE_USAGE = "Usage: embed auth device revoke <device_id> [--json]";
115
+ const AUTH_DEVICE_RENAME_USAGE = "Usage: embed auth device rename <device_id> --label <name> [--json]";
90
116
  const BOARD_REGISTRY_LIST_USAGE = "Usage: embed board registry list [--json]";
91
117
  const BOARD_REGISTRY_SHOW_USAGE = "Usage: embed board registry show <template_id> [--json]";
92
118
  const BOARD_METHODS_USAGE = "Usage: embed board methods <template_id> [--json]";
93
119
  const BOARD_KNOWLEDGE_USAGE = "Usage: embed board knowledge <template_id> [--json]";
120
+ const BOARD_KNOWLEDGE_SEARCH_USAGE = "Usage: embed board knowledge search <template_id> --query <text> [--source board_pack|build_template|registry] [--limit 5] [--json]";
94
121
  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
122
  const MODEL_LIST_USAGE = "Usage: embed model list [--json]";
96
123
  const MODEL_DEFAULT_USAGE = "Usage: embed model default [--json]";
97
124
  const SERVICE_MODES_USAGE = "Usage: embed service modes [--json]";
98
- 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]";
125
+ 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-command <cmd>] [--run] [--approve] [--json]";
126
+ let cachedLocalHardwareFingerprint;
99
127
  const TOOL_LIST_USAGE = "Usage: embed tool list [--json]";
100
128
  const TOOL_CALL_USAGE = "Usage: embed tool call <capability_id> [--input-json '<json>'] [--approve] [--json]";
101
- 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]";
129
+ 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]";
130
+ 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-command <cmd>] [--run] [--timeout 30] [--json]";
102
131
  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
132
  const TASK_STATES = new Set([
104
133
  "created",
@@ -147,15 +176,17 @@ async function main(argv) {
147
176
  return output(parsed, await bridgePost("/v1/board/taishanpi/deploy", request), renderBoardDeployResult);
148
177
  }
149
178
  if (area === "bridge" && action === "start") {
150
- startServer({
151
- host: stringFlag(parsed, "host"),
152
- port: numberFlag(parsed, "port")
153
- });
154
- return await waitForever();
179
+ return await runBridgeStart(parsed);
155
180
  }
156
181
  if (area === "bridge" && action === "status") {
157
182
  return output(parsed, await bridgeGet("/healthz"), renderBridgeStatus);
158
183
  }
184
+ if (area === "mcp" && action === "start") {
185
+ return await runMcpStart(parsed);
186
+ }
187
+ if (area === "mcp") {
188
+ return output(parsed, fail("invalid_args", MCP_START_USAGE), undefined, 2);
189
+ }
159
190
  if (area === "cloud" && action === "status") {
160
191
  return output(parsed, await cloudGet("/healthz"), renderCloudStatus);
161
192
  }
@@ -216,6 +247,17 @@ async function main(argv) {
216
247
  return output(parsed, await cloudGet(`/v1/board-registry/templates/${encodeURIComponent(idResult.value)}/methods`), renderBoardMethods);
217
248
  }
218
249
  if (action === "knowledge") {
250
+ if (parsed.command[2] === "search") {
251
+ const request = boardKnowledgeSearchRequest(parsed);
252
+ if (typeof request === "string") {
253
+ return output(parsed, fail("invalid_args", request), undefined, 2);
254
+ }
255
+ const params = new URLSearchParams({ q: request.query, limit: String(request.limit) });
256
+ if (request.source) {
257
+ params.set("source", request.source);
258
+ }
259
+ return output(parsed, await cloudGet(`/v1/board-registry/templates/${encodeURIComponent(request.templateId)}/knowledge-search?${params.toString()}`), renderBoardKnowledgeSearch);
260
+ }
219
261
  if (parsed.command[2] === "file") {
220
262
  const request = boardKnowledgeFileRequest(parsed);
221
263
  if (typeof request === "string") {
@@ -241,6 +283,7 @@ async function main(argv) {
241
283
  BOARD_REGISTRY_SHOW_USAGE,
242
284
  BOARD_METHODS_USAGE,
243
285
  BOARD_KNOWLEDGE_USAGE,
286
+ BOARD_KNOWLEDGE_SEARCH_USAGE,
244
287
  BOARD_KNOWLEDGE_FILE_USAGE
245
288
  ].join("\n")), undefined, 2);
246
289
  }
@@ -279,7 +322,18 @@ async function main(argv) {
279
322
  const result = await pluginInstall(parsed);
280
323
  return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
281
324
  }
282
- return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE].join("\n")), undefined, 2);
325
+ if (action === "update") {
326
+ if (parsed.command[2] === "check") {
327
+ const result = await pluginUpdateCheck(parsed);
328
+ return output(parsed, result, renderPluginUpdateCheck, result.ok ? 0 : 2);
329
+ }
330
+ if (["codex", "opencode", "all"].includes(parsed.command[2] ?? "")) {
331
+ const result = await pluginUpdate(parsed);
332
+ return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
333
+ }
334
+ return output(parsed, fail("invalid_args", [PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
335
+ }
336
+ return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE, PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
283
337
  }
284
338
  if (area === "auth" && action === "login") {
285
339
  const result = await authLogin(parsed);
@@ -288,6 +342,31 @@ async function main(argv) {
288
342
  if (area === "auth" && action === "status") {
289
343
  return output(parsed, ok(await authStatus()), renderAuthStatus);
290
344
  }
345
+ if (area === "auth" && action === "device") {
346
+ const deviceAction = parsed.command[2] ?? "status";
347
+ if (deviceAction === "status") {
348
+ const result = await authDeviceStatus(parsed);
349
+ return output(parsed, result, renderAuthDeviceStatus, result.ok ? 0 : 2);
350
+ }
351
+ if (deviceAction === "list") {
352
+ const result = await authDeviceList(parsed);
353
+ return output(parsed, result, renderAuthDeviceList, result.ok ? 0 : 2);
354
+ }
355
+ if (deviceAction === "revoke") {
356
+ const result = await authDeviceRevoke(parsed);
357
+ return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
358
+ }
359
+ if (deviceAction === "rename") {
360
+ const result = await authDeviceRename(parsed);
361
+ return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
362
+ }
363
+ return output(parsed, fail("invalid_args", [
364
+ AUTH_DEVICE_STATUS_USAGE,
365
+ AUTH_DEVICE_LIST_USAGE,
366
+ AUTH_DEVICE_REVOKE_USAGE,
367
+ AUTH_DEVICE_RENAME_USAGE
368
+ ].join("\n")), undefined, 2);
369
+ }
291
370
  if (area === "auth" && action === "logout") {
292
371
  await rm(DEFAULT_AUTH_FILE, { force: true });
293
372
  return output(parsed, ok(await authStatus()), renderAuthStatus);
@@ -339,6 +418,9 @@ async function main(argv) {
339
418
  ACCOUNT_KEY_REVOKE_USAGE
340
419
  ].join("\n")), undefined, 2);
341
420
  }
421
+ if (area === "usage" || area === "billing") {
422
+ return output(parsed, quotaAndBillingDisabled(), undefined, 2);
423
+ }
342
424
  if (area === "usage") {
343
425
  if (action === "record") {
344
426
  const body = usageRecordBody(parsed);
@@ -367,6 +449,16 @@ async function main(argv) {
367
449
  USAGE_EVENTS_USAGE
368
450
  ].join("\n")), undefined, 2);
369
451
  }
452
+ if (area === "mcp") {
453
+ if (action === "log" || action === "tool-event") {
454
+ const body = mcpToolEventBody(parsed);
455
+ if (typeof body === "string") {
456
+ return output(parsed, fail("invalid_args", body), undefined, 2);
457
+ }
458
+ return output(parsed, await cloudPost("/v1/mcp/tool-events", body), renderMcpToolEvent);
459
+ }
460
+ return output(parsed, fail("invalid_args", MCP_TOOL_EVENT_USAGE), undefined, 2);
461
+ }
370
462
  if (area === "billing") {
371
463
  if (action === "statement") {
372
464
  const request = billingStatementRequest(parsed);
@@ -506,6 +598,20 @@ async function main(argv) {
506
598
  return output(parsed, fail("invalid_args", [IMAGE_BOOT_LOGO_COMPOSE_USAGE, IMAGE_DTB_COMPOSE_USAGE].join("\n")), undefined, 2);
507
599
  }
508
600
  if (area === "local") {
601
+ if (action === "toolchain" && parsed.command[2] === "list") {
602
+ const request = localToolchainListRequest(parsed);
603
+ if (typeof request === "string") {
604
+ return output(parsed, fail("invalid_args", request), undefined, 2);
605
+ }
606
+ return output(parsed, ok(await listLocalToolchainEnvironments(request)), renderLocalToolchainList);
607
+ }
608
+ if (action === "toolchain" && parsed.command[2] === "installed") {
609
+ const request = localToolchainListRequest(parsed, LOCAL_TOOLCHAIN_INSTALLED_USAGE);
610
+ if (typeof request === "string") {
611
+ return output(parsed, fail("invalid_args", request), undefined, 2);
612
+ }
613
+ return output(parsed, ok(await listLocalToolchainEnvironments({ ...request, installedOnly: true })), renderLocalToolchainList);
614
+ }
509
615
  if (action === "toolchain" && parsed.command[2] === "latest") {
510
616
  const request = localToolchainLatestRequest(parsed);
511
617
  if (typeof request === "string") {
@@ -527,12 +633,34 @@ async function main(argv) {
527
633
  }
528
634
  return output(parsed, ok(await installLocalToolchain(request)), renderLocalToolchainInstall);
529
635
  }
636
+ if (action === "toolchain" && (parsed.command[2] === "uninstall" || parsed.command[2] === "remove")) {
637
+ const request = localToolchainUninstallRequest(parsed);
638
+ if (typeof request === "string") {
639
+ return output(parsed, fail("invalid_args", request), undefined, 2);
640
+ }
641
+ return output(parsed, ok(await uninstallLocalToolchain(request)), renderLocalToolchainUninstall);
642
+ }
530
643
  if (action === "toolchain" && parsed.command[2] === "validate") {
531
644
  const request = localToolchainValidateRequest(parsed);
532
645
  if (typeof request === "string") {
533
646
  return output(parsed, fail("invalid_args", request), undefined, 2);
534
647
  }
535
- return output(parsed, ok(await validateLocalToolchain(request.releaseRoot)), renderLocalToolchainValidation);
648
+ return output(parsed, ok(await validateLocalToolchain(request)), renderLocalToolchainValidation);
649
+ }
650
+ if (action === "wsl" && parsed.command[2] === "status") {
651
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
652
+ const extra = parsed.command.slice(3);
653
+ if (unknownFlag || extra.length > 0) {
654
+ return output(parsed, fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${LOCAL_WSL_STATUS_USAGE}` : LOCAL_WSL_STATUS_USAGE), undefined, 2);
655
+ }
656
+ return output(parsed, ok(await windowsWslStatus()), renderWindowsWslStatus);
657
+ }
658
+ if (action === "wsl" && parsed.command[2] === "install") {
659
+ const request = localWslInstallRequest(parsed);
660
+ if (typeof request === "string") {
661
+ return output(parsed, fail("invalid_args", request), undefined, 2);
662
+ }
663
+ return output(parsed, ok(await windowsWslInstall(request)), renderWindowsWslInstall);
536
664
  }
537
665
  if (action === "compile" && parsed.command[2] === "taishanpi") {
538
666
  const request = localCompileTaishanPiRequest(parsed, await authStatus());
@@ -549,10 +677,15 @@ async function main(argv) {
549
677
  return output(parsed, ok(await buildTaishanPiQtSmoke(request)), renderLocalCompileResult);
550
678
  }
551
679
  return output(parsed, fail("invalid_args", [
680
+ LOCAL_TOOLCHAIN_LIST_USAGE,
681
+ LOCAL_TOOLCHAIN_INSTALLED_USAGE,
552
682
  LOCAL_TOOLCHAIN_LATEST_USAGE,
553
683
  LOCAL_TOOLCHAIN_CURRENT_USAGE,
554
684
  LOCAL_TOOLCHAIN_INSTALL_USAGE,
685
+ LOCAL_TOOLCHAIN_UNINSTALL_USAGE,
555
686
  LOCAL_TOOLCHAIN_VALIDATE_USAGE,
687
+ LOCAL_WSL_STATUS_USAGE,
688
+ LOCAL_WSL_INSTALL_USAGE,
556
689
  LOCAL_COMPILE_TAISHANPI_USAGE,
557
690
  LOCAL_BUILD_QT_SMOKE_USAGE
558
691
  ].join("\n")), undefined, 2);
@@ -984,11 +1117,18 @@ async function main(argv) {
984
1117
  return output(parsed, fail("unknown_command", `Unknown command: ${parsed.command.join(" ")}`), undefined, 2);
985
1118
  }
986
1119
  catch (error) {
987
- return output(parsed, fail("command_failed", error instanceof Error ? error.message : String(error), {
988
- remediation: `Check that embed-local-bridge is running at ${DEFAULT_BRIDGE_URL}. Start it with: embed bridge start`
1120
+ const message = error instanceof Error ? error.message : String(error);
1121
+ return output(parsed, fail("command_failed", message, {
1122
+ remediation: commandFailureRemediation(message)
989
1123
  }), undefined, 1);
990
1124
  }
991
1125
  }
1126
+ function commandFailureRemediation(message) {
1127
+ if (message.includes("TaishanPi") && message.includes("WSL2")) {
1128
+ return "Run: embedlabs local wsl status";
1129
+ }
1130
+ return `Check that embed-local-bridge is running at ${DEFAULT_BRIDGE_URL}. Start it with: embed bridge start`;
1131
+ }
992
1132
  function parseArgs(argv) {
993
1133
  const command = [];
994
1134
  const flags = {};
@@ -1098,6 +1238,7 @@ function flashBody(parsed, includeApproval) {
1098
1238
  variant_id: stringFlag(parsed, "variant"),
1099
1239
  hardware_profile_id: stringFlag(parsed, "hardware-profile"),
1100
1240
  profile_id: stringFlag(parsed, "profile"),
1241
+ local_device_id: stringFlag(parsed, "local-device-id") ?? stringFlag(parsed, "device-id"),
1101
1242
  image_dir: stringFlag(parsed, "image-dir"),
1102
1243
  artifact_path: stringFlag(parsed, "artifact"),
1103
1244
  target_volume_path: stringFlag(parsed, "target-volume")
@@ -1126,11 +1267,11 @@ async function doctor() {
1126
1267
  const bridgeHealth = await apiDoctorCheck("bridge_health", "Local Bridge health", `${bridgeBaseUrl}/healthz`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderHealthSummary, healthStatus);
1127
1268
  checks.push(bridgeHealth);
1128
1269
  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));
1270
+ checks.push(await apiDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDeviceScanSummary, warningIfWarnings));
1130
1271
  checks.push(await apiDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDebugToolScanSummary, warningIfWarnings));
1131
1272
  }
1132
1273
  else {
1133
- checks.push(dependentDoctorCheck("device_scan", "Device scan", `${bridgeBaseUrl}/v1/devices`, "Device scan requires a reachable Local Bridge."));
1274
+ checks.push(dependentDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "Device inventory requires a reachable Local Bridge."));
1134
1275
  checks.push(dependentDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "Debug tool scan requires a reachable Local Bridge."));
1135
1276
  }
1136
1277
  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 +1330,8 @@ function authDoctorCheck(status) {
1189
1330
  : {
1190
1331
  code: "auth_not_ready",
1191
1332
  message: "No CLI auth token is configured.",
1192
- remediation: "Run: embed auth login --token <token>"
1333
+ remediation: cloudAuthSetupRemediation(),
1334
+ details: cloudAuthSetupDetails()
1193
1335
  }
1194
1336
  };
1195
1337
  }
@@ -1322,7 +1464,7 @@ function warningIfWarnings(data) {
1322
1464
  }
1323
1465
  function renderDeviceScanSummary(result) {
1324
1466
  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}`;
1467
+ return `Device inventory snapshot: ${result.devices.length} device(s), ${result.usb.length} USB item(s), ${result.serial.length} serial port(s).${warningText}`;
1326
1468
  }
1327
1469
  function renderDebugToolScanSummary(result) {
1328
1470
  const available = result.tools.filter((tool) => tool.available).length;
@@ -1340,16 +1482,142 @@ function isApiResponse(value) {
1340
1482
  return isJsonObject(error) && typeof error.code === "string" && typeof error.message === "string";
1341
1483
  }
1342
1484
  async function bridgeGet(path) {
1343
- const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`);
1344
- return await response.json();
1485
+ return await bridgeRequest("GET", path);
1345
1486
  }
1346
1487
  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)
1488
+ return await bridgeRequest("POST", path, body);
1489
+ }
1490
+ async function bridgeRequest(method, path, body) {
1491
+ const bodyText = body === undefined ? "" : JSON.stringify(body);
1492
+ const makeRequest = async () => {
1493
+ const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
1494
+ method,
1495
+ headers: bridgeHeaders(method, path, method === "POST" ? bodyText : "", method === "POST" ? { "content-type": "application/json" } : {}),
1496
+ body: method === "POST" ? bodyText : undefined
1497
+ });
1498
+ return await response.json();
1499
+ };
1500
+ try {
1501
+ return await makeRequest();
1502
+ }
1503
+ catch (error) {
1504
+ if (!isBridgeConnectionFailure(error)) {
1505
+ throw error;
1506
+ }
1507
+ const started = await ensureBridgeStartedForRequest();
1508
+ if (!started.ok) {
1509
+ return started;
1510
+ }
1511
+ return await makeRequest();
1512
+ }
1513
+ }
1514
+ function bridgeHeaders(method, path, bodyText, base = {}) {
1515
+ const token = process.env.EMBED_BRIDGE_TOKEN?.trim();
1516
+ if (!token) {
1517
+ return base;
1518
+ }
1519
+ const headers = {
1520
+ ...base,
1521
+ authorization: `Bearer ${token}`
1522
+ };
1523
+ addBridgeRequestSignature(headers, method, path, bodyText, token);
1524
+ return headers;
1525
+ }
1526
+ function addBridgeRequestSignature(headers, method, pathWithQuery, bodyText, token) {
1527
+ if (process.env.EMBED_BRIDGE_SIGNING === "0") {
1528
+ return;
1529
+ }
1530
+ const timestamp = String(Math.floor(Date.now() / 1000));
1531
+ const nonce = randomBytes(16).toString("hex");
1532
+ const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
1533
+ const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
1534
+ const canonical = cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1535
+ headers["x-embed-key-id"] = keyId;
1536
+ headers["x-embed-timestamp"] = timestamp;
1537
+ headers["x-embed-nonce"] = nonce;
1538
+ headers["x-embed-body-sha256"] = bodySha256;
1539
+ headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
1540
+ }
1541
+ function isBridgeConnectionFailure(error) {
1542
+ const message = error instanceof Error ? error.message : String(error);
1543
+ return message.includes("fetch failed") ||
1544
+ message.includes("ECONNREFUSED") ||
1545
+ message.includes("ECONNRESET") ||
1546
+ message.includes("UND_ERR_SOCKET");
1547
+ }
1548
+ async function ensureBridgeStartedForRequest() {
1549
+ if (process.env.EMBED_BRIDGE_AUTO_START === "0") {
1550
+ return fail("bridge_unavailable", `embed-local-bridge is not running at ${DEFAULT_BRIDGE_URL}.`, {
1551
+ remediation: `Start it with: embed bridge start`
1552
+ });
1553
+ }
1554
+ let bridgeURL;
1555
+ try {
1556
+ bridgeURL = new URL(DEFAULT_BRIDGE_URL);
1557
+ }
1558
+ catch {
1559
+ return fail("bridge_url_invalid", `EMBED_BRIDGE_URL is not a valid URL: ${DEFAULT_BRIDGE_URL}`);
1560
+ }
1561
+ if (!isLocalBridgeURL(bridgeURL)) {
1562
+ return fail("bridge_unavailable", `embed-local-bridge is not reachable at ${DEFAULT_BRIDGE_URL}.`, {
1563
+ remediation: `Start the bridge for that host, or set EMBED_BRIDGE_URL to a local bridge URL.`
1564
+ });
1565
+ }
1566
+ const launcher = await resolveBridgeLauncher();
1567
+ const host = bridgeURL.hostname === "::1" ? "::1" : bridgeURL.hostname || "127.0.0.1";
1568
+ const port = bridgeURL.port || "18083";
1569
+ const env = {
1570
+ ...process.env,
1571
+ EMBED_BRIDGE_HOST: host,
1572
+ EMBED_BRIDGE_PORT: port
1573
+ };
1574
+ const child = spawn(launcher.command, [...launcher.args, "--host", host, "--port", port], {
1575
+ cwd: process.cwd(),
1576
+ detached: true,
1577
+ stdio: "ignore",
1578
+ env
1579
+ });
1580
+ child.unref();
1581
+ const ready = await waitForBridgeHealth(bridgeURL, 8000);
1582
+ if (!ready.ok) {
1583
+ return ready;
1584
+ }
1585
+ return ok({
1586
+ started: true,
1587
+ bridge_url: DEFAULT_BRIDGE_URL,
1588
+ command: launcher.command
1351
1589
  });
1352
- return await response.json();
1590
+ }
1591
+ function isLocalBridgeURL(url) {
1592
+ const host = url.hostname.toLowerCase();
1593
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
1594
+ }
1595
+ async function waitForBridgeHealth(bridgeURL, timeoutMs) {
1596
+ const deadline = Date.now() + timeoutMs;
1597
+ let lastError = "";
1598
+ while (Date.now() < deadline) {
1599
+ try {
1600
+ const response = await fetch(new URL("/healthz", bridgeURL), {
1601
+ headers: bridgeHeaders("GET", "/healthz", "")
1602
+ });
1603
+ const parsed = await response.json();
1604
+ if (parsed.ok) {
1605
+ return parsed;
1606
+ }
1607
+ lastError = parsed.error?.message ?? `HTTP ${response.status}`;
1608
+ }
1609
+ catch (error) {
1610
+ lastError = error instanceof Error ? error.message : String(error);
1611
+ }
1612
+ await delay(100);
1613
+ }
1614
+ return fail("bridge_start_failed", `embed-local-bridge did not become healthy at ${DEFAULT_BRIDGE_URL}.`, {
1615
+ remediation: `Run embed bridge start in a separate terminal and retry.`,
1616
+ details: { last_error: lastError }
1617
+ });
1618
+ }
1619
+ function delay(ms) {
1620
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
1353
1621
  }
1354
1622
  async function cloudGet(path) {
1355
1623
  return await cloudRequest("GET", path);
@@ -1360,16 +1628,23 @@ async function cloudPost(path, body) {
1360
1628
  async function cloudDownloadArtifact(artifactId, outputPath) {
1361
1629
  try {
1362
1630
  const headers = {};
1363
- const token = await cloudAuthToken();
1364
- if (token) {
1365
- headers.authorization = `Bearer ${token}`;
1631
+ const auth = await cloudAuthConfig();
1632
+ if (auth.token) {
1633
+ if (auth.device) {
1634
+ const integrity = await validateLocalDeviceIntegrity(auth.device);
1635
+ if (!integrity.ok) {
1636
+ return integrity;
1637
+ }
1638
+ }
1639
+ headers.authorization = `Bearer ${auth.token}`;
1640
+ addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", auth.token, auth.device);
1366
1641
  }
1367
1642
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/artifacts/${encodeURIComponent(artifactId)}/download`, {
1368
1643
  headers: Object.keys(headers).length > 0 ? headers : undefined
1369
1644
  });
1370
1645
  if (!response.ok) {
1371
1646
  const parsed = await parseErrorResponse(response);
1372
- return parsed ?? fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
1647
+ return parsed ? enrichCloudAuthFailure(parsed, Boolean(auth.token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
1373
1648
  }
1374
1649
  const bytes = Buffer.from(await response.arrayBuffer());
1375
1650
  const expectedSha256 = response.headers.get("x-embed-artifact-sha256")?.trim();
@@ -1398,19 +1673,28 @@ async function cloudDownloadArtifact(artifactId, outputPath) {
1398
1673
  async function cloudRequest(method, path, body) {
1399
1674
  try {
1400
1675
  const headers = {};
1401
- if (body !== undefined) {
1676
+ const bodyText = body === undefined ? "" : JSON.stringify(body);
1677
+ if (bodyText) {
1402
1678
  headers["content-type"] = "application/json";
1403
1679
  }
1404
- const token = await cloudAuthToken();
1405
- if (token) {
1406
- headers.authorization = `Bearer ${token}`;
1680
+ const auth = await cloudAuthConfig();
1681
+ if (auth.token) {
1682
+ if (auth.device) {
1683
+ const integrity = await validateLocalDeviceIntegrity(auth.device);
1684
+ if (!integrity.ok) {
1685
+ return integrity;
1686
+ }
1687
+ }
1688
+ headers.authorization = `Bearer ${auth.token}`;
1689
+ addCloudRequestSignature(headers, method, path, bodyText, auth.token, auth.device);
1407
1690
  }
1408
1691
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}${path}`, {
1409
1692
  method,
1410
1693
  headers: Object.keys(headers).length > 0 ? headers : undefined,
1411
- body: body === undefined ? undefined : JSON.stringify(body)
1694
+ body: body === undefined ? undefined : bodyText
1412
1695
  });
1413
- return await response.json();
1696
+ const parsed = await response.json();
1697
+ return enrichCloudAuthFailure(parsed, Boolean(auth.token));
1414
1698
  }
1415
1699
  catch (error) {
1416
1700
  return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
@@ -1418,6 +1702,129 @@ async function cloudRequest(method, path, body) {
1418
1702
  });
1419
1703
  }
1420
1704
  }
1705
+ async function validateLocalDeviceIntegrity(device) {
1706
+ const current = await localHardwareFingerprint();
1707
+ if (current.fingerprint_hash === device.fingerprint_hash) {
1708
+ return ok(undefined);
1709
+ }
1710
+ return fail("tool_integrity_check_failed", TOOL_INTEGRITY_RELOGIN_MESSAGE, {
1711
+ remediation: [
1712
+ "当前 Embed Labs CLI/插件配置绑定的电脑与本机硬件唯一码不一致。",
1713
+ TOOL_INTEGRITY_RELOGIN_MESSAGE,
1714
+ "如果账号设备数量已达上限,请先在原电脑或用户后台撤销旧设备。"
1715
+ ].join("\n"),
1716
+ details: {
1717
+ expected_fingerprint_hash: device.fingerprint_hash,
1718
+ current_fingerprint_hash: current.fingerprint_hash,
1719
+ platform: current.platform,
1720
+ arch: current.arch,
1721
+ fingerprint_source: current.source
1722
+ }
1723
+ });
1724
+ }
1725
+ function enrichCloudAuthFailure(response, hadToken) {
1726
+ if (response.ok) {
1727
+ return response;
1728
+ }
1729
+ if (response.error.code.startsWith("device_") || response.error.code.startsWith("request_signature_")) {
1730
+ return fail(response.error.code, response.error.message, {
1731
+ remediation: [
1732
+ "This computer is not fully registered for the configured Embed Labs API Token.",
1733
+ "Run: embedlabs auth login --token <your_token>",
1734
+ "Then verify with: embedlabs auth device status",
1735
+ "If the account already has too many devices, revoke one with: embedlabs auth device revoke <device_id>"
1736
+ ].join("\n"),
1737
+ details: response.error.details
1738
+ });
1739
+ }
1740
+ if (response.error.code !== "unauthorized") {
1741
+ return response;
1742
+ }
1743
+ if (!hadToken) {
1744
+ 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.", {
1745
+ remediation: cloudAuthSetupRemediation(),
1746
+ details: cloudAuthSetupDetails()
1747
+ });
1748
+ }
1749
+ 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.", {
1750
+ remediation: cloudAuthSetupRemediation(),
1751
+ details: cloudAuthSetupDetails()
1752
+ });
1753
+ }
1754
+ function cloudAuthSetupRemediation() {
1755
+ return [
1756
+ `1. Open ${DEFAULT_DASHBOARD_URL} and register or sign in.`,
1757
+ "2. Create or copy your Embed Labs API Token from the user dashboard.",
1758
+ "3. Run: embedlabs auth login --token <your_token>",
1759
+ "4. For automation, set: EMBED_API_TOKEN=<your_token>",
1760
+ "5. Verify with: embedlabs auth status"
1761
+ ].join("\n");
1762
+ }
1763
+ function cloudAuthSetupDetails() {
1764
+ return {
1765
+ dashboard_url: DEFAULT_DASHBOARD_URL,
1766
+ login_command: "embedlabs auth login --token <your_token>",
1767
+ env_var: "EMBED_API_TOKEN",
1768
+ auth_status_command: "embedlabs auth status",
1769
+ auth_file: DEFAULT_AUTH_FILE
1770
+ };
1771
+ }
1772
+ function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token, device) {
1773
+ if (process.env.EMBED_CLOUD_API_SIGNING === "0") {
1774
+ return;
1775
+ }
1776
+ const timestamp = String(Math.floor(Date.now() / 1000));
1777
+ const nonce = randomBytes(16).toString("hex");
1778
+ const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
1779
+ const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
1780
+ const canonical = device
1781
+ ? cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, EMBED_CLIENT_NAME, EMBED_CLIENT_VERSION)
1782
+ : cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1783
+ headers["x-embed-key-id"] = keyId;
1784
+ headers["x-embed-timestamp"] = timestamp;
1785
+ headers["x-embed-nonce"] = nonce;
1786
+ headers["x-embed-body-sha256"] = bodySha256;
1787
+ if (device) {
1788
+ headers["x-embed-signature-version"] = "v2";
1789
+ headers["x-embed-device-id"] = device.device_id;
1790
+ headers["x-embed-device-fingerprint-sha256"] = device.fingerprint_hash;
1791
+ headers["x-embed-client-name"] = EMBED_CLIENT_NAME;
1792
+ headers["x-embed-client-version"] = EMBED_CLIENT_VERSION;
1793
+ headers["x-embed-device-signature"] = signCrypto(null, Buffer.from(canonical), device.private_key_pem).toString("base64url");
1794
+ }
1795
+ headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
1796
+ }
1797
+ function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256) {
1798
+ return [
1799
+ method.toUpperCase(),
1800
+ normalizeCloudPathForSignature(pathWithQuery),
1801
+ timestamp,
1802
+ nonce,
1803
+ bodySha256
1804
+ ].join("\n");
1805
+ }
1806
+ function cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, clientName, clientVersion) {
1807
+ return [
1808
+ method.toUpperCase(),
1809
+ normalizeCloudPathForSignature(pathWithQuery),
1810
+ timestamp,
1811
+ nonce,
1812
+ bodySha256,
1813
+ device.device_id,
1814
+ device.fingerprint_hash,
1815
+ clientName,
1816
+ clientVersion
1817
+ ].join("\n");
1818
+ }
1819
+ function normalizeCloudPathForSignature(pathWithQuery) {
1820
+ try {
1821
+ const parsed = new URL(pathWithQuery, "http://embed.local");
1822
+ return `${parsed.pathname}${parsed.search}`;
1823
+ }
1824
+ catch {
1825
+ return pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`;
1826
+ }
1827
+ }
1421
1828
  async function pluginList(parsed) {
1422
1829
  const releaseDir = stringFlag(parsed, "release-dir");
1423
1830
  const manifest = releaseDir ? await readPluginReleaseManifest(releaseDir) : undefined;
@@ -1517,13 +1924,112 @@ async function pluginInstall(parsed) {
1517
1924
  await rm(tempDir, { recursive: true, force: true });
1518
1925
  }
1519
1926
  }
1927
+ async function pluginUpdateCheck(parsed) {
1928
+ const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
1929
+ if (unknownFlag) {
1930
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
1931
+ }
1932
+ const unexpected = parsed.command.slice(3);
1933
+ if (unexpected.length > 0) {
1934
+ return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
1935
+ }
1936
+ const remoteManifest = await fetchRemotePluginManifest(parsed);
1937
+ if (!remoteManifest.ok) {
1938
+ return remoteManifest;
1939
+ }
1940
+ const manifest = remoteManifest.data;
1941
+ const codexPackage = manifest.packages?.find((item) => item.id === "codex-embed-labs");
1942
+ const opencodePackage = manifest.packages?.find((item) => item.id === "opencode-embed-labs");
1943
+ const codexTarget = join(codexPluginTargetRoot(parsed, true), CODEX_PLUGIN_NAME);
1944
+ const openCodeTarget = openCodePluginTargetRoot(parsed, true);
1945
+ return ok({
1946
+ release_url: pluginReleaseBaseUrl(parsed),
1947
+ latest_version: manifest.version,
1948
+ release_notes: normalizedReleaseNotes(manifest.release_notes),
1949
+ plugins: [
1950
+ await pluginUpdateItem({
1951
+ id: "codex",
1952
+ displayName: "Embed Labs Codex plugin",
1953
+ targetPath: codexTarget,
1954
+ installedVersion: await installedCodexPluginVersion(codexTarget),
1955
+ latestVersion: codexPackage?.version ?? manifest.version,
1956
+ releaseFile: codexPackage?.file,
1957
+ updateCommand: "embedlabs plugin update codex"
1958
+ }),
1959
+ await pluginUpdateItem({
1960
+ id: "opencode",
1961
+ displayName: "Embed Labs OpenCode plugin",
1962
+ targetPath: openCodeTarget,
1963
+ installedVersion: await installedOpenCodePluginVersion(openCodeTarget),
1964
+ latestVersion: opencodePackage?.version ?? manifest.version,
1965
+ releaseFile: opencodePackage?.file,
1966
+ updateCommand: "embedlabs plugin update opencode"
1967
+ })
1968
+ ]
1969
+ });
1970
+ }
1971
+ function normalizedReleaseNotes(notes) {
1972
+ if (!Array.isArray(notes)) {
1973
+ return [];
1974
+ }
1975
+ return notes
1976
+ .filter((item) => typeof item === "string" && item.trim().length > 0)
1977
+ .map((item) => item.trim());
1978
+ }
1979
+ async function pluginUpdate(parsed) {
1980
+ const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
1981
+ if (unknownFlag) {
1982
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_USAGE}`);
1983
+ }
1984
+ const target = parsed.command[2];
1985
+ if (!target || !["codex", "opencode", "all"].includes(target)) {
1986
+ return fail("invalid_args", PLUGIN_UPDATE_USAGE);
1987
+ }
1988
+ const unexpected = parsed.command.slice(3);
1989
+ if (unexpected.length > 0) {
1990
+ return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_USAGE}`);
1991
+ }
1992
+ const installParsed = {
1993
+ ...parsed,
1994
+ command: ["plugin", "install", target],
1995
+ flags: { ...parsed.flags, force: true }
1996
+ };
1997
+ return await pluginInstall(installParsed);
1998
+ }
1999
+ async function pluginUpdateItem(input) {
2000
+ const installed = !!input.installedVersion;
2001
+ const updateAvailable = !!input.latestVersion && input.installedVersion !== input.latestVersion;
2002
+ const notes = [];
2003
+ if (!installed) {
2004
+ notes.push("Plugin is not installed in the selected target.");
2005
+ }
2006
+ else if (updateAvailable) {
2007
+ notes.push("A newer plugin release is available. Run the update command, then restart Codex/OpenCode.");
2008
+ }
2009
+ else {
2010
+ notes.push("Plugin is up to date for the selected release channel.");
2011
+ }
2012
+ return {
2013
+ id: input.id,
2014
+ display_name: input.displayName,
2015
+ installed,
2016
+ installed_version: input.installedVersion,
2017
+ latest_version: input.latestVersion,
2018
+ update_available: updateAvailable,
2019
+ target_path: input.targetPath,
2020
+ release_file: input.releaseFile,
2021
+ update_command: input.updateCommand,
2022
+ notes
2023
+ };
2024
+ }
1520
2025
  async function installCodexPlugin(parsed, context) {
1521
2026
  const source = await resolveCodexPluginSource(context);
1522
2027
  if (!source.ok) {
1523
2028
  return source;
1524
2029
  }
1525
2030
  const targetRoot = codexPluginTargetRoot(parsed, context.installingAll);
1526
- const targetPath = join(targetRoot, "embed-labs");
2031
+ const targetPath = join(targetRoot, CODEX_PLUGIN_NAME);
2032
+ const legacyCleanup = await cleanupLegacyCodexPluginRemnants(targetRoot);
1527
2033
  if (await pathExists(targetPath) && !booleanFlag(parsed, "force")) {
1528
2034
  return fail("plugin_already_installed", `Codex plugin already exists at ${targetPath}.`, {
1529
2035
  remediation: "Pass --force to replace it, or pass --codex-target/--target to install into a different directory."
@@ -1533,16 +2039,24 @@ async function installCodexPlugin(parsed, context) {
1533
2039
  await mkdir(targetRoot, { recursive: true });
1534
2040
  await cp(source.data.sourcePath, targetPath, { recursive: true });
1535
2041
  const mcpRegistration = await maybeRegisterCodexMcp(parsed, targetRoot, targetPath);
2042
+ const marketplaceRegistration = await maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath);
1536
2043
  return ok({
1537
2044
  id: "codex",
1538
2045
  target_path: targetPath,
1539
2046
  source: source.data.sourceLabel,
1540
2047
  version: source.data.version,
1541
2048
  command_hint: mcpRegistration.registered
1542
- ? "Codex MCP was registered. Start a new Codex session to reload tools."
2049
+ ? (marketplaceRegistration.registered
2050
+ ? "Codex MCP and plugin marketplace entry were registered. Fully restart Codex to reload @Embed Labs."
2051
+ : "Codex MCP was registered. Start a new Codex session to reload tools.")
1543
2052
  : mcpRegistration.hint,
2053
+ warning: legacyCodexCleanupWarning(legacyCleanup),
1544
2054
  mcp_registered: mcpRegistration.registered,
1545
- mcp_warning: mcpRegistration.warning
2055
+ mcp_warning: mcpRegistration.warning,
2056
+ marketplace_registered: marketplaceRegistration.registered,
2057
+ marketplace_path: marketplaceRegistration.marketplacePath,
2058
+ marketplace_warning: marketplaceRegistration.warning,
2059
+ cleanup: legacyCleanup
1546
2060
  });
1547
2061
  }
1548
2062
  async function installOpenCodePlugin(parsed, context) {
@@ -1551,15 +2065,17 @@ async function installOpenCodePlugin(parsed, context) {
1551
2065
  return source;
1552
2066
  }
1553
2067
  const targetRoot = openCodePluginTargetRoot(parsed, context.installingAll);
1554
- const wrapperPath = join(targetRoot, "plugins", "development-board-toolchain.js");
1555
- if (await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
2068
+ const globalInstall = isGlobalOpenCodeRoot(targetRoot);
2069
+ const wrapperPath = join(targetRoot, "plugins", "embed-labs.js");
2070
+ const legacyCleanup = await cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall);
2071
+ if (!globalInstall && await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
1556
2072
  return fail("plugin_already_installed", `OpenCode plugin wrapper already exists at ${wrapperPath}.`, {
1557
2073
  remediation: "Pass --force to replace it, or pass --opencode-target/--target to install into a different directory."
1558
2074
  });
1559
2075
  }
1560
2076
  await mkdir(join(targetRoot, "plugins"), { recursive: true });
1561
2077
  const packagePath = await prepareOpenCodePackageForInstall(targetRoot, source.data.packagePath);
1562
- const npmResult = await runLocalProcess("npm", [
2078
+ const npmResult = await runLocalProcess(npmCommand(), [
1563
2079
  "install",
1564
2080
  "--prefix",
1565
2081
  targetRoot,
@@ -1578,15 +2094,25 @@ async function installOpenCodePlugin(parsed, context) {
1578
2094
  });
1579
2095
  }
1580
2096
  await ensureOpenCodeInstallPackageJson(targetRoot);
1581
- await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
2097
+ if (globalInstall) {
2098
+ await rm(wrapperPath, { force: true });
2099
+ legacyCleanup.legacy_removed_config_entries?.push(...await ensureOpenCodeGlobalPluginConfig());
2100
+ }
2101
+ else {
2102
+ await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
2103
+ }
1582
2104
  const duplicateWarning = await openCodeDuplicatePluginWarning(targetRoot);
2105
+ const cleanupWarning = legacyOpenCodeCleanupWarning(legacyCleanup);
1583
2106
  return ok({
1584
2107
  id: "opencode",
1585
2108
  target_path: targetRoot,
1586
2109
  source: source.data.sourceLabel,
1587
2110
  version: source.data.version,
1588
- command_hint: "Start OpenCode from the project containing this .opencode directory.",
1589
- warning: duplicateWarning
2111
+ command_hint: globalInstall
2112
+ ? "Restart OpenCode so the global embed-labs package plugin is reloaded."
2113
+ : "Start OpenCode from the project containing this .opencode directory.",
2114
+ warning: combineWarnings(cleanupWarning, duplicateWarning),
2115
+ cleanup: legacyCleanup
1590
2116
  });
1591
2117
  }
1592
2118
  async function resolveCodexPluginSource(context) {
@@ -1641,7 +2167,7 @@ async function resolveOpenCodePluginSource(context) {
1641
2167
  remediation: "Run from the Embed-Labs-Cloud repo root or pass --release-dir pointing to a plugin release directory."
1642
2168
  });
1643
2169
  }
1644
- const packed = await runLocalProcess("npm", ["pack", packagePath, "--pack-destination", context.tempDir, "--json"]);
2170
+ const packed = await runLocalProcess(npmCommand(), ["pack", packagePath, "--pack-destination", context.tempDir, "--json"]);
1645
2171
  if (packed.code !== 0) {
1646
2172
  return fail("opencode_plugin_pack_failed", "npm pack failed while preparing the OpenCode plugin source package.", {
1647
2173
  details: {
@@ -1800,14 +2326,465 @@ function openCodePluginTargetRoot(parsed, installingAll) {
1800
2326
  return resolve(target ?? defaultOpenCodeRoot());
1801
2327
  }
1802
2328
  function defaultCodexPluginRoot() {
1803
- return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "plugins");
2329
+ return join(defaultCodexHome(), "plugins");
2330
+ }
2331
+ function defaultCodexHome() {
2332
+ return resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
2333
+ }
2334
+ function codexConfigPath() {
2335
+ return join(defaultCodexHome(), "config.toml");
2336
+ }
2337
+ async function cleanupLegacyCodexPluginRemnants(targetRoot) {
2338
+ const removedPaths = [];
2339
+ const removedConfigTables = [];
2340
+ const warnings = [];
2341
+ const stoppedProcesses = await stopLegacyCodexPluginProcesses(warnings);
2342
+ const legacyPaths = [
2343
+ join(targetRoot, "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME)
2344
+ ];
2345
+ for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
2346
+ legacyPaths.push(join(targetRoot, "cache", marketplaceName, CODEX_PLUGIN_NAME));
2347
+ }
2348
+ for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
2349
+ legacyPaths.push(join(targetRoot, pluginName));
2350
+ legacyPaths.push(join(targetRoot, "cache", pluginName));
2351
+ for (const marketplaceName of [CODEX_MARKETPLACE_NAME, ...LEGACY_CODEX_MARKETPLACE_NAMES]) {
2352
+ legacyPaths.push(join(targetRoot, "cache", marketplaceName, pluginName));
2353
+ }
2354
+ }
2355
+ legacyPaths.push(...await discoverLegacyCodexCachePaths(targetRoot));
2356
+ if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
2357
+ 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"));
2358
+ legacyPaths.push(...legacyCodexLocalMarketplacePaths(), ...await discoverLegacyHomeAgentsMarketplacePaths(warnings), ...await discoverLegacyCodexProjectMarketplacePaths(warnings), ...legacyDevelopmentBoardRuntimePluginPaths());
2359
+ }
2360
+ for (const candidate of legacyPaths) {
2361
+ try {
2362
+ if (await pathExists(candidate)) {
2363
+ await rm(candidate, { recursive: true, force: true });
2364
+ removedPaths.push(candidate);
2365
+ }
2366
+ }
2367
+ catch (error) {
2368
+ warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2369
+ }
2370
+ }
2371
+ if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
2372
+ const configPath = codexConfigPath();
2373
+ try {
2374
+ if (await pathExists(configPath)) {
2375
+ const current = await readFile(configPath, "utf8");
2376
+ const updated = removeLegacyCodexConfigTables(current);
2377
+ if (updated.text !== current) {
2378
+ await writeFile(configPath, updated.text, "utf8");
2379
+ }
2380
+ removedConfigTables.push(...updated.removedTables);
2381
+ }
2382
+ }
2383
+ catch (error) {
2384
+ warnings.push(`Could not update ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
2385
+ }
2386
+ }
2387
+ const removedHistoryEntries = await cleanupLegacyCodexTextState(warnings);
2388
+ return {
2389
+ legacy_removed_paths: Array.from(new Set(removedPaths)),
2390
+ legacy_removed_config_tables: removedConfigTables,
2391
+ legacy_removed_history_entries: removedHistoryEntries,
2392
+ legacy_stopped_processes: stoppedProcesses,
2393
+ ...(warnings.length > 0 ? { warnings } : {})
2394
+ };
2395
+ }
2396
+ async function stopLegacyCodexPluginProcesses(warnings) {
2397
+ if (process.platform === "win32")
2398
+ return 0;
2399
+ try {
2400
+ const ps = await runLocalProcess("ps", ["-axo", "pid=,command="]);
2401
+ if (ps.code !== 0)
2402
+ return 0;
2403
+ let stopped = 0;
2404
+ for (const line of ps.stdout.split("\n")) {
2405
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
2406
+ if (!match)
2407
+ continue;
2408
+ const pid = Number(match[1]);
2409
+ const command = match[2] || "";
2410
+ if (!isLegacyCodexPluginProcess(command))
2411
+ continue;
2412
+ try {
2413
+ process.kill(pid, "SIGTERM");
2414
+ stopped += 1;
2415
+ }
2416
+ catch (error) {
2417
+ warnings.push(`Could not stop legacy Codex plugin process ${pid}: ${error instanceof Error ? error.message : String(error)}`);
2418
+ }
2419
+ }
2420
+ return stopped;
2421
+ }
2422
+ catch (error) {
2423
+ warnings.push(`Could not scan legacy Codex plugin processes: ${error instanceof Error ? error.message : String(error)}`);
2424
+ return 0;
2425
+ }
2426
+ }
2427
+ function isLegacyCodexPluginProcess(command) {
2428
+ const trimmed = command.trim();
2429
+ return /^\/.*\/dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
2430
+ || /^dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
2431
+ || legacyDevelopmentBoardRuntimeProcessPatterns().some((pattern) => pattern.test(trimmed));
2432
+ }
2433
+ function legacyDevelopmentBoardRuntimeProcessPatterns() {
2434
+ const home = escapeRegExp(homedir());
2435
+ return [
2436
+ new RegExp(`^${home}/Library/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
2437
+ new RegExp(`^${home}/Library/Application Support/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
2438
+ new RegExp(`^${home}/Library/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
2439
+ new RegExp(`^${home}/Library/Application Support/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
2440
+ new RegExp(`^${home}/.*?/DBT-Agent\\.app/Contents/MacOS/DBT-Agent(?:\\s|$)`)
2441
+ ];
2442
+ }
2443
+ function legacyDevelopmentBoardRuntimePluginPaths() {
2444
+ return [
2445
+ join(homedir(), "Library", "development-board-toolchain", "runtime", "editor_plugins"),
2446
+ join(homedir(), "Library", "development-board-toolchain", "runtime", "opencode_plugin"),
2447
+ join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "editor_plugins"),
2448
+ join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "opencode_plugin")
2449
+ ];
2450
+ }
2451
+ function legacyCodexLocalMarketplacePaths() {
2452
+ return Array.from(LEGACY_CODEX_MARKETPLACE_NAMES)
2453
+ .filter((name) => name !== CODEX_MARKETPLACE_NAME)
2454
+ .map((name) => join(defaultCodexHome(), "local-marketplaces", name));
2455
+ }
2456
+ async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
2457
+ const paths = [];
2458
+ const pluginRoot = join(homedir(), ".agents", "plugins");
2459
+ try {
2460
+ const entries = await readdir(pluginRoot, { withFileTypes: true });
2461
+ for (const entry of entries) {
2462
+ if (!entry.isFile() || !entry.name.startsWith("marketplace.json"))
2463
+ continue;
2464
+ const filePath = join(pluginRoot, entry.name);
2465
+ try {
2466
+ const current = await readFile(filePath, "utf8");
2467
+ if (isLegacyHomeAgentsMarketplace(current)) {
2468
+ paths.push(filePath);
2469
+ }
2470
+ }
2471
+ catch (error) {
2472
+ warnings.push(`Could not inspect legacy Codex home marketplace ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2473
+ }
2474
+ }
2475
+ }
2476
+ catch {
2477
+ return paths;
2478
+ }
2479
+ return paths;
2480
+ }
2481
+ function isLegacyHomeAgentsMarketplace(text) {
2482
+ return marketplaceTextHasLegacyCodexPlugin(text);
2483
+ }
2484
+ async function discoverLegacyCodexProjectMarketplacePaths(warnings) {
2485
+ const configPath = codexConfigPath();
2486
+ let text = "";
2487
+ try {
2488
+ text = await readFile(configPath, "utf8");
2489
+ }
2490
+ catch {
2491
+ return [];
2492
+ }
2493
+ const paths = new Set();
2494
+ for (const projectPath of legacyCodexProjectPathsFromConfig(text)) {
2495
+ for (const candidate of [
2496
+ join(projectPath, ".agents", "plugins", "marketplace.json"),
2497
+ join(projectPath, "platform_plugin", ".agents", "plugins", "marketplace.json"),
2498
+ join(projectPath, "platform_plugins", "codex_plugin", ".agents", "plugins", "marketplace.json")
2499
+ ]) {
2500
+ try {
2501
+ if (!await pathExists(candidate))
2502
+ continue;
2503
+ const current = await readFile(candidate, "utf8");
2504
+ if (marketplaceTextHasLegacyCodexPlugin(current)) {
2505
+ paths.add(candidate);
2506
+ }
2507
+ }
2508
+ catch (error) {
2509
+ warnings.push(`Could not inspect legacy Codex project marketplace ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2510
+ }
2511
+ }
2512
+ }
2513
+ return Array.from(paths);
2514
+ }
2515
+ function legacyCodexProjectPathsFromConfig(text) {
2516
+ const paths = [];
2517
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2518
+ for (const line of lines) {
2519
+ const table = parseTomlTableHeader(line);
2520
+ const match = table ? /^projects\."([^"]+)"$/.exec(table) : undefined;
2521
+ if (match?.[1] && /DBT-Agent-Project|development-board-toolchain|dbt-agent/i.test(match[1])) {
2522
+ paths.push(match[1].replace(/\\"/g, '"'));
2523
+ }
2524
+ }
2525
+ return paths;
2526
+ }
2527
+ function marketplaceTextHasLegacyCodexPlugin(text) {
2528
+ try {
2529
+ const parsed = JSON.parse(text);
2530
+ const marketplaceName = typeof parsed.name === "string" ? parsed.name : "";
2531
+ const marketplaceDisplayName = typeof parsed.interface?.displayName === "string" ? parsed.interface.displayName : "";
2532
+ const marketplaceLooksLegacy = legacyTextHasCodexPluginResidue(marketplaceName)
2533
+ || legacyTextHasCodexPluginResidue(marketplaceDisplayName)
2534
+ || LEGACY_CODEX_MARKETPLACE_NAMES.has(marketplaceName);
2535
+ return (parsed.plugins ?? []).some((plugin) => {
2536
+ const values = [
2537
+ plugin.name,
2538
+ plugin.category,
2539
+ plugin.source?.path,
2540
+ plugin.interface?.displayName
2541
+ ].filter((value) => typeof value === "string");
2542
+ return values.some(legacyTextHasCodexPluginResidue) || marketplaceLooksLegacy && values.some((value) => /embed-labs|dbt|development-board/i.test(value));
2543
+ });
2544
+ }
2545
+ catch {
2546
+ return legacyTextHasCodexPluginResidue(text);
2547
+ }
2548
+ }
2549
+ async function discoverLegacyCodexCachePaths(targetRoot) {
2550
+ const paths = [];
2551
+ const cacheRoot = join(targetRoot, "cache");
2552
+ try {
2553
+ const marketplaces = await readdir(cacheRoot, { withFileTypes: true });
2554
+ for (const entry of marketplaces) {
2555
+ if (!entry.isDirectory())
2556
+ continue;
2557
+ if (LEGACY_CODEX_MARKETPLACE_NAMES.has(entry.name)) {
2558
+ paths.push(join(cacheRoot, entry.name, CODEX_PLUGIN_NAME));
2559
+ }
2560
+ for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
2561
+ paths.push(join(cacheRoot, entry.name, pluginName));
2562
+ }
2563
+ }
2564
+ }
2565
+ catch {
2566
+ return paths;
2567
+ }
2568
+ return paths;
2569
+ }
2570
+ async function cleanupLegacyCodexTextState(warnings) {
2571
+ let removed = 0;
2572
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "history.jsonl"), warnings);
2573
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "session_index.jsonl"), warnings);
2574
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "rules", "default.rules"), warnings);
2575
+ return removed;
2576
+ }
2577
+ async function cleanupLegacyCodexTextFile(filePath, warnings) {
2578
+ try {
2579
+ if (!await pathExists(filePath))
2580
+ return 0;
2581
+ const current = await readFile(filePath, "utf8");
2582
+ const lines = current.split("\n");
2583
+ let removed = 0;
2584
+ const kept = lines.filter((line) => {
2585
+ if (!line)
2586
+ return true;
2587
+ if (isLegacyCodexHistoryMention(line)) {
2588
+ removed += 1;
2589
+ return false;
2590
+ }
2591
+ return true;
2592
+ });
2593
+ if (removed > 0) {
2594
+ await writeFile(filePath, kept.join("\n"), "utf8");
2595
+ }
2596
+ return removed;
2597
+ }
2598
+ catch (error) {
2599
+ warnings.push(`Could not clean Codex legacy text state ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2600
+ return 0;
2601
+ }
2602
+ }
2603
+ function isLegacyCodexHistoryMention(line) {
2604
+ return line.includes("plugin://dbt-agent@plugins")
2605
+ || line.includes("plugin://dbt-agent@embed-labs")
2606
+ || line.includes("development-board-toolchain-dev")
2607
+ || line.includes("development-board-toolchain")
2608
+ || /plugin:\/\/Dbt Agent@/i.test(line)
2609
+ || /plugin:\/\/deve@/i.test(line)
2610
+ || /dbt-agent/i.test(line);
2611
+ }
2612
+ function removeLegacyCodexConfigTables(text) {
2613
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2614
+ const output = [];
2615
+ const removedTables = [];
2616
+ let skipping = false;
2617
+ for (const line of lines) {
2618
+ const table = parseTomlTableHeader(line);
2619
+ if (table) {
2620
+ skipping = isLegacyCodexConfigTable(table);
2621
+ if (skipping) {
2622
+ removedTables.push(table);
2623
+ continue;
2624
+ }
2625
+ }
2626
+ if (!skipping) {
2627
+ output.push(line);
2628
+ }
2629
+ }
2630
+ return { text: output.join("").replace(/\n{3,}/g, "\n\n"), removedTables };
2631
+ }
2632
+ function parseTomlTableHeader(line) {
2633
+ const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line);
2634
+ return match?.[1]?.trim();
2635
+ }
2636
+ function isLegacyCodexConfigTable(table) {
2637
+ return /^plugins\."dbt-agent@[^"]+"$/.test(table)
2638
+ || /^plugins\."Dbt Agent@[^"]+"$/i.test(table)
2639
+ || /^plugins\."deve@[^"]+"$/i.test(table)
2640
+ || isLegacyEmbedLabsCodexMarketplaceConfigTable(table)
2641
+ || table === "mcp_servers.dbt-agent"
2642
+ || table.startsWith("mcp_servers.dbt-agent.")
2643
+ || table === 'mcp_servers."dbt-agent"'
2644
+ || table.startsWith('mcp_servers."dbt-agent".')
2645
+ || table === "mcp_servers.deve"
2646
+ || table.startsWith("mcp_servers.deve.")
2647
+ || /^projects\."[^"]*\/DBT-Agent-Project(?:\/[^"]*)?"$/.test(table);
2648
+ }
2649
+ function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
2650
+ for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
2651
+ if (table === `marketplaces.${marketplaceName}`) {
2652
+ return true;
2653
+ }
2654
+ if (table === `plugins."${CODEX_PLUGIN_NAME}@${marketplaceName}"`) {
2655
+ return true;
2656
+ }
2657
+ }
2658
+ return false;
2659
+ }
2660
+ function legacyTextHasCodexPluginResidue(value) {
2661
+ return /dbt-agent|Dbt Agent|development-board-toolchain|development-board-toolchain-dev/i.test(value)
2662
+ || value === "deve"
2663
+ || value.replace(/\\/g, "/").includes("/plugins/deve")
2664
+ || value.includes("plugin://deve@");
2665
+ }
2666
+ function legacyCodexCleanupWarning(cleanup) {
2667
+ const parts = [];
2668
+ if (cleanup.legacy_removed_paths.length > 0) {
2669
+ parts.push(`removed ${cleanup.legacy_removed_paths.length} stale/legacy Codex plugin path(s)`);
2670
+ }
2671
+ if (cleanup.legacy_removed_config_tables?.length) {
2672
+ parts.push(`removed ${cleanup.legacy_removed_config_tables.length} legacy Codex config table(s)`);
2673
+ }
2674
+ if (cleanup.legacy_removed_history_entries) {
2675
+ parts.push(`removed ${cleanup.legacy_removed_history_entries} legacy Codex text-state mention(s)`);
2676
+ }
2677
+ if (cleanup.legacy_stopped_processes) {
2678
+ parts.push(`stopped ${cleanup.legacy_stopped_processes} legacy Codex plugin process(es)`);
2679
+ }
2680
+ if (cleanup.warnings?.length) {
2681
+ parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
2682
+ }
2683
+ return parts.length > 0 ? `Codex plugin cleanup: ${parts.join(", ")}.` : undefined;
2684
+ }
2685
+ async function cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall) {
2686
+ const removedPaths = [];
2687
+ const warnings = [];
2688
+ const legacyPaths = [
2689
+ join(targetRoot, "plugins", "development-board-toolchain.js"),
2690
+ join(targetRoot, "plugins", "development-board-toolchain-dev.js"),
2691
+ join(targetRoot, "plugins", "dbt-agent.js"),
2692
+ join(targetRoot, "plugins", "Dbt Agent.js"),
2693
+ join(targetRoot, "plugins", "deve.js"),
2694
+ join(targetRoot, "plugins", "deve"),
2695
+ join(targetRoot, "node_modules", "development-board-toolchain"),
2696
+ join(targetRoot, "node_modules", "development-board-toolchain-dev"),
2697
+ join(targetRoot, "node_modules", "dbt-agent")
2698
+ ];
2699
+ if (globalInstall) {
2700
+ legacyPaths.push(join(targetRoot, "plugins", "embed-labs.js"));
2701
+ legacyPaths.push(...await discoverLegacyOpenCodeBackupPaths(targetRoot, warnings));
2702
+ legacyPaths.push(...await discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings));
2703
+ }
2704
+ for (const candidate of legacyPaths) {
2705
+ try {
2706
+ if (await pathExists(candidate)) {
2707
+ await rm(candidate, { recursive: true, force: true });
2708
+ removedPaths.push(candidate);
2709
+ }
2710
+ }
2711
+ catch (error) {
2712
+ warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2713
+ }
2714
+ }
2715
+ return {
2716
+ legacy_removed_paths: removedPaths,
2717
+ legacy_removed_config_entries: [],
2718
+ ...(warnings.length > 0 ? { warnings } : {})
2719
+ };
2720
+ }
2721
+ async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
2722
+ const paths = [];
2723
+ try {
2724
+ const entries = await readdir(targetRoot, { withFileTypes: true });
2725
+ for (const entry of entries) {
2726
+ if (!entry.isFile() || !entry.name.includes(".bak"))
2727
+ continue;
2728
+ const filePath = join(targetRoot, entry.name);
2729
+ try {
2730
+ const current = await readFile(filePath, "utf8");
2731
+ if (legacyTextHasCodexPluginResidue(current)) {
2732
+ paths.push(filePath);
2733
+ }
2734
+ }
2735
+ catch (error) {
2736
+ warnings.push(`Could not inspect legacy OpenCode backup ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2737
+ }
2738
+ }
2739
+ }
2740
+ catch {
2741
+ return paths;
2742
+ }
2743
+ return paths;
2744
+ }
2745
+ async function discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings) {
2746
+ const paths = [];
2747
+ const cacheRoot = join(targetRoot, ".embed-labs", "plugin-cache");
2748
+ try {
2749
+ const entries = await readdir(cacheRoot, { withFileTypes: true });
2750
+ for (const entry of entries) {
2751
+ if (!entry.isFile())
2752
+ continue;
2753
+ if (/^(embed-labs|embed-labs-opencode-plugin)-\d+\.\d+\.\d+\.tgz$/.test(entry.name)) {
2754
+ paths.push(join(cacheRoot, entry.name));
2755
+ }
2756
+ }
2757
+ }
2758
+ catch (error) {
2759
+ if (error.code !== "ENOENT") {
2760
+ warnings.push(`Could not inspect OpenCode plugin cache ${cacheRoot}: ${error instanceof Error ? error.message : String(error)}`);
2761
+ }
2762
+ }
2763
+ return paths;
2764
+ }
2765
+ function legacyOpenCodeCleanupWarning(cleanup) {
2766
+ const parts = [];
2767
+ if (cleanup.legacy_removed_paths.length > 0) {
2768
+ parts.push(`removed ${cleanup.legacy_removed_paths.length} legacy OpenCode plugin path(s)`);
2769
+ }
2770
+ if (cleanup.legacy_removed_config_entries?.length) {
2771
+ parts.push(`removed ${cleanup.legacy_removed_config_entries.length} legacy OpenCode config entry(s)`);
2772
+ }
2773
+ if (cleanup.warnings?.length) {
2774
+ parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
2775
+ }
2776
+ return parts.length > 0 ? `Legacy OpenCode cleanup: ${parts.join(", ")}.` : undefined;
2777
+ }
2778
+ function combineWarnings(...warnings) {
2779
+ const actual = warnings.filter((warning) => Boolean(warning));
2780
+ return actual.length > 0 ? actual.join(" ") : undefined;
1804
2781
  }
1805
2782
  async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
1806
2783
  const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
1807
2784
  if (explicitTarget && process.env.EMBED_CODEX_MCP_REGISTER !== "1") {
1808
2785
  return {
1809
2786
  registered: false,
1810
- hint: `Installed into a custom target. Register manually with: codex mcp add embed-labs -- node ${join(targetPath, "scripts", "embed-labs-mcp-bridge.mjs")}`
2787
+ hint: "Installed into a custom target. Register manually with: codex mcp add embed-labs -- embedlabs mcp start"
1811
2788
  };
1812
2789
  }
1813
2790
  const bridgePath = join(targetPath, "scripts", "embed-labs-mcp-bridge.mjs");
@@ -1826,11 +2803,12 @@ async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
1826
2803
  };
1827
2804
  }
1828
2805
  const embedCliBin = process.env.EMBED_CLI_BIN?.trim() || await resolveExecutableOnPath("embedlabs") || await resolveExecutableOnPath("embed") || "";
2806
+ const mcpLauncher = await resolveEmbedCliMcpLauncher(embedCliBin);
1829
2807
  const authFile = resolve(process.env.EMBED_AUTH_FILE?.trim() || DEFAULT_AUTH_FILE);
1830
2808
  const cloudUrl = pluginMcpCloudApiUrl(parsed);
1831
2809
  const existing = await runLocalProcess(codexBin, ["mcp", "get", "embed-labs", "--json"]);
1832
- if (existing.code === 0 && codexMcpAlreadyRegistered(existing.stdout, bridgePath, cloudUrl, authFile, embedCliBin)) {
1833
- const warning = await upsertCodexMcpRuntimeConfig(bridgePath);
2810
+ if (existing.code === 0 && codexMcpAlreadyRegistered(existing.stdout, bridgePath, cloudUrl, authFile, embedCliBin, mcpLauncher.command, mcpLauncher.args)) {
2811
+ const warning = await upsertCodexMcpRuntimeConfig(mcpLauncher.command, mcpLauncher.args);
1834
2812
  if (warning) {
1835
2813
  return { registered: true, warning };
1836
2814
  }
@@ -1844,31 +2822,152 @@ async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
1844
2822
  "--env",
1845
2823
  `EMBED_CLOUD_API_URL=${cloudUrl}`,
1846
2824
  "--env",
1847
- `EMBED_AUTH_FILE=${authFile}`
2825
+ `EMBED_AUTH_FILE=${authFile}`,
2826
+ "--env",
2827
+ `EMBED_MCP_BRIDGE_PATH=${bridgePath}`
1848
2828
  ];
1849
2829
  if (embedCliBin) {
1850
2830
  args.push("--env", `EMBED_CLI_BIN=${embedCliBin}`);
1851
2831
  }
1852
- args.push("--", process.execPath, bridgePath);
2832
+ args.push("--", mcpLauncher.command, ...mcpLauncher.args);
1853
2833
  const addResult = await runLocalProcess(codexBin, args);
1854
2834
  if (addResult.code !== 0) {
1855
2835
  return {
1856
2836
  registered: false,
1857
- hint: `Codex plugin installed. Register manually with: codex mcp add embed-labs -- ${process.execPath} ${bridgePath}`,
2837
+ hint: "Codex plugin installed. Register manually with: codex mcp add embed-labs -- embedlabs mcp start",
1858
2838
  warning: `codex mcp add failed: ${addResult.stderr.trim() || addResult.stdout.trim() || `exit ${addResult.code}`}`
1859
2839
  };
1860
2840
  }
1861
- const warning = await upsertCodexMcpRuntimeConfig(bridgePath);
2841
+ const warning = await upsertCodexMcpRuntimeConfig(mcpLauncher.command, mcpLauncher.args);
1862
2842
  return warning ? { registered: true, warning } : { registered: true };
1863
2843
  }
1864
- function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embedCliBin) {
2844
+ async function maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath) {
2845
+ const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
2846
+ if (explicitTarget && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
2847
+ return {
2848
+ registered: false,
2849
+ warning: "Codex plugin marketplace entry was not registered because a custom target was used. Set EMBED_CODEX_MARKETPLACE_REGISTER=1 to register it anyway."
2850
+ };
2851
+ }
2852
+ if (resolve(targetRoot) !== resolve(defaultCodexPluginRoot()) && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
2853
+ return {
2854
+ registered: false,
2855
+ warning: "Codex plugin marketplace entry was not registered because the install target is not the default Codex plugin root."
2856
+ };
2857
+ }
2858
+ const marketplacePath = defaultCodexLocalMarketplaceRoot();
2859
+ const marketplacePluginPath = join(marketplacePath, "plugins", CODEX_PLUGIN_NAME);
2860
+ try {
2861
+ if (!await pathExists(join(targetPath, ".codex-plugin", "plugin.json"))) {
2862
+ return {
2863
+ registered: false,
2864
+ warning: `Codex plugin manifest was not found at ${join(targetPath, ".codex-plugin", "plugin.json")}.`
2865
+ };
2866
+ }
2867
+ await rm(marketplacePluginPath, { recursive: true, force: true });
2868
+ await mkdir(dirname(marketplacePluginPath), { recursive: true });
2869
+ await cp(targetPath, marketplacePluginPath, { recursive: true });
2870
+ await refreshCodexPluginCache(targetPath);
2871
+ await writeCodexLocalMarketplaceManifest(marketplacePath);
2872
+ const warning = await upsertCodexPluginMarketplaceConfig(marketplacePath);
2873
+ return warning ? { registered: true, marketplacePath, warning } : { registered: true, marketplacePath };
2874
+ }
2875
+ catch (error) {
2876
+ return {
2877
+ registered: false,
2878
+ marketplacePath,
2879
+ warning: `Could not register Codex plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`
2880
+ };
2881
+ }
2882
+ }
2883
+ function defaultCodexLocalMarketplaceRoot() {
2884
+ return join(defaultCodexHome(), "local-marketplaces", CODEX_PLUGIN_NAME);
2885
+ }
2886
+ async function refreshCodexPluginCache(targetPath) {
2887
+ const manifestPath = join(targetPath, ".codex-plugin", "plugin.json");
2888
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
2889
+ const version = typeof manifest.version === "string" && manifest.version.trim()
2890
+ ? manifest.version.trim()
2891
+ : "local";
2892
+ const cachePluginRoot = join(defaultCodexPluginRoot(), "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME);
2893
+ const cacheVersionPath = join(cachePluginRoot, version);
2894
+ await rm(cachePluginRoot, { recursive: true, force: true });
2895
+ await mkdir(dirname(cacheVersionPath), { recursive: true });
2896
+ await cp(targetPath, cacheVersionPath, { recursive: true });
2897
+ }
2898
+ async function writeCodexLocalMarketplaceManifest(marketplacePath) {
2899
+ const manifestPath = join(marketplacePath, ".agents", "plugins", "marketplace.json");
2900
+ const manifest = {
2901
+ name: CODEX_MARKETPLACE_NAME,
2902
+ interface: {
2903
+ displayName: "Embed Labs"
2904
+ },
2905
+ plugins: [
2906
+ {
2907
+ name: CODEX_PLUGIN_NAME,
2908
+ source: {
2909
+ source: "local",
2910
+ path: `./plugins/${CODEX_PLUGIN_NAME}`
2911
+ },
2912
+ policy: {
2913
+ installation: "AVAILABLE",
2914
+ authentication: "ON_USE"
2915
+ },
2916
+ category: "Developer Tools"
2917
+ }
2918
+ ]
2919
+ };
2920
+ await mkdir(dirname(manifestPath), { recursive: true });
2921
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
2922
+ }
2923
+ async function upsertCodexPluginMarketplaceConfig(marketplacePath) {
2924
+ const configPath = codexConfigPath();
2925
+ try {
2926
+ await mkdir(dirname(configPath), { recursive: true });
2927
+ let text = "";
2928
+ try {
2929
+ text = await readFile(configPath, "utf8");
2930
+ }
2931
+ catch {
2932
+ text = "";
2933
+ }
2934
+ const original = text;
2935
+ const cleaned = removeLegacyCodexConfigTables(text).text;
2936
+ let updated = upsertTomlTableKeys(cleaned, `marketplaces.${CODEX_MARKETPLACE_NAME}`, {
2937
+ source_type: tomlString("local"),
2938
+ source: tomlString(marketplacePath),
2939
+ last_updated: tomlString(new Date().toISOString().replace(/\.\d{3}Z$/, "Z"))
2940
+ });
2941
+ updated = upsertTomlTableKeys(updated, `plugins."${CODEX_PLUGIN_NAME}@${CODEX_MARKETPLACE_NAME}"`, {
2942
+ enabled: "true"
2943
+ });
2944
+ if (updated !== original) {
2945
+ await writeFile(configPath, updated, "utf8");
2946
+ }
2947
+ return undefined;
2948
+ }
2949
+ catch (error) {
2950
+ return `${configPath} could not be updated with the Embed Labs plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`;
2951
+ }
2952
+ }
2953
+ async function resolveEmbedCliMcpLauncher(embedCliBin) {
2954
+ const explicit = embedCliBin?.trim();
2955
+ if (explicit) {
2956
+ return { command: explicit, args: ["mcp", "start"] };
2957
+ }
2958
+ const pathBin = await resolveExecutableOnPath("embedlabs") || await resolveExecutableOnPath("embed");
2959
+ if (pathBin) {
2960
+ return { command: pathBin, args: ["mcp", "start"] };
2961
+ }
2962
+ return { command: process.execPath, args: [join(CLI_MODULE_DIR, "index.js"), "mcp", "start"] };
2963
+ }
2964
+ function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embedCliBin, mcpCommand, mcpArgs) {
1865
2965
  try {
1866
2966
  const parsed = JSON.parse(stdout);
1867
2967
  const transport = parsed.transport;
1868
- if (transport?.type !== "stdio" || transport.command !== process.execPath)
1869
- return false;
1870
- if (!transport.args?.includes(bridgePath))
2968
+ if (transport?.type !== "stdio")
1871
2969
  return false;
2970
+ const args = transport.args || [];
1872
2971
  const env = transport.env || {};
1873
2972
  if (env.EMBED_CLOUD_API_URL !== cloudUrl)
1874
2973
  return false;
@@ -1876,13 +2975,16 @@ function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embed
1876
2975
  return false;
1877
2976
  if (embedCliBin && env.EMBED_CLI_BIN !== embedCliBin)
1878
2977
  return false;
1879
- return true;
2978
+ const directBridge = transport.command === process.execPath && args.includes(bridgePath);
2979
+ const cliMcp = transport.command === mcpCommand
2980
+ && mcpArgs.every((arg, index) => args[index] === arg);
2981
+ return directBridge || cliMcp;
1880
2982
  }
1881
2983
  catch {
1882
2984
  return false;
1883
2985
  }
1884
2986
  }
1885
- async function upsertCodexMcpRuntimeConfig(bridgePath) {
2987
+ async function upsertCodexMcpRuntimeConfig(command, args) {
1886
2988
  const configPath = join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "config.toml");
1887
2989
  try {
1888
2990
  await mkdir(dirname(configPath), { recursive: true });
@@ -1894,8 +2996,8 @@ async function upsertCodexMcpRuntimeConfig(bridgePath) {
1894
2996
  text = "";
1895
2997
  }
1896
2998
  const updated = upsertTomlTableKeys(text, "mcp_servers.embed-labs", {
1897
- command: tomlString(process.execPath),
1898
- args: `[${tomlString(bridgePath)}]`,
2999
+ command: tomlString(command),
3000
+ args: `[${args.map(tomlString).join(", ")}]`,
1899
3001
  startup_timeout_sec: "120"
1900
3002
  });
1901
3003
  if (updated !== text) {
@@ -1972,11 +3074,229 @@ async function resolveExecutableOnPath(name) {
1972
3074
  }
1973
3075
  return undefined;
1974
3076
  }
3077
+ async function runBridgeStart(parsed) {
3078
+ const host = stringFlag(parsed, "host");
3079
+ const port = numberFlag(parsed, "port");
3080
+ const bridge = await resolveBridgeLauncher();
3081
+ const args = [...bridge.args];
3082
+ if (host) {
3083
+ args.push("--host", host);
3084
+ }
3085
+ if (port !== undefined) {
3086
+ args.push("--port", String(port));
3087
+ }
3088
+ const env = {
3089
+ ...process.env,
3090
+ ...(host ? { EMBED_BRIDGE_HOST: host } : {}),
3091
+ ...(port !== undefined ? { EMBED_BRIDGE_PORT: String(port) } : {})
3092
+ };
3093
+ const child = spawn(bridge.command, args, {
3094
+ stdio: "inherit",
3095
+ env
3096
+ });
3097
+ const forwardSignal = (signal) => {
3098
+ if (!child.killed) {
3099
+ child.kill(signal);
3100
+ }
3101
+ };
3102
+ process.once("SIGINT", forwardSignal);
3103
+ process.once("SIGTERM", forwardSignal);
3104
+ return await new Promise((resolveCode) => {
3105
+ child.on("error", (error) => {
3106
+ process.off("SIGINT", forwardSignal);
3107
+ process.off("SIGTERM", forwardSignal);
3108
+ console.error(error instanceof Error ? error.message : String(error));
3109
+ resolveCode(1);
3110
+ });
3111
+ child.on("close", (code, signal) => {
3112
+ process.off("SIGINT", forwardSignal);
3113
+ process.off("SIGTERM", forwardSignal);
3114
+ if (signal === "SIGINT" || signal === "SIGTERM") {
3115
+ resolveCode(0);
3116
+ }
3117
+ else {
3118
+ resolveCode(code ?? 0);
3119
+ }
3120
+ });
3121
+ });
3122
+ }
3123
+ async function runMcpStart(parsed) {
3124
+ const unknownFlag = firstUnknownFlag(parsed, ["bridge-path"]);
3125
+ const unexpected = parsed.command.slice(2);
3126
+ if (unknownFlag || unexpected.length > 0) {
3127
+ console.error(unknownFlag ? `Unknown flag --${unknownFlag}. ${MCP_START_USAGE}` : MCP_START_USAGE);
3128
+ return 2;
3129
+ }
3130
+ const bridge = await resolveMcpBridgeLauncher(stringFlag(parsed, "bridge-path"));
3131
+ if (!bridge) {
3132
+ console.error([
3133
+ "Embed Labs MCP bridge was not found.",
3134
+ "Run npm run build in the source checkout, reinstall the embedlabs npm package, or set EMBED_MCP_BRIDGE_PATH to the bridge script.",
3135
+ "Expected command shape for MCP clients: embedlabs mcp start"
3136
+ ].join("\n"));
3137
+ return 1;
3138
+ }
3139
+ const child = spawn(bridge.command, bridge.args, {
3140
+ stdio: "inherit",
3141
+ env: {
3142
+ ...process.env,
3143
+ EMBED_CLIENT_KIND: process.env.EMBED_CLIENT_KIND || "mcp_client",
3144
+ EMBED_MCP_BRIDGE_PATH: bridge.bridgePath
3145
+ }
3146
+ });
3147
+ const forwardSignal = (signal) => {
3148
+ if (!child.killed) {
3149
+ child.kill(signal);
3150
+ }
3151
+ };
3152
+ process.once("SIGINT", forwardSignal);
3153
+ process.once("SIGTERM", forwardSignal);
3154
+ return await new Promise((resolveCode) => {
3155
+ child.on("error", (error) => {
3156
+ process.off("SIGINT", forwardSignal);
3157
+ process.off("SIGTERM", forwardSignal);
3158
+ console.error(error instanceof Error ? error.message : String(error));
3159
+ resolveCode(1);
3160
+ });
3161
+ child.on("close", (code, signal) => {
3162
+ process.off("SIGINT", forwardSignal);
3163
+ process.off("SIGTERM", forwardSignal);
3164
+ if (signal === "SIGINT" || signal === "SIGTERM") {
3165
+ resolveCode(0);
3166
+ }
3167
+ else {
3168
+ resolveCode(code ?? 0);
3169
+ }
3170
+ });
3171
+ });
3172
+ }
3173
+ async function resolveMcpBridgeLauncher(overridePath) {
3174
+ const explicitPath = overridePath?.trim() || process.env.EMBED_MCP_BRIDGE_PATH?.trim();
3175
+ const candidates = [
3176
+ explicitPath ? resolve(explicitPath) : "",
3177
+ join(CLI_MODULE_DIR, "embed-labs-mcp-bridge.mjs"),
3178
+ sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", "scripts", "embed-labs-mcp-bridge.mjs"),
3179
+ join(defaultCodexPluginRoot(), CODEX_PLUGIN_NAME, "scripts", "embed-labs-mcp-bridge.mjs")
3180
+ ].filter(Boolean);
3181
+ for (const candidate of candidates) {
3182
+ try {
3183
+ await access(candidate, constants.R_OK);
3184
+ return { command: process.execPath, args: [candidate], bridgePath: candidate };
3185
+ }
3186
+ catch {
3187
+ // Keep looking.
3188
+ }
3189
+ }
3190
+ return undefined;
3191
+ }
3192
+ async function resolveBridgeLauncher() {
3193
+ const explicitBinary = process.env.EMBED_LOCAL_BRIDGE_BINARY?.trim();
3194
+ if (explicitBinary) {
3195
+ try {
3196
+ await access(explicitBinary, constants.X_OK);
3197
+ return { command: explicitBinary, args: [] };
3198
+ }
3199
+ catch {
3200
+ // Fall through so the package launcher can print its clearer repair message.
3201
+ }
3202
+ }
3203
+ const pathBinary = await resolveExecutableOnPath(process.platform === "win32" ? "embed-local-bridge.cmd" : "embed-local-bridge");
3204
+ if (pathBinary) {
3205
+ return { command: pathBinary, args: [] };
3206
+ }
3207
+ const packageLauncher = await resolveBridgePackageLauncher();
3208
+ if (packageLauncher) {
3209
+ return { command: process.execPath, args: [packageLauncher] };
3210
+ }
3211
+ return {
3212
+ command: process.execPath,
3213
+ args: [resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js")]
3214
+ };
3215
+ }
3216
+ async function resolveBridgePackageLauncher() {
3217
+ const candidates = [];
3218
+ try {
3219
+ const packageJson = require.resolve("@embed-labs/local-bridge/package.json");
3220
+ candidates.push(join(dirname(packageJson), "dist", "index.js"));
3221
+ }
3222
+ catch {
3223
+ // Source checkout fallback below.
3224
+ }
3225
+ candidates.push(resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js"));
3226
+ for (const candidate of candidates) {
3227
+ try {
3228
+ await access(candidate, constants.R_OK);
3229
+ return candidate;
3230
+ }
3231
+ catch {
3232
+ // Keep looking.
3233
+ }
3234
+ }
3235
+ return undefined;
3236
+ }
1975
3237
  function defaultOpenCodeRoot() {
1976
- return join(process.cwd(), ".opencode");
3238
+ return globalOpenCodeRoot();
3239
+ }
3240
+ function globalOpenCodeRoot() {
3241
+ return join(homedir(), ".config", "opencode");
3242
+ }
3243
+ function isGlobalOpenCodeRoot(targetRoot) {
3244
+ return resolve(targetRoot) === resolve(globalOpenCodeRoot());
3245
+ }
3246
+ async function ensureOpenCodeGlobalPluginConfig() {
3247
+ const configPath = join(globalOpenCodeRoot(), "opencode.json");
3248
+ let existing = {};
3249
+ try {
3250
+ const parsed = JSON.parse(await readFile(configPath, "utf8"));
3251
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3252
+ existing = parsed;
3253
+ }
3254
+ }
3255
+ catch {
3256
+ existing = {};
3257
+ }
3258
+ const configured = Array.isArray(existing.plugin)
3259
+ ? existing.plugin.filter((item) => typeof item === "string")
3260
+ : Array.isArray(existing.plugins)
3261
+ ? existing.plugins.filter((item) => typeof item === "string")
3262
+ : [];
3263
+ const removed = configured.filter(isLegacyOpenCodePluginConfigEntry);
3264
+ const cleaned = configured.filter((item) => !isLegacyOpenCodePluginConfigEntry(item));
3265
+ if (!cleaned.includes("embed-labs")) {
3266
+ cleaned.push("embed-labs");
3267
+ }
3268
+ await writeFile(configPath, `${JSON.stringify({
3269
+ ...existing,
3270
+ plugin: cleaned,
3271
+ plugins: undefined
3272
+ }, null, 2)}\n`, "utf8");
3273
+ return removed;
3274
+ }
3275
+ function isLegacyOpenCodePluginConfigEntry(item) {
3276
+ const normalized = item.trim().replace(/\\/g, "/");
3277
+ const pathOnly = normalized.split(/[?#]/, 1)[0] || normalized;
3278
+ return normalized === "dbt-agent"
3279
+ || normalized === "Dbt Agent"
3280
+ || normalized === "deve"
3281
+ || normalized === "development-board-toolchain"
3282
+ || normalized === "development-board-toolchain-dev"
3283
+ || normalized === "./plugins/deve"
3284
+ || normalized === "./plugins/deve.js"
3285
+ || /(?:^|\/)plugins\/deve(?:\.js)?$/.test(pathOnly)
3286
+ || /(?:^|\/)plugins\/dbt-agent(?:\.js)?$/.test(pathOnly)
3287
+ || normalized === "./plugins/development-board-toolchain"
3288
+ || normalized === "./plugins/development-board-toolchain.js"
3289
+ || normalized === "./plugins/development-board-toolchain-dev"
3290
+ || normalized === "./plugins/development-board-toolchain-dev.js"
3291
+ || pathOnly.endsWith("/plugins/development-board-toolchain")
3292
+ || pathOnly.endsWith("/plugins/development-board-toolchain.js")
3293
+ || pathOnly.endsWith("/plugins/development-board-toolchain-dev")
3294
+ || pathOnly.endsWith("/plugins/development-board-toolchain-dev.js")
3295
+ || normalized.includes("dbt-agent")
3296
+ || normalized.includes("development-board-toolchain");
1977
3297
  }
1978
3298
  async function openCodeDuplicatePluginWarning(targetRoot) {
1979
- const globalRoot = join(homedir(), ".config", "opencode");
3299
+ const globalRoot = globalOpenCodeRoot();
1980
3300
  if (resolve(targetRoot) === resolve(globalRoot))
1981
3301
  return undefined;
1982
3302
  const configPath = join(globalRoot, "opencode.json");
@@ -2008,6 +3328,21 @@ async function localPluginVersion(kind) {
2008
3328
  return undefined;
2009
3329
  }
2010
3330
  }
3331
+ async function installedCodexPluginVersion(pluginPath) {
3332
+ return await readPackageVersion(join(pluginPath, ".codex-plugin", "plugin.json"));
3333
+ }
3334
+ async function installedOpenCodePluginVersion(targetRoot) {
3335
+ return await readPackageVersion(join(targetRoot, "node_modules", "embed-labs", "package.json"));
3336
+ }
3337
+ async function readPackageVersion(filePath) {
3338
+ try {
3339
+ const parsed = JSON.parse(await readFile(filePath, "utf8"));
3340
+ return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : undefined;
3341
+ }
3342
+ catch {
3343
+ return undefined;
3344
+ }
3345
+ }
2011
3346
  async function localPluginSourcesAvailable() {
2012
3347
  return await pathExists(sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", ".codex-plugin", "plugin.json"))
2013
3348
  && await pathExists(sourceCheckoutPath("platform_plugins", "opencode_plugin", "package.json"));
@@ -2026,7 +3361,8 @@ async function pathExists(pathValue) {
2026
3361
  }
2027
3362
  async function runLocalProcess(command, args) {
2028
3363
  return await new Promise((resolveProcess) => {
2029
- const child = spawn(command, args, {
3364
+ const launcher = localProcessLauncher(command, args);
3365
+ const child = spawn(launcher.command, launcher.args, {
2030
3366
  cwd: process.cwd(),
2031
3367
  env: process.env,
2032
3368
  stdio: ["ignore", "pipe", "pipe"]
@@ -2049,6 +3385,50 @@ async function runLocalProcess(command, args) {
2049
3385
  });
2050
3386
  });
2051
3387
  }
3388
+ function npmCommand() {
3389
+ return platform() === "win32" ? "npm.cmd" : "npm";
3390
+ }
3391
+ function localProcessLauncher(command, args) {
3392
+ if (platform() !== "win32" || !/\.(?:cmd|bat)$/i.test(command)) {
3393
+ return { command, args };
3394
+ }
3395
+ return {
3396
+ command: process.env.ComSpec || "cmd.exe",
3397
+ args: ["/d", "/s", "/c", windowsCommandLine([command, ...args])]
3398
+ };
3399
+ }
3400
+ function windowsCommandLine(args) {
3401
+ return args.map(windowsQuoteArg).join(" ");
3402
+ }
3403
+ function windowsQuoteArg(arg) {
3404
+ if (arg.length > 0 && !/[\s"]/u.test(arg)) {
3405
+ return arg;
3406
+ }
3407
+ let quoted = "\"";
3408
+ let backslashes = 0;
3409
+ for (const char of arg) {
3410
+ if (char === "\\") {
3411
+ backslashes += 1;
3412
+ continue;
3413
+ }
3414
+ if (char === "\"") {
3415
+ quoted += "\\".repeat((backslashes * 2) + 1);
3416
+ quoted += "\"";
3417
+ backslashes = 0;
3418
+ continue;
3419
+ }
3420
+ if (backslashes > 0) {
3421
+ quoted += "\\".repeat(backslashes);
3422
+ backslashes = 0;
3423
+ }
3424
+ quoted += char;
3425
+ }
3426
+ if (backslashes > 0) {
3427
+ quoted += "\\".repeat(backslashes * 2);
3428
+ }
3429
+ quoted += "\"";
3430
+ return quoted;
3431
+ }
2052
3432
  async function parseErrorResponse(response) {
2053
3433
  const text = await response.text();
2054
3434
  if (!text.trim()) {
@@ -2066,19 +3446,65 @@ async function parseErrorResponse(response) {
2066
3446
  return undefined;
2067
3447
  }
2068
3448
  async function cloudAuthToken() {
3449
+ return (await cloudAuthConfig()).token;
3450
+ }
3451
+ async function cloudAuthConfig() {
2069
3452
  const envToken = process.env.EMBED_API_TOKEN?.trim();
2070
3453
  if (envToken) {
2071
- return envToken;
3454
+ const fileConfig = await readLocalAuthFile();
3455
+ return {
3456
+ ...fileConfig,
3457
+ token: envToken,
3458
+ profile: process.env.EMBED_AUTH_PROFILE ?? fileConfig.profile ?? "default",
3459
+ source: "env"
3460
+ };
2072
3461
  }
3462
+ const fileConfig = await readLocalAuthFile();
3463
+ return {
3464
+ ...fileConfig,
3465
+ token: fileConfig.token?.trim() || undefined,
3466
+ profile: fileConfig.profile ?? "default",
3467
+ source: fileConfig.token ? "file" : undefined
3468
+ };
3469
+ }
3470
+ async function readLocalAuthFile() {
2073
3471
  try {
2074
3472
  const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
2075
- const fileToken = typeof parsed.token === "string" ? parsed.token.trim() : "";
2076
- return fileToken || undefined;
3473
+ return normalizeLocalAuthFile(parsed);
2077
3474
  }
2078
3475
  catch {
2079
- return undefined;
3476
+ return {};
2080
3477
  }
2081
3478
  }
3479
+ function normalizeLocalAuthFile(parsed) {
3480
+ const device = isJsonObject(parsed.device) ? parsed.device : undefined;
3481
+ const normalizedDevice = device && typeof device.device_id === "string" && typeof device.fingerprint_hash === "string" && typeof device.private_key_pem === "string"
3482
+ ? {
3483
+ device_id: device.device_id,
3484
+ fingerprint_hash: device.fingerprint_hash,
3485
+ private_key_pem: device.private_key_pem,
3486
+ public_key_pem: typeof device.public_key_pem === "string" ? device.public_key_pem : undefined,
3487
+ label: typeof device.label === "string" ? device.label : undefined,
3488
+ platform: typeof device.platform === "string" ? device.platform : undefined,
3489
+ arch: typeof device.arch === "string" ? device.arch : undefined,
3490
+ hostname_hash: typeof device.hostname_hash === "string" ? device.hostname_hash : undefined,
3491
+ registered_at: typeof device.registered_at === "string" ? device.registered_at : undefined
3492
+ }
3493
+ : undefined;
3494
+ return {
3495
+ profile: typeof parsed.profile === "string" ? parsed.profile : undefined,
3496
+ token: typeof parsed.token === "string" ? parsed.token.trim() : undefined,
3497
+ updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined,
3498
+ account_id: typeof parsed.account_id === "string" ? parsed.account_id : undefined,
3499
+ api_key_id: typeof parsed.api_key_id === "string" ? parsed.api_key_id : undefined,
3500
+ device: normalizedDevice
3501
+ };
3502
+ }
3503
+ async function writeLocalAuthFile(config) {
3504
+ await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
3505
+ await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify(config, null, 2)}\n`, "utf8");
3506
+ await chmod(DEFAULT_AUTH_FILE, 0o600).catch(() => undefined);
3507
+ }
2082
3508
  function serviceBaseUrl(url) {
2083
3509
  return url.replace(/\/+$/, "");
2084
3510
  }
@@ -2101,9 +3527,16 @@ async function naturalLanguageQuery(parsed) {
2101
3527
  }
2102
3528
  const text = parsed.command.slice(1).join(" ").trim();
2103
3529
  if (!text) {
2104
- return { response: fail("invalid_args", QUERY_USAGE, { remediation: "Try: embed query \"查一下我的额度\"." }) };
3530
+ return { response: fail("invalid_args", QUERY_USAGE, { remediation: "Try: embed query \"当前支持哪些开发板\"." }) };
2105
3531
  }
2106
3532
  const normalized = text.toLowerCase();
3533
+ if (isQuotaOrBillingIntent(normalized)) {
3534
+ return {
3535
+ response: fail("quota_not_supported", "当前 Embed Labs 服务器只提供 MCP 服务,不提供额度、充值、余额、账本或存储配额功能。", {
3536
+ remediation: "请使用 embedlabs auth status 查看登录和设备绑定状态,或在 MCP 客户端中直接调用开发板、工具链、知识库和本地硬件相关工具。"
3537
+ })
3538
+ };
3539
+ }
2107
3540
  const accountId = await queryAccountId(parsed);
2108
3541
  const needsAccount = queryNeedsAccount(normalized);
2109
3542
  if (needsAccount && !accountId.ok) {
@@ -2122,7 +3555,7 @@ async function naturalLanguageQuery(parsed) {
2122
3555
  if (!amountUsd) {
2123
3556
  return {
2124
3557
  response: fail("amount_required", "Recharge requests need an amount.", {
2125
- remediation: "Try: embed query \"用 USDC 链上充值 100 美元\"."
3558
+ remediation: "当前 MCP 服务不提供充值流程。"
2126
3559
  })
2127
3560
  };
2128
3561
  }
@@ -2215,13 +3648,8 @@ async function naturalLanguageQuery(parsed) {
2215
3648
  response: fail("query_intent_unknown", "I could not map that request to a stable CLI action yet.", {
2216
3649
  remediation: [
2217
3650
  "Supported examples:",
2218
- "embed query \"查一下我的额度\"",
2219
- "embed query \"用 USDC 链上充值 100 美元\"",
2220
- "embed query \"看一下 token 用量\"",
2221
- "embed query \"看一下磁盘空间\"",
2222
3651
  "embed query \"列出我的 API key\"",
2223
3652
  "embed query \"有哪些开发板模板\"",
2224
- "embed query \"当前可以用哪些模型\"",
2225
3653
  "embed query \"本地有哪些工具能力\""
2226
3654
  ].join("\n")
2227
3655
  })
@@ -2434,6 +3862,10 @@ function agentRunToolInputs(parsed) {
2434
3862
  if (remotePath.error) {
2435
3863
  return remotePath.error;
2436
3864
  }
3865
+ const runCommand = optionalTrimmedStringFlag(parsed, "run-command");
3866
+ if (runCommand.error) {
3867
+ return runCommand.error;
3868
+ }
2437
3869
  const user = optionalTrimmedStringFlag(parsed, "user");
2438
3870
  if (user.error) {
2439
3871
  return user.error;
@@ -2487,6 +3919,7 @@ function agentRunToolInputs(parsed) {
2487
3919
  user: user.value,
2488
3920
  artifact_path: artifact.value,
2489
3921
  remote_path: remotePath.value,
3922
+ run_command: runCommand.value,
2490
3923
  run: booleanFlag(parsed, "run") || undefined,
2491
3924
  timeout_seconds: timeout.value
2492
3925
  });
@@ -2567,13 +4000,15 @@ async function queryAccountId(parsed) {
2567
4000
  return { ok: true, value: auth.data.account_id };
2568
4001
  }
2569
4002
  function queryNeedsAccount(normalized) {
4003
+ return isApiKeyIntent(normalized);
4004
+ }
4005
+ function isQuotaOrBillingIntent(normalized) {
2570
4006
  return isRechargeIntent(normalized)
2571
4007
  || isBalanceIntent(normalized)
2572
4008
  || isUsageIntent(normalized)
2573
4009
  || isBillingStatementIntent(normalized)
2574
4010
  || isStorageSettlementIntent(normalized)
2575
- || isStorageIntent(normalized)
2576
- || isApiKeyIntent(normalized);
4011
+ || isStorageIntent(normalized);
2577
4012
  }
2578
4013
  function isRechargeIntent(normalized) {
2579
4014
  return /(充值|充钱|续费|购买额度|买额度|top\s*up|recharge|add credits|buy credits)/i.test(normalized);
@@ -3422,6 +4857,47 @@ function boardKnowledgeFileRequest(parsed) {
3422
4857
  outputPath: outputPath.value
3423
4858
  };
3424
4859
  }
4860
+ function boardKnowledgeSearchRequest(parsed) {
4861
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "query", "source", "limit"]);
4862
+ if (unknownFlag) {
4863
+ return `Unknown flag --${unknownFlag}. ${BOARD_KNOWLEDGE_SEARCH_USAGE}`;
4864
+ }
4865
+ const templateId = parsed.command[3]?.trim();
4866
+ if (!templateId) {
4867
+ return BOARD_KNOWLEDGE_SEARCH_USAGE;
4868
+ }
4869
+ const extra = parsed.command.slice(4);
4870
+ if (extra.length > 0) {
4871
+ return `Unexpected argument: ${extra[0]}. ${BOARD_KNOWLEDGE_SEARCH_USAGE}`;
4872
+ }
4873
+ const query = optionalTrimmedStringFlag(parsed, "query");
4874
+ if (query.error) {
4875
+ return query.error;
4876
+ }
4877
+ if (!query.value) {
4878
+ return BOARD_KNOWLEDGE_SEARCH_USAGE;
4879
+ }
4880
+ const source = optionalTrimmedStringFlag(parsed, "source");
4881
+ if (source.error) {
4882
+ return source.error;
4883
+ }
4884
+ if (source.value && !["board_pack", "build_template", "registry"].includes(source.value)) {
4885
+ return BOARD_KNOWLEDGE_SEARCH_USAGE;
4886
+ }
4887
+ const limit = optionalPositiveIntegerFlag(parsed, "limit");
4888
+ if (limit.error) {
4889
+ return limit.error;
4890
+ }
4891
+ if (limit.value !== undefined && limit.value > 10) {
4892
+ return "--limit must be between 1 and 10.";
4893
+ }
4894
+ return {
4895
+ templateId,
4896
+ query: query.value,
4897
+ source: source.value,
4898
+ limit: limit.value ?? 5
4899
+ };
4900
+ }
3425
4901
  function toolCallRequest(parsed) {
3426
4902
  const unknownFlag = firstUnknownFlag(parsed, ["json", "input-json", "approve"]);
3427
4903
  if (unknownFlag) {
@@ -3469,7 +4945,7 @@ function isTaishanPiDeployCommand(parsed) {
3469
4945
  || area === "deploy";
3470
4946
  }
3471
4947
  function boardDeployTaishanPiRequest(parsed) {
3472
- const unknownFlag = firstUnknownFlag(parsed, ["json", "host", "user", "artifact", "artifact-path", "remote-path", "run", "timeout", "approve", "approved"]);
4948
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "host", "user", "artifact", "artifact-path", "remote-path", "run-command", "run", "timeout", "approve", "approved"]);
3473
4949
  if (unknownFlag) {
3474
4950
  return `Unknown flag --${unknownFlag}. ${BOARD_DEPLOY_TAISHANPI_USAGE}`;
3475
4951
  }
@@ -3511,6 +4987,10 @@ function boardDeployTaishanPiRequest(parsed) {
3511
4987
  if (remotePath.error) {
3512
4988
  return remotePath.error;
3513
4989
  }
4990
+ const runCommand = optionalTrimmedStringFlag(parsed, "run-command");
4991
+ if (runCommand.error) {
4992
+ return runCommand.error;
4993
+ }
3514
4994
  const timeout = optionalNonNegativeIntegerFlag(parsed, "timeout");
3515
4995
  if (timeout.error) {
3516
4996
  return timeout.error;
@@ -3525,6 +5005,7 @@ function boardDeployTaishanPiRequest(parsed) {
3525
5005
  user: user.value,
3526
5006
  artifact_path: artifact.value,
3527
5007
  remote_path: remotePath.value,
5008
+ run_command: runCommand.value,
3528
5009
  run: booleanFlag(parsed, "run") || undefined,
3529
5010
  timeout_seconds: timeout.value,
3530
5011
  approved: approval.value || undefined
@@ -3537,30 +5018,272 @@ async function authLogin(parsed) {
3537
5018
  return fail("invalid_args", "Usage: embed auth login --token <token> [--profile default] [--json]");
3538
5019
  }
3539
5020
  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 });
5021
+ const current = await readLocalAuthFile();
5022
+ const localDevice = await buildLocalDeviceAuth(parsed, current.device);
5023
+ const registration = await registerLocalDevice(token.trim(), localDevice.registration);
5024
+ if (!registration.ok) {
5025
+ return fail(registration.error.code, registration.error.message, {
5026
+ remediation: registration.error.remediation,
5027
+ details: registration.error.details
5028
+ });
5029
+ }
5030
+ const device = {
5031
+ device_id: registration.data.device.device_id,
5032
+ fingerprint_hash: localDevice.device.fingerprint_hash,
5033
+ private_key_pem: localDevice.device.private_key_pem,
5034
+ public_key_pem: localDevice.device.public_key_pem,
5035
+ label: registration.data.device.label ?? localDevice.device.label,
5036
+ platform: registration.data.device.platform ?? localDevice.device.platform,
5037
+ arch: registration.data.device.arch ?? localDevice.device.arch,
5038
+ hostname_hash: registration.data.device.hostname_hash ?? localDevice.device.hostname_hash,
5039
+ registered_at: registration.data.device.first_seen_at
5040
+ };
5041
+ await writeLocalAuthFile({
5042
+ profile,
5043
+ token: token.trim(),
5044
+ updated_at: updatedAt,
5045
+ account_id: registration.data.device.account_id,
5046
+ api_key_id: registration.data.device.api_key_id,
5047
+ device
5048
+ });
5049
+ return ok({
5050
+ authenticated: true,
5051
+ profile,
5052
+ source: "file",
5053
+ updated_at: updatedAt,
5054
+ account_id: registration.data.device.account_id,
5055
+ api_key_id: registration.data.device.api_key_id,
5056
+ device_id: device.device_id,
5057
+ device_fingerprint_hash: device.fingerprint_hash,
5058
+ device_label: device.label,
5059
+ device_registered_at: device.registered_at,
5060
+ device_private_key_configured: true
5061
+ });
3543
5062
  }
3544
5063
  async function authStatus() {
3545
- if (process.env.EMBED_API_TOKEN?.trim()) {
5064
+ const envToken = process.env.EMBED_API_TOKEN?.trim();
5065
+ const file = await readLocalAuthFile();
5066
+ const deviceIntegrity = file.device
5067
+ ? (await validateLocalDeviceIntegrity(file.device)).ok ? "ok" : "failed"
5068
+ : "unbound";
5069
+ if (envToken) {
3546
5070
  return {
3547
5071
  authenticated: true,
3548
5072
  profile: process.env.EMBED_AUTH_PROFILE ?? "default",
3549
- source: "env"
5073
+ source: "env",
5074
+ account_id: file.account_id,
5075
+ api_key_id: file.api_key_id,
5076
+ device_id: file.device?.device_id,
5077
+ device_fingerprint_hash: file.device?.fingerprint_hash,
5078
+ device_label: file.device?.label,
5079
+ device_registered_at: file.device?.registered_at,
5080
+ device_private_key_configured: Boolean(file.device?.private_key_pem),
5081
+ device_integrity: deviceIntegrity
3550
5082
  };
3551
5083
  }
5084
+ return {
5085
+ authenticated: Boolean(file.token?.trim()),
5086
+ profile: file.profile ?? "default",
5087
+ source: file.token ? "file" : undefined,
5088
+ updated_at: file.updated_at,
5089
+ account_id: file.account_id,
5090
+ api_key_id: file.api_key_id,
5091
+ device_id: file.device?.device_id,
5092
+ device_fingerprint_hash: file.device?.fingerprint_hash,
5093
+ device_label: file.device?.label,
5094
+ device_registered_at: file.device?.registered_at,
5095
+ device_private_key_configured: Boolean(file.device?.private_key_pem),
5096
+ device_integrity: deviceIntegrity
5097
+ };
5098
+ }
5099
+ async function authDeviceStatus(parsed) {
5100
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
5101
+ const unexpected = parsed.command.slice(3);
5102
+ if (unknownFlag || unexpected.length > 0) {
5103
+ return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_STATUS_USAGE}` : AUTH_DEVICE_STATUS_USAGE);
5104
+ }
5105
+ const local = await authStatus();
5106
+ const remote = local.authenticated ? await cloudGet("/v1/me/devices") : undefined;
5107
+ if (remote && !remote.ok) {
5108
+ return ok({ local });
5109
+ }
5110
+ return ok({ local, remote: remote?.data });
5111
+ }
5112
+ async function authDeviceList(parsed) {
5113
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
5114
+ const unexpected = parsed.command.slice(3);
5115
+ if (unknownFlag || unexpected.length > 0) {
5116
+ return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_LIST_USAGE}` : AUTH_DEVICE_LIST_USAGE);
5117
+ }
5118
+ return await cloudGet("/v1/me/devices");
5119
+ }
5120
+ async function authDeviceRevoke(parsed) {
5121
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
5122
+ if (unknownFlag) {
5123
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_REVOKE_USAGE}`);
5124
+ }
5125
+ const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_REVOKE_USAGE);
5126
+ if (!id.ok) {
5127
+ return fail("invalid_args", id.error);
5128
+ }
5129
+ return await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}/revoke`, {});
5130
+ }
5131
+ async function authDeviceRename(parsed) {
5132
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "label"]);
5133
+ if (unknownFlag) {
5134
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_RENAME_USAGE}`);
5135
+ }
5136
+ const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_RENAME_USAGE);
5137
+ if (!id.ok) {
5138
+ return fail("invalid_args", id.error);
5139
+ }
5140
+ const label = stringFlag(parsed, "label");
5141
+ if (!label?.trim()) {
5142
+ return fail("invalid_args", AUTH_DEVICE_RENAME_USAGE);
5143
+ }
5144
+ const updated = await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}`, { label: label.trim() });
5145
+ if (updated.ok) {
5146
+ const auth = await readLocalAuthFile();
5147
+ if (auth.device?.device_id === updated.data.device_id) {
5148
+ await writeLocalAuthFile({ ...auth, device: { ...auth.device, label: updated.data.label } });
5149
+ }
5150
+ }
5151
+ return updated;
5152
+ }
5153
+ async function buildLocalDeviceAuth(parsed, existing) {
5154
+ const fingerprint = await localHardwareFingerprint();
5155
+ const keyPair = existing?.fingerprint_hash === fingerprint.fingerprint_hash && existing.private_key_pem && existing.public_key_pem
5156
+ ? { privateKeyPem: existing.private_key_pem, publicKeyPem: existing.public_key_pem }
5157
+ : generateLocalDeviceKeyPair();
5158
+ const label = stringFlag(parsed, "label")?.trim()
5159
+ || existing?.label
5160
+ || `${fingerprint.platform} ${fingerprint.arch}`;
5161
+ const device = {
5162
+ device_id: existing?.fingerprint_hash === fingerprint.fingerprint_hash ? existing.device_id : "",
5163
+ fingerprint_hash: fingerprint.fingerprint_hash,
5164
+ private_key_pem: keyPair.privateKeyPem,
5165
+ public_key_pem: keyPair.publicKeyPem,
5166
+ label,
5167
+ platform: fingerprint.platform,
5168
+ arch: fingerprint.arch,
5169
+ hostname_hash: fingerprint.hostname_hash,
5170
+ registered_at: existing?.registered_at
5171
+ };
5172
+ return {
5173
+ device,
5174
+ registration: {
5175
+ fingerprint_hash: fingerprint.fingerprint_hash,
5176
+ public_key: keyPair.publicKeyPem,
5177
+ label,
5178
+ platform: fingerprint.platform,
5179
+ arch: fingerprint.arch,
5180
+ hostname_hash: fingerprint.hostname_hash,
5181
+ client_name: EMBED_CLIENT_NAME,
5182
+ client_version: EMBED_CLIENT_VERSION,
5183
+ metadata: {
5184
+ fingerprint_version: "v1",
5185
+ fingerprint_source: fingerprint.source
5186
+ }
5187
+ }
5188
+ };
5189
+ }
5190
+ function generateLocalDeviceKeyPair() {
5191
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
5192
+ return {
5193
+ privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
5194
+ publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString()
5195
+ };
5196
+ }
5197
+ async function registerLocalDevice(token, body) {
3552
5198
  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
5199
+ const bodyText = JSON.stringify(body);
5200
+ const headers = {
5201
+ "content-type": "application/json",
5202
+ authorization: `Bearer ${token}`
3559
5203
  };
5204
+ addCloudRequestSignature(headers, "POST", "/v1/me/devices/register", bodyText, token);
5205
+ const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/me/devices/register`, {
5206
+ method: "POST",
5207
+ headers,
5208
+ body: bodyText
5209
+ });
5210
+ const parsed = await response.json();
5211
+ return enrichCloudAuthFailure(parsed, true);
5212
+ }
5213
+ catch (error) {
5214
+ return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
5215
+ remediation: `Check that embed cloud-api is running at ${DEFAULT_CLOUD_API_URL}. Start it with: npm run cloud-api`
5216
+ });
5217
+ }
5218
+ }
5219
+ async function localHardwareFingerprint() {
5220
+ cachedLocalHardwareFingerprint ??= localHardwareFingerprintUncached();
5221
+ return await cachedLocalHardwareFingerprint;
5222
+ }
5223
+ async function localHardwareFingerprintUncached() {
5224
+ const platformName = platform();
5225
+ const archName = arch();
5226
+ const raw = await localHardwareId(platformName);
5227
+ const fingerprintHash = createHash("sha256")
5228
+ .update(`embed-labs:device:v1:${platformName}:${raw.value}`)
5229
+ .digest("hex");
5230
+ const hostnameHash = createHash("sha256")
5231
+ .update(`embed-labs:hostname:v1:${hostname()}`)
5232
+ .digest("hex");
5233
+ return {
5234
+ fingerprint_hash: fingerprintHash,
5235
+ platform: platformName,
5236
+ arch: archName,
5237
+ hostname_hash: hostnameHash,
5238
+ source: raw.source
5239
+ };
5240
+ }
5241
+ async function localHardwareId(platformName) {
5242
+ if (platformName === "darwin") {
5243
+ const result = await runLocalProcess("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
5244
+ const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(result.stdout);
5245
+ if (match?.[1]) {
5246
+ return { value: match[1], source: "macos_ioplatformuuid" };
5247
+ }
5248
+ }
5249
+ if (platformName === "win32") {
5250
+ const result = await runLocalProcess("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
5251
+ const match = /MachineGuid\s+REG_\w+\s+([^\r\n]+)/.exec(result.stdout);
5252
+ if (match?.[1]?.trim()) {
5253
+ return { value: match[1].trim(), source: "windows_machineguid" };
5254
+ }
5255
+ }
5256
+ if (platformName === "linux") {
5257
+ for (const pathValue of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
5258
+ try {
5259
+ const value = (await readFile(pathValue, "utf8")).trim();
5260
+ if (value) {
5261
+ return { value, source: `linux:${pathValue}` };
5262
+ }
5263
+ }
5264
+ catch {
5265
+ // Try the next stable machine id location.
5266
+ }
5267
+ }
5268
+ }
5269
+ const generated = await localGeneratedInstallId();
5270
+ return { value: generated, source: "generated_install_id" };
5271
+ }
5272
+ async function localGeneratedInstallId() {
5273
+ try {
5274
+ const parsed = JSON.parse(await readFile(DEFAULT_DEVICE_FILE, "utf8"));
5275
+ if (typeof parsed.generated_install_id === "string" && parsed.generated_install_id.trim()) {
5276
+ return parsed.generated_install_id.trim();
5277
+ }
3560
5278
  }
3561
5279
  catch {
3562
- return { authenticated: false, profile: "default" };
5280
+ // Fall through and create a local-only fallback id.
3563
5281
  }
5282
+ const generated = `install_${randomBytes(24).toString("base64url")}`;
5283
+ await mkdir(dirname(DEFAULT_DEVICE_FILE), { recursive: true });
5284
+ await writeFile(DEFAULT_DEVICE_FILE, `${JSON.stringify({ generated_install_id: generated, created_at: new Date().toISOString() }, null, 2)}\n`, "utf8");
5285
+ await chmod(DEFAULT_DEVICE_FILE, 0o600).catch(() => undefined);
5286
+ return generated;
3564
5287
  }
3565
5288
  function accountCreateBody(parsed) {
3566
5289
  const unknownFlag = firstUnknownFlag(parsed, ["json", "email", "display-name"]);
@@ -3716,6 +5439,80 @@ function usageRecordBody(parsed) {
3716
5439
  created_at: createdAtResult.value
3717
5440
  });
3718
5441
  }
5442
+ function mcpToolEventBody(parsed) {
5443
+ const unknownFlag = firstUnknownFlag(parsed, [
5444
+ "json",
5445
+ "account",
5446
+ "account-id",
5447
+ "tool",
5448
+ "client",
5449
+ "mode",
5450
+ "server-model-used",
5451
+ "success",
5452
+ "local-device-id",
5453
+ "local_device_id",
5454
+ "request-id",
5455
+ "duration-ms",
5456
+ "input-summary",
5457
+ "output-summary"
5458
+ ]);
5459
+ if (unknownFlag) {
5460
+ return `Unknown flag --${unknownFlag}. ${MCP_TOOL_EVENT_USAGE}`;
5461
+ }
5462
+ const extra = parsed.command.slice(2);
5463
+ if (extra.length > 0) {
5464
+ return `Unexpected argument: ${extra[0]}. ${MCP_TOOL_EVENT_USAGE}`;
5465
+ }
5466
+ const toolResult = optionalTrimmedStringFlag(parsed, "tool");
5467
+ if (toolResult.error)
5468
+ return toolResult.error;
5469
+ if (!toolResult.value)
5470
+ return MCP_TOOL_EVENT_USAGE;
5471
+ const accountResult = optionalTrimmedStringAliasFlag(parsed, ["account", "account-id"], "account or account-id");
5472
+ if (accountResult.error)
5473
+ return accountResult.error;
5474
+ const clientResult = optionalTrimmedStringFlag(parsed, "client");
5475
+ if (clientResult.error)
5476
+ return clientResult.error;
5477
+ const modeResult = optionalTrimmedStringFlag(parsed, "mode");
5478
+ if (modeResult.error)
5479
+ return modeResult.error;
5480
+ const localDeviceResult = optionalTrimmedStringAliasFlag(parsed, ["local-device-id", "local_device_id"], "local-device-id");
5481
+ if (localDeviceResult.error)
5482
+ return localDeviceResult.error;
5483
+ const requestIdResult = optionalTrimmedStringFlag(parsed, "request-id");
5484
+ if (requestIdResult.error)
5485
+ return requestIdResult.error;
5486
+ const inputSummaryResult = optionalTrimmedStringFlag(parsed, "input-summary");
5487
+ if (inputSummaryResult.error)
5488
+ return inputSummaryResult.error;
5489
+ const outputSummaryResult = optionalTrimmedStringFlag(parsed, "output-summary");
5490
+ if (outputSummaryResult.error)
5491
+ return outputSummaryResult.error;
5492
+ const durationResult = optionalNonNegativeIntegerFlag(parsed, "duration-ms");
5493
+ if (durationResult.error)
5494
+ return durationResult.error;
5495
+ const serverModelUsed = optionalBooleanFlag(parsed, "server-model-used");
5496
+ if (typeof serverModelUsed === "string")
5497
+ return serverModelUsed;
5498
+ const success = optionalBooleanFlag(parsed, "success");
5499
+ if (typeof success === "string")
5500
+ return success;
5501
+ return compactBody({
5502
+ account_id: accountResult.value,
5503
+ tool_name: toolResult.value,
5504
+ client: clientResult.value,
5505
+ mode: modeResult.value,
5506
+ local_device_id: localDeviceResult.value,
5507
+ server_model_used: serverModelUsed,
5508
+ success,
5509
+ request_id: requestIdResult.value,
5510
+ duration_ms: durationResult.value,
5511
+ input_summary: inputSummaryResult.value,
5512
+ output_summary: outputSummaryResult.value,
5513
+ metadata: localDeviceResult.value ? { local_device_id: localDeviceResult.value } : undefined
5514
+ });
5515
+ }
3719
5516
  function usageSummaryRequest(parsed) {
3720
5517
  const unknownFlag = firstUnknownFlag(parsed, ["json", "account", "account-id", "api-key-id", "from", "to"]);
3721
5518
  if (unknownFlag) {
@@ -4051,6 +5848,29 @@ function billingSnapshotListRequest(parsed) {
4051
5848
  }
4052
5849
  return { path: `/v1/accounts/${encodeURIComponent(accountResult.value)}/billing/snapshots` };
4053
5850
  }
5851
+ function localToolchainListRequest(parsed, usage = LOCAL_TOOLCHAIN_LIST_USAGE) {
5852
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "install-root"]);
5853
+ if (unknownFlag) {
5854
+ return `Unknown flag --${unknownFlag}. ${usage}`;
5855
+ }
5856
+ const extra = parsed.command.slice(3);
5857
+ if (extra.length > 0) {
5858
+ return `Unexpected argument: ${extra[0]}. ${usage}`;
5859
+ }
5860
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
5861
+ if (board.error)
5862
+ return board.error;
5863
+ const channel = optionalTrimmedStringFlag(parsed, "channel");
5864
+ if (channel.error)
5865
+ return channel.error;
5866
+ const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
5867
+ if (metadataRoot.error)
5868
+ return metadataRoot.error;
5869
+ const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
5870
+ if (installRoot.error)
5871
+ return installRoot.error;
5872
+ return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value, installRoot: installRoot.value };
5873
+ }
4054
5874
  function localToolchainLatestRequest(parsed) {
4055
5875
  const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root"]);
4056
5876
  if (unknownFlag) {
@@ -4071,6 +5891,32 @@ function localToolchainLatestRequest(parsed) {
4071
5891
  return metadataRoot.error;
4072
5892
  return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value };
4073
5893
  }
5894
+ function localWslInstallRequest(parsed) {
5895
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "distribution", "distro", "no-launch", "web-download", "timeout-ms"]);
5896
+ if (unknownFlag) {
5897
+ return `Unknown flag --${unknownFlag}. ${LOCAL_WSL_INSTALL_USAGE}`;
5898
+ }
5899
+ const extra = parsed.command.slice(3);
5900
+ if (extra.length > 0) {
5901
+ return `Unexpected argument: ${extra[0]}. ${LOCAL_WSL_INSTALL_USAGE}`;
5902
+ }
5903
+ const distribution = stringFlag(parsed, "distribution") ?? stringFlag(parsed, "distro");
5904
+ const noLaunch = optionalBooleanFlag(parsed, "no-launch");
5905
+ if (typeof noLaunch === "string")
5906
+ return noLaunch;
5907
+ const webDownload = optionalBooleanFlag(parsed, "web-download");
5908
+ if (typeof webDownload === "string")
5909
+ return webDownload;
5910
+ const timeoutMs = optionalIntegerFlag(parsed, "timeout-ms", 1_000, 3_600_000);
5911
+ if (timeoutMs.error)
5912
+ return timeoutMs.error;
5913
+ return {
5914
+ distribution,
5915
+ noLaunch,
5916
+ webDownload,
5917
+ timeoutMs: timeoutMs.value
5918
+ };
5919
+ }
4074
5920
  function localToolchainCurrentRequest(parsed) {
4075
5921
  const unknownFlag = firstUnknownFlag(parsed, ["json", "install-root"]);
4076
5922
  if (unknownFlag) {
@@ -4086,7 +5932,7 @@ function localToolchainCurrentRequest(parsed) {
4086
5932
  return { installRoot: installRoot.value };
4087
5933
  }
4088
5934
  function localToolchainInstallRequest(parsed) {
4089
- const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "force"]);
5935
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "mode", "force"]);
4090
5936
  if (unknownFlag) {
4091
5937
  return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_INSTALL_USAGE}`;
4092
5938
  }
@@ -4115,6 +5961,9 @@ function localToolchainInstallRequest(parsed) {
4115
5961
  const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
4116
5962
  if (installRoot.error)
4117
5963
  return installRoot.error;
5964
+ const mode = optionalTrimmedStringFlag(parsed, "mode");
5965
+ if (mode.error)
5966
+ return mode.error;
4118
5967
  return {
4119
5968
  boardId: board.value,
4120
5969
  channel: channel.value,
@@ -4122,11 +5971,32 @@ function localToolchainInstallRequest(parsed) {
4122
5971
  sourceUrl: sourceUrl.value,
4123
5972
  sourceReleaseRoot: sourceReleaseRoot.value,
4124
5973
  installRoot: installRoot.value,
5974
+ mode: mode.value,
4125
5975
  force: booleanFlag(parsed, "force")
4126
5976
  };
4127
5977
  }
5978
+ function localToolchainUninstallRequest(parsed) {
5979
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "install-root", "yes", "force"]);
5980
+ if (unknownFlag) {
5981
+ return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_UNINSTALL_USAGE}`;
5982
+ }
5983
+ const extra = parsed.command.slice(3);
5984
+ if (extra.length > 0) {
5985
+ return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_UNINSTALL_USAGE}`;
5986
+ }
5987
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
5988
+ if (board.error)
5989
+ return board.error;
5990
+ if (!board.value) {
5991
+ return LOCAL_TOOLCHAIN_UNINSTALL_USAGE;
5992
+ }
5993
+ const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
5994
+ if (installRoot.error)
5995
+ return installRoot.error;
5996
+ return { boardId: board.value, installRoot: installRoot.value };
5997
+ }
4128
5998
  function localToolchainValidateRequest(parsed) {
4129
- const unknownFlag = firstUnknownFlag(parsed, ["json", "release-root"]);
5999
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "release-root", "mode"]);
4130
6000
  if (unknownFlag) {
4131
6001
  return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_VALIDATE_USAGE}`;
4132
6002
  }
@@ -4138,7 +6008,15 @@ function localToolchainValidateRequest(parsed) {
4138
6008
  if (releaseRoot.error) {
4139
6009
  return releaseRoot.error;
4140
6010
  }
4141
- return { releaseRoot: releaseRoot.value };
6011
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
6012
+ if (board.error) {
6013
+ return board.error;
6014
+ }
6015
+ const mode = optionalTrimmedStringFlag(parsed, "mode");
6016
+ if (mode.error) {
6017
+ return mode.error;
6018
+ }
6019
+ return { releaseRoot: releaseRoot.value, mode: mode.value, boardId: board.value };
4142
6020
  }
4143
6021
  function localCompileTaishanPiRequest(parsed, auth) {
4144
6022
  const unknownFlag = firstUnknownFlag(parsed, ["json", "source", "output", "release-root", "account", "account-id"]);
@@ -4223,7 +6101,9 @@ function localToolchainAuthContext(auth, accountId) {
4223
6101
  authenticated: auth.authenticated,
4224
6102
  profile: auth.profile,
4225
6103
  source: auth.source,
4226
- account_id: accountId
6104
+ account_id: accountId,
6105
+ api_key_id: auth.api_key_id,
6106
+ device_id: auth.device_id
4227
6107
  };
4228
6108
  }
4229
6109
  function usageEventsRequest(parsed) {
@@ -4633,6 +6513,35 @@ function renderPluginList(result) {
4633
6513
  `install="${plugin.install_command}"`
4634
6514
  ].filter(Boolean).join(" ")).join("\n");
4635
6515
  }
6516
+ function renderPluginUpdateCheck(result) {
6517
+ const lines = [
6518
+ `release_url=${result.release_url}`,
6519
+ result.latest_version ? `latest_version=${result.latest_version}` : ""
6520
+ ].filter(Boolean);
6521
+ if (result.release_notes.length > 0) {
6522
+ lines.push("release_notes:");
6523
+ for (const note of result.release_notes) {
6524
+ lines.push(` - ${note}`);
6525
+ }
6526
+ }
6527
+ for (const plugin of result.plugins) {
6528
+ lines.push("");
6529
+ lines.push(`${plugin.display_name} (${plugin.id})`);
6530
+ lines.push(` installed=${plugin.installed}`);
6531
+ lines.push(` installed_version=${plugin.installed_version ?? "none"}`);
6532
+ lines.push(` latest_version=${plugin.latest_version ?? "unknown"}`);
6533
+ lines.push(` update_available=${plugin.update_available}`);
6534
+ lines.push(` target=${plugin.target_path}`);
6535
+ lines.push(` update_command=${plugin.update_command}`);
6536
+ if (plugin.release_file) {
6537
+ lines.push(` release_file=${plugin.release_file}`);
6538
+ }
6539
+ for (const note of plugin.notes) {
6540
+ lines.push(` note=${note}`);
6541
+ }
6542
+ }
6543
+ return lines.join("\n");
6544
+ }
4636
6545
  function renderPluginInstall(result) {
4637
6546
  const lines = ["Installed plugins:"];
4638
6547
  for (const item of result.installed) {
@@ -4654,6 +6563,15 @@ function renderPluginInstall(result) {
4654
6563
  if (item.mcp_warning) {
4655
6564
  lines.push(` warning=${item.mcp_warning}`);
4656
6565
  }
6566
+ if (item.marketplace_registered !== undefined) {
6567
+ lines.push(` codex_marketplace_registered=${item.marketplace_registered}`);
6568
+ }
6569
+ if (item.marketplace_path) {
6570
+ lines.push(` codex_marketplace=${item.marketplace_path}`);
6571
+ }
6572
+ if (item.marketplace_warning) {
6573
+ lines.push(` warning=${item.marketplace_warning}`);
6574
+ }
4657
6575
  }
4658
6576
  return lines.join("\n");
4659
6577
  }
@@ -4685,9 +6603,53 @@ function renderAgentRunResult(result) {
4685
6603
  return lines.join("\n");
4686
6604
  }
4687
6605
  function renderAuthStatus(status) {
4688
- return status.authenticated
4689
- ? `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`
4690
- : `Not authenticated profile=${status.profile}`;
6606
+ if (!status.authenticated) {
6607
+ return `Not authenticated profile=${status.profile}`;
6608
+ }
6609
+ return [
6610
+ `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`,
6611
+ status.account_id ? `account=${status.account_id}` : "",
6612
+ status.api_key_id ? `api_key=${status.api_key_id}` : "",
6613
+ status.device_id ? `device=${status.device_id}` : "device=not_registered",
6614
+ status.device_label ? `device_label=${status.device_label}` : "",
6615
+ status.device_integrity ? `device_integrity=${status.device_integrity}` : "",
6616
+ status.device_private_key_configured === false ? "device_private_key=missing" : ""
6617
+ ].filter(Boolean).join("\n");
6618
+ }
6619
+ function renderAuthDeviceStatus(status) {
6620
+ const lines = [renderAuthStatus(status.local)];
6621
+ if (status.remote) {
6622
+ const activeCount = status.remote.devices.filter((device) => device.status === "active").length;
6623
+ lines.push(`remote_devices=${activeCount}/${status.remote.device_limit}`);
6624
+ const localDevice = status.local.device_id
6625
+ ? status.remote.devices.find((device) => device.device_id === status.local.device_id)
6626
+ : undefined;
6627
+ if (localDevice) {
6628
+ lines.push(`remote_current=${renderAuthDevice(localDevice)}`);
6629
+ }
6630
+ }
6631
+ return lines.join("\n");
6632
+ }
6633
+ function renderAuthDeviceList(result) {
6634
+ if (result.devices.length === 0) {
6635
+ return `No registered devices. device_limit=${result.device_limit}`;
6636
+ }
6637
+ return [
6638
+ `device_limit=${result.device_limit}`,
6639
+ ...result.devices.map(renderAuthDevice)
6640
+ ].join("\n");
6641
+ }
6642
+ function renderAuthDevice(device) {
6643
+ return [
6644
+ `${device.device_id} account=${device.account_id}`,
6645
+ device.api_key_id ? `api_key=${device.api_key_id}` : "",
6646
+ `status=${device.status}`,
6647
+ device.label ? `label=${device.label}` : "",
6648
+ device.platform ? `platform=${device.platform}` : "",
6649
+ device.arch ? `arch=${device.arch}` : "",
6650
+ `last_seen_at=${device.last_seen_at}`,
6651
+ device.revoked_at ? `revoked_at=${device.revoked_at}` : ""
6652
+ ].filter(Boolean).join(" ");
4691
6653
  }
4692
6654
  function renderAccount(account) {
4693
6655
  return [
@@ -4737,6 +6699,20 @@ function renderUsageRecord(record) {
4737
6699
  `created_at=${record.created_at}`
4738
6700
  ].filter(Boolean).join(" ");
4739
6701
  }
6702
+ function renderMcpToolEvent(event) {
6703
+ return [
6704
+ `${event.event_id} tool=${event.tool_name}`,
6705
+ event.account_id ? `account=${event.account_id}` : "",
6706
+ event.api_key_id ? `api_key=${event.api_key_id}` : "",
6707
+ `client=${event.client}`,
6708
+ `mode=${event.mode}`,
6709
+ `server_model_used=${event.server_model_used}`,
6710
+ `success=${event.success}`,
6711
+ event.request_id ? `request=${event.request_id}` : "",
6712
+ event.duration_ms !== undefined ? `duration_ms=${event.duration_ms}` : "",
6713
+ `created_at=${event.created_at}`
6714
+ ].filter(Boolean).join(" ");
6715
+ }
4740
6716
  function renderUsageSummary(summary) {
4741
6717
  const lines = [
4742
6718
  summary.account_id ? `account=${summary.account_id}` : "",
@@ -5048,6 +7024,20 @@ function renderBoardKnowledge(data) {
5048
7024
  `title=${file.title}`
5049
7025
  ].join(" ")).join("\n");
5050
7026
  }
7027
+ function renderBoardKnowledgeSearch(data) {
7028
+ const result = data;
7029
+ const matches = Array.isArray(result.matches) ? result.matches : [];
7030
+ if (matches.length === 0) {
7031
+ return "No matching board knowledge snippets.";
7032
+ }
7033
+ return matches.map((match, index) => [
7034
+ `#${index + 1}`,
7035
+ `${match.source}:${match.path}`,
7036
+ `score=${match.score}`,
7037
+ `title=${match.title}`,
7038
+ `excerpt=${match.excerpt.replace(/\s+/g, " ").trim()}`
7039
+ ].join(" ")).join("\n");
7040
+ }
5051
7041
  function renderBoardKnowledgeFile(file) {
5052
7042
  return [
5053
7043
  `template=${file.template_id}`,
@@ -5260,6 +7250,194 @@ function renderBuildWorkspaceSourcePatch(result) {
5260
7250
  }
5261
7251
  return lines.join("\n");
5262
7252
  }
7253
+ function renderLocalToolchainList(result) {
7254
+ const installedCount = result.environments.filter((environment) => !!environment.installed).length;
7255
+ const availableCount = result.environments.filter((environment) => environment.status === "available").length;
7256
+ const updateCount = result.environments.filter((environment) => environment.status === "update_available").length;
7257
+ const lines = [
7258
+ `Local development environments: ${result.environments.length}`,
7259
+ `installed=${installedCount} available=${availableCount} updates=${updateCount}`,
7260
+ `host=${result.host}`,
7261
+ `channel=${result.channel}`,
7262
+ result.metadata_source === "local_override" ? `metadata_override=${result.metadata_root}` : "metadata=production/built-in",
7263
+ `install_root=${result.install_root}`,
7264
+ `registry=${result.registry_path}`
7265
+ ];
7266
+ for (const environment of result.environments) {
7267
+ lines.push("");
7268
+ lines.push(`${environment.display_name} (${environment.board_id})`);
7269
+ lines.push(` status=${localToolchainStatusLabel(environment.status)}`);
7270
+ lines.push(` latest=${environment.latest.version}`);
7271
+ if (environment.installed) {
7272
+ lines.push(` installed=${environment.installed.version ?? "unknown"} mode=${environment.installed.mode ?? "unknown"}`);
7273
+ if (environment.installed.release_root) {
7274
+ lines.push(` release_root=${environment.installed.release_root}`);
7275
+ }
7276
+ }
7277
+ lines.push(` install_modes=${environment.install_modes.join(",")}`);
7278
+ if (environment.execution) {
7279
+ lines.push(` execution=${environment.execution.kind} supported=${environment.execution.supported}`);
7280
+ if (environment.execution.required_host) {
7281
+ lines.push(` execution_required_host=${environment.execution.required_host}`);
7282
+ }
7283
+ if (environment.execution.actual_host) {
7284
+ lines.push(` execution_actual_host=${environment.execution.actual_host}`);
7285
+ }
7286
+ if (environment.execution.reason) {
7287
+ lines.push(` execution_reason=${environment.execution.reason}`);
7288
+ }
7289
+ }
7290
+ lines.push(` install_command=${environment.install_command}`);
7291
+ if (environment.status === "update_available") {
7292
+ lines.push(` update_command=${environment.update_command}`);
7293
+ }
7294
+ if (environment.components?.length) {
7295
+ lines.push(` component_catalog=${localToolchainComponentSummary(environment.components)}`);
7296
+ if (environment.latest.default_mode) {
7297
+ lines.push(` default_mode_download=${environment.latest.default_mode}: ${localToolchainComponentSummaryForMode(environment.components, environment.latest.default_mode)}`);
7298
+ }
7299
+ if (environment.installed?.mode && environment.installed.mode !== environment.latest.default_mode) {
7300
+ lines.push(` installed_mode_components=${environment.installed.mode}: ${localToolchainComponentSummaryForMode(environment.components, environment.installed.mode)}`);
7301
+ }
7302
+ lines.push(` mode_downloads=${localToolchainModeSummaries(environment.install_modes, environment.components).join("; ")}`);
7303
+ lines.push(` package_groups=${localToolchainComponentGroups(environment.components).join(", ")}`);
7304
+ lines.push(` detail_command=embedlabs local toolchain latest --board ${environment.board_id}${result.channel === "stable" ? "" : ` --channel ${result.channel}`}`);
7305
+ }
7306
+ if (environment.notes.length > 0) {
7307
+ for (const note of environment.notes) {
7308
+ lines.push(` note=${note}`);
7309
+ }
7310
+ }
7311
+ }
7312
+ return lines.join("\n");
7313
+ }
7314
+ function localToolchainStatusLabel(status) {
7315
+ if (status === "installed")
7316
+ return "installed/已安装";
7317
+ if (status === "available")
7318
+ return "available/可安装";
7319
+ if (status === "update_available")
7320
+ return "update_available/可更新";
7321
+ if (status === "unsupported_host")
7322
+ return "unsupported_host/当前系统暂不支持";
7323
+ return status;
7324
+ }
7325
+ function renderWindowsWslStatus(result) {
7326
+ const lines = [
7327
+ "Windows WSL2 status",
7328
+ `host=${result.host}`,
7329
+ `applicable=${result.applicable}`,
7330
+ `wsl_available=${result.wsl_available}`,
7331
+ `usable=${result.usable}`,
7332
+ `taishanpi_execution=${result.taishanpi_execution.status} supported=${result.taishanpi_execution.supported}`,
7333
+ `taishanpi_required_host=${result.taishanpi_execution.required_host}`,
7334
+ result.taishanpi_execution.actual_host ? `taishanpi_actual_host=${result.taishanpi_execution.actual_host}` : "",
7335
+ `taishanpi_reason=${result.taishanpi_execution.reason}`,
7336
+ `checked_at=${result.checked_at}`,
7337
+ `status_command=${result.commands.status}`,
7338
+ `list_command=${result.commands.list}`,
7339
+ `list_online_command=${result.commands.list_online}`,
7340
+ `install_command=${result.commands.install_ubuntu}`
7341
+ ].filter(Boolean);
7342
+ if (result.distributions.length > 0) {
7343
+ lines.push("distributions:");
7344
+ for (const distro of result.distributions) {
7345
+ lines.push(` ${distro.default ? "*" : "-"} ${distro.name} state=${distro.state ?? "unknown"} version=${distro.version ?? "unknown"}`);
7346
+ }
7347
+ }
7348
+ if (result.online_distributions.length > 0) {
7349
+ lines.push("online_distributions:");
7350
+ for (const distro of result.online_distributions) {
7351
+ lines.push(` ${distro.default ? "*" : "-"} ${distro.name}${distro.friendly_name ? ` (${distro.friendly_name})` : ""}`);
7352
+ }
7353
+ }
7354
+ for (const note of result.notes) {
7355
+ lines.push(`note=${note}`);
7356
+ }
7357
+ return lines.join("\n");
7358
+ }
7359
+ function renderWindowsWslInstall(result) {
7360
+ const lines = [
7361
+ "Windows WSL2 install",
7362
+ `host=${result.host}`,
7363
+ `command=${result.command.join(" ")}`,
7364
+ `exit_code=${result.exit_code}`,
7365
+ `usable_after=${result.status_after.usable}`
7366
+ ];
7367
+ if (result.stdout_tail.length > 0) {
7368
+ lines.push("stdout_tail:");
7369
+ for (const line of result.stdout_tail) {
7370
+ lines.push(` ${line}`);
7371
+ }
7372
+ }
7373
+ if (result.stderr_tail.length > 0) {
7374
+ lines.push("stderr_tail:");
7375
+ for (const line of result.stderr_tail) {
7376
+ lines.push(` ${line}`);
7377
+ }
7378
+ }
7379
+ for (const note of result.notes) {
7380
+ lines.push(`note=${note}`);
7381
+ }
7382
+ return lines.join("\n");
7383
+ }
7384
+ function localToolchainComponentSummary(components) {
7385
+ const totalBytes = components.reduce((total, component) => total + component.size_bytes, 0);
7386
+ return `${components.length} components, ${formatByteSize(totalBytes)}`;
7387
+ }
7388
+ function localToolchainComponentSummaryForMode(components, mode) {
7389
+ return localToolchainComponentSummary(localToolchainComponentsForMode(components, mode));
7390
+ }
7391
+ function localToolchainModeSummaries(modes, components) {
7392
+ return modes.map((mode) => `${mode}=${localToolchainComponentSummaryForMode(components, mode)}`);
7393
+ }
7394
+ function localToolchainComponentsForMode(components, mode) {
7395
+ return components.filter((component) => {
7396
+ if (!component.install_modes?.length) {
7397
+ return true;
7398
+ }
7399
+ return component.install_modes.includes(mode);
7400
+ });
7401
+ }
7402
+ function localToolchainComponentGroups(components) {
7403
+ const groups = new Set();
7404
+ for (const component of components) {
7405
+ const text = `${component.id} ${component.role ?? ""}`.toLowerCase();
7406
+ if (text.includes("arm-none-eabi") || text.includes("bare-metal") || text.includes("compiler"))
7407
+ groups.add("compiler/ARM 裸机编译器");
7408
+ if (text.includes("pico-sdk") || text.includes("sdk-core"))
7409
+ groups.add("pico-sdk/C/C++ SDK");
7410
+ if (text.includes("sysroot") || text.includes("cross"))
7411
+ groups.add("sysroot/交叉运行库");
7412
+ if (text.includes("qt"))
7413
+ groups.add("qt/Qt 应用支持");
7414
+ if (text.includes("rockchip") || text.includes("boot") || text.includes("resource"))
7415
+ groups.add("boot-flash/启动与烧写工具");
7416
+ if (text.includes("image") || text.includes("rootfs"))
7417
+ groups.add("images/镜像资源");
7418
+ if (text.includes("initial-firmware"))
7419
+ groups.add("initial-firmware/初始化镜像");
7420
+ if (text.includes("rp2350-monitor"))
7421
+ groups.add("rp2350-monitor/可选硬件监控镜像");
7422
+ if (text.includes("meta"))
7423
+ groups.add("metadata/知识与脚本元数据");
7424
+ }
7425
+ return groups.size > 0 ? [...groups] : ["runtime/运行时工具"];
7426
+ }
7427
+ function formatByteSize(bytes) {
7428
+ if (!Number.isFinite(bytes) || bytes < 0) {
7429
+ return "unknown size";
7430
+ }
7431
+ const units = ["B", "KB", "MB", "GB", "TB"];
7432
+ let value = bytes;
7433
+ let unit = 0;
7434
+ while (value >= 1024 && unit < units.length - 1) {
7435
+ value /= 1024;
7436
+ unit += 1;
7437
+ }
7438
+ const fixed = unit === 0 || value >= 10 ? value.toFixed(0) : value.toFixed(1);
7439
+ return `${fixed} ${units[unit]}`;
7440
+ }
5263
7441
  function renderLocalToolchainLatest(result) {
5264
7442
  const lines = [
5265
7443
  `board=${result.board_id}`,
@@ -5267,11 +7445,26 @@ function renderLocalToolchainLatest(result) {
5267
7445
  `version=${result.version}`,
5268
7446
  `host=${result.host}`,
5269
7447
  result.metadata_root ? `metadata_root=${result.metadata_root}` : "metadata=built-in",
5270
- result.download ? `download=${result.download.mirror_kind}:${result.download.source_url}` : "",
5271
- result.download ? `archive_sha256=${result.download.archive.sha256}` : "",
5272
- result.download ? `archive_size_bytes=${result.download.archive.size_bytes}` : "",
7448
+ result.download?.source_url ? `download=${result.download.mirror_kind}:${result.download.source_url}` : "",
7449
+ result.download?.archive ? `archive_sha256=${result.download.archive.sha256}` : "",
7450
+ result.download?.archive ? `archive_size_bytes=${result.download.archive.size_bytes}` : "",
7451
+ result.download?.components?.length ? `components=${result.download.components.length}` : "",
7452
+ result.download?.default_mode ? `default_mode=${result.download.default_mode}` : "",
5273
7453
  result.download_error ? `download_error=${result.download_error}` : ""
5274
7454
  ].filter(Boolean);
7455
+ if (result.download?.components?.length) {
7456
+ const modes = [...new Set(result.download.components.flatMap((component) => component.install_modes ?? ["all"]))].filter((mode) => mode !== "all");
7457
+ if (result.download.default_mode) {
7458
+ lines.push(`default_mode_download=${result.download.default_mode}: ${localToolchainDownloadComponentSummaryForMode(result.download.components, result.download.default_mode)}`);
7459
+ }
7460
+ if (modes.length > 0) {
7461
+ lines.push(`mode_downloads=${modes.map((mode) => `${mode}=${localToolchainDownloadComponentSummaryForMode(result.download?.components ?? [], mode)}`).join("; ")}`);
7462
+ }
7463
+ lines.push("download_components:");
7464
+ for (const component of result.download.components) {
7465
+ lines.push(` ${component.id}@${component.version} modes=${component.install_modes?.join(",") || "all"} size=${formatByteSize(component.archive.size_bytes)} file=${component.archive.file}`);
7466
+ }
7467
+ }
5275
7468
  if (result.packages.length > 0) {
5276
7469
  lines.push("packages:");
5277
7470
  for (const pkg of result.packages) {
@@ -5280,6 +7473,11 @@ function renderLocalToolchainLatest(result) {
5280
7473
  }
5281
7474
  return lines.join("\n");
5282
7475
  }
7476
+ function localToolchainDownloadComponentSummaryForMode(components, mode) {
7477
+ const selected = localToolchainComponentsForMode(components ?? [], mode);
7478
+ const totalBytes = selected.reduce((total, component) => total + component.archive.size_bytes, 0);
7479
+ return `${selected.length} components, ${formatByteSize(totalBytes)}`;
7480
+ }
5283
7481
  function renderLocalToolchainCurrent(result) {
5284
7482
  if (!result.installed) {
5285
7483
  return [
@@ -5294,6 +7492,7 @@ function renderLocalToolchainCurrent(result) {
5294
7492
  `board=${result.board_id}`,
5295
7493
  result.version ? `version=${result.version}` : "",
5296
7494
  result.channel ? `channel=${result.channel}` : "",
7495
+ result.mode ? `mode=${result.mode}` : "",
5297
7496
  result.release_root ? `release_root=${result.release_root}` : "",
5298
7497
  `install_root=${result.install_root}`,
5299
7498
  `registry=${result.registry_path}`
@@ -5306,13 +7505,21 @@ function renderLocalToolchainInstall(result) {
5306
7505
  `version=${result.version}`,
5307
7506
  `channel=${result.channel}`,
5308
7507
  `host=${result.host}`,
7508
+ `mode=${result.mode}`,
5309
7509
  `install_root=${result.install_root}`,
5310
7510
  `release_root=${result.release_root}`,
5311
7511
  `registry=${result.registry_path}`,
5312
7512
  `source=${result.source.kind}:${result.source.value}`,
5313
7513
  result.source.downloaded_path ? `downloaded=${result.source.downloaded_path}` : "",
7514
+ result.source.components?.length ? `components=${result.source.components.length}` : "",
5314
7515
  `validation=${result.validation.ok ? "ok" : "failed"}`
5315
7516
  ].filter(Boolean);
7517
+ if (result.source.components?.length) {
7518
+ lines.push("installed_components:");
7519
+ for (const component of result.source.components) {
7520
+ lines.push(` ${component.id}@${component.version} ${component.mirror_kind || ""} bytes=${component.size_bytes}`);
7521
+ }
7522
+ }
5316
7523
  if (result.installed_paths.length > 0) {
5317
7524
  lines.push("installed_paths:");
5318
7525
  for (const installedPath of result.installed_paths) {
@@ -5327,16 +7534,50 @@ function renderLocalToolchainInstall(result) {
5327
7534
  }
5328
7535
  return lines.join("\n");
5329
7536
  }
7537
+ function renderLocalToolchainUninstall(result) {
7538
+ const lines = [
7539
+ result.removed ? "Local toolchain uninstalled." : "Local toolchain was not installed.",
7540
+ `board=${result.board_id}`,
7541
+ `install_root=${result.install_root}`,
7542
+ `registry=${result.registry_path}`,
7543
+ `removed_registry_entry=${result.removed_registry_entry}`,
7544
+ `observed_at=${result.observed_at}`
7545
+ ];
7546
+ if (result.removed_paths.length > 0) {
7547
+ lines.push("removed_paths:");
7548
+ for (const removedPath of result.removed_paths) {
7549
+ lines.push(` ${removedPath}`);
7550
+ }
7551
+ }
7552
+ if (result.remaining_installed_boards.length > 0) {
7553
+ lines.push(`remaining_installed_boards=${result.remaining_installed_boards.join(",")}`);
7554
+ }
7555
+ return lines.join("\n");
7556
+ }
5330
7557
  function renderLocalToolchainValidation(result) {
5331
7558
  const lines = [
5332
7559
  result.ok ? "Local toolchain ready." : "Local toolchain not ready.",
5333
7560
  `board=${result.board_id}`,
7561
+ `mode=${result.mode}`,
5334
7562
  `host=${result.host.platform}/${result.host.arch}`,
5335
- `release_root=${result.release_root}`
7563
+ `release_root=${result.release_root}`,
7564
+ `summary=${result.summary_for_user}`
5336
7565
  ];
7566
+ if (!result.ok && result.missing_groups.length > 0) {
7567
+ lines.push(`missing_groups=${result.missing_groups.join(", ")}`);
7568
+ }
7569
+ if (result.repair_command) {
7570
+ lines.push(`repair_command=${result.repair_command}`);
7571
+ }
5337
7572
  for (const check of result.checked_paths) {
5338
7573
  lines.push(`${check.exists ? "ok" : "missing"} ${check.label}: ${check.path}`);
5339
7574
  }
7575
+ if (result.path_leaks.length > 0) {
7576
+ lines.push("path_leaks:");
7577
+ for (const leak of result.path_leaks) {
7578
+ lines.push(` ${leak.label}: ${leak.path} contains ${leak.forbidden}`);
7579
+ }
7580
+ }
5340
7581
  if (result.notes.length > 0) {
5341
7582
  lines.push("notes:");
5342
7583
  for (const note of result.notes) {
@@ -5785,6 +8026,22 @@ function stringFlag(parsed, name) {
5785
8026
  function booleanFlag(parsed, name) {
5786
8027
  return parsed.flags[name] === true;
5787
8028
  }
8029
+ function optionalBooleanFlag(parsed, name) {
8030
+ const values = flagValues(parsed, name);
8031
+ if (values.length === 0)
8032
+ return undefined;
8033
+ const value = values[values.length - 1];
8034
+ if (value === true)
8035
+ return true;
8036
+ if (typeof value !== "string")
8037
+ return `--${name} must be true or false.`;
8038
+ const normalized = value.trim().toLowerCase();
8039
+ if (["1", "true", "yes", "y", "on"].includes(normalized))
8040
+ return true;
8041
+ if (["0", "false", "no", "n", "off"].includes(normalized))
8042
+ return false;
8043
+ return `--${name} must be true or false.`;
8044
+ }
5788
8045
  function switchFlag(parsed, name) {
5789
8046
  const values = flagValues(parsed, name);
5790
8047
  for (const value of values) {
@@ -5948,6 +8205,24 @@ function optionalPositiveIntegerFlag(parsed, name) {
5948
8205
  }
5949
8206
  return { value };
5950
8207
  }
8208
+ function optionalIntegerFlag(parsed, name, min, max) {
8209
+ const values = flagValues(parsed, name);
8210
+ if (values.length === 0) {
8211
+ return {};
8212
+ }
8213
+ if (values.some((value) => typeof value !== "string")) {
8214
+ return { error: `--${name} requires a value.` };
8215
+ }
8216
+ const raw = values[values.length - 1].trim();
8217
+ if (!/^-?\d+$/.test(raw)) {
8218
+ return { error: `--${name} must be an integer.` };
8219
+ }
8220
+ const value = Number(raw);
8221
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
8222
+ return { error: `--${name} must be an integer from ${min} through ${max}.` };
8223
+ }
8224
+ return { value };
8225
+ }
5951
8226
  function jsonObjectListFlag(parsed, name) {
5952
8227
  const objects = [];
5953
8228
  for (const raw of flagValues(parsed, name)) {
@@ -6065,12 +8340,17 @@ async function waitForever() {
6065
8340
  });
6066
8341
  return 0;
6067
8342
  }
8343
+ function quotaAndBillingDisabled() {
8344
+ return fail("quota_not_supported", "当前 Embed Labs 服务器只提供 MCP 服务,不提供额度、充值、余额、账本、存储配额或用量计费功能。", {
8345
+ remediation: "请使用 auth、plugin、board、local toolchain、mcp start、device 和 Local Bridge 相关命令。"
8346
+ });
8347
+ }
6068
8348
  function printHelp() {
6069
8349
  printCliHelp(`embed CLI
6070
8350
 
6071
8351
  Usage:
6072
8352
  embed <command> [options]
6073
- embed query "查一下我的额度"
8353
+ embed query "当前支持哪些开发板"
6074
8354
  embed help getting-started
6075
8355
  embed help commands
6076
8356
 
@@ -6080,68 +8360,57 @@ Main workflow:
6080
8360
  2. Sign in with a token:
6081
8361
  embed auth login --token <token>
6082
8362
  # or set EMBED_API_TOKEN / create an account API key for automation.
6083
- 3. Inspect server model routing:
8363
+ 3. Install or update local AI client plugins:
6084
8364
  embed plugin install codex
6085
8365
  embed plugin install opencode
6086
- embed service modes
6087
- embed model list
6088
- embed model default
8366
+ embed plugin update check
6089
8367
  4. Run a natural-language local tool loop:
6090
8368
  embed agent run --prompt "验证开发板状态"
6091
8369
  5. Validate or use the local TaishanPi toolchain:
8370
+ embed local toolchain list
8371
+ embed local toolchain installed
6092
8372
  embed local toolchain latest
6093
8373
  embed local toolchain install
8374
+ embed local toolchain uninstall --board pico2w-rp2350-monitor
6094
8375
  embed local toolchain validate
6095
8376
  embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
6096
8377
  embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
6097
- 6. Pick a cloud build template:
8378
+ embed local build qt-smoke --source ./taishanpi-app --target-name app --build-dir ./.embed-labs/build/app
8379
+ 6. Query board knowledge and method metadata:
6098
8380
  embed board registry list
6099
8381
  embed board methods taishanpi-1m-rk3566
6100
8382
  embed board knowledge taishanpi-1m-rk3566
8383
+ embed board knowledge search taishanpi-1m-rk3566 --query "UART pinout"
6101
8384
  embed build template list
6102
8385
  embed build template show <template_id>
6103
- 7. Provision and populate a build workspace:
6104
- embed build workspace provision --account <account_id> --project <project_id> --template <template_id>
6105
- embed build resource lease create --workspace <workspace_id> --execution-mode cloud_worker
6106
- embed build workspace source put <workspace_id> --file ./main.c:src/main.c
6107
- embed build workspace source list <workspace_id>
6108
- embed build workspace source get <workspace_id> --path src/main.c --output ./main.c
6109
- embed build workspace source search <workspace_id> --query init --glob "**/*.c"
6110
- embed build workspace source patch <workspace_id> --patch ./fix.patch
6111
- embed build workspace release <workspace_id> --dry-run
6112
- 8. Generate application source on the server and follow artifacts:
6113
- embed build application generate --workspace <workspace_id> --prompt "Create a minimal Linux app" --provider bai --model gpt-5.2
6114
- embed build application compile --workspace <workspace_id> --source app/generated.c --execution-mode docker_worker
6115
- embed build image generate --workspace <workspace_id> --prompt "Generate a minimal TaishanPi image"
8386
+ 7. Generate small image packages and compose locally:
6116
8387
  embed build image boot-logo --logo ./logo.png --board taishanpi --variant 1M-RK3566 --output ./boot-logo-package.json
6117
8388
  embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
6118
8389
  embed cloud task artifacts <task_id>
6119
8390
  embed artifact download <artifact_id> --output ./artifact.bin
6120
- 9. Check credits or create a recharge QR:
6121
- embed billing balance --account <account_id>
6122
- embed billing tokens --account <account_id>
6123
- embed billing ledger --account <account_id>
6124
- embed billing storage --account <account_id>
6125
- embed billing storage settle --account <account_id> --dry-run
6126
- embed billing recharge create --account <account_id> --amount-usd 10 --provider onchain --qr
6127
-
6128
8391
  Local hardware:
6129
8392
  embed bridge start
8393
+ embed mcp start
6130
8394
  embed agent run --prompt "验证开发板状态"
6131
8395
  embed agent run --prompt "部署泰山派应用" --host 198.19.77.2 --artifact ./artifact.bin --remote-path /userdata/embed-labs/apps/app --approve --run
6132
8396
  embed agent run --prompt "部署生成的泰山派应用" --host 198.19.77.2 --artifact-task <task_id> --remote-path /userdata/embed-labs/apps/app --approve --run
6133
8397
  embed tool list
6134
- embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22,15301]}'
8398
+ embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22]}'
6135
8399
  embed tool call wifi.scan --input-json '{"host":"198.19.77.2","user":"root"}'
8400
+ embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve
6136
8401
  embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}'
6137
8402
  embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}'
6138
8403
  embed device list
8404
+ embed local toolchain list
8405
+ embed local toolchain installed
6139
8406
  embed local toolchain latest
6140
8407
  embed local toolchain current
6141
8408
  embed local toolchain install
8409
+ embed local toolchain uninstall --board pico2w-rp2350-monitor
6142
8410
  embed local toolchain validate
6143
8411
  embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
6144
8412
  embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
8413
+ embed local build qt-smoke --source ./taishanpi-app --target-name app --build-dir ./.embed-labs/build/app
6145
8414
  embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
6146
8415
  embed deploy taishanpi --host 198.19.77.2 --artifact ./artifact.bin --approve --run
6147
8416
  embed flash plan --board <rp2350|taishanpi>
@@ -6152,16 +8421,14 @@ Help:
6152
8421
 
6153
8422
  Environment:
6154
8423
  EMBED_BRIDGE_URL=http://127.0.0.1:18083
6155
- EMBED_CLOUD_API_URL=http://127.0.0.1:18100
8424
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
6156
8425
  EMBED_API_TOKEN=<token>
6157
8426
  CODEX_HOME=~/.codex
6158
8427
 
6159
8428
  Natural language:
6160
- embed query "查一下我的额度"
6161
- embed query "用 USDC 链上充值 100 美元"
6162
- embed query "看一下 token 用量"
6163
- embed query "看一下磁盘空间"
6164
- embed query "结算磁盘空间扣费"
8429
+ embed query "当前支持哪些开发板"
8430
+ embed query "验证开发板状态"
8431
+ embed query "安装泰山派本地开发环境"
6165
8432
  `);
6166
8433
  }
6167
8434
  function printHelpTopic(topic) {
@@ -6207,20 +8474,21 @@ Install local AI client plugins explicitly:
6207
8474
  embed plugin list
6208
8475
  embed plugin install codex
6209
8476
  embed plugin install opencode
8477
+ embed plugin update check
8478
+ embed plugin update all
6210
8479
 
6211
- Cloud build path:
8480
+ Local MCP service path:
6212
8481
 
6213
- embed service modes
6214
- embed model list
6215
- embed model default
6216
8482
  embed agent run --prompt "验证开发板状态"
6217
8483
  embed local toolchain validate
6218
8484
  embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
6219
8485
  embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
8486
+ embed local build qt-smoke --source ./taishanpi-app --target-name app --build-dir ./.embed-labs/build/app
6220
8487
  embed board registry list
6221
8488
  embed board registry show taishanpi-1m-rk3566
6222
8489
  embed board methods taishanpi-1m-rk3566
6223
8490
  embed board knowledge taishanpi-1m-rk3566
8491
+ embed board knowledge search taishanpi-1m-rk3566 --query "UART pinout"
6224
8492
  embed build template list
6225
8493
  embed build template show <template_id>
6226
8494
  embed build workspace provision --account <account_id> --project <project_id> --template <template_id>
@@ -6231,9 +8499,6 @@ Cloud build path:
6231
8499
  embed build workspace source search <workspace_id> --query init --glob "**/*.c"
6232
8500
  embed build workspace source patch <workspace_id> --patch ./fix.patch
6233
8501
  embed build workspace release <workspace_id> --dry-run
6234
- embed build application generate --workspace <workspace_id> --prompt "Create a minimal Linux app" --provider bai --model gpt-5.2
6235
- embed build application compile --workspace <workspace_id> --source app/generated.c --execution-mode docker_worker
6236
- embed build image generate --workspace <workspace_id> --prompt "Generate a minimal TaishanPi image"
6237
8502
  embed build image boot-logo --logo ./logo.png --board taishanpi --variant 1M-RK3566 --output ./boot-logo-package.json
6238
8503
  embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
6239
8504
  embed cloud task status <task_id>
@@ -6242,29 +8507,18 @@ Cloud build path:
6242
8507
 
6243
8508
  Natural language shortcuts:
6244
8509
 
6245
- embed query "查一下我的额度"
6246
- embed query "用 USDC 链上充值 100 美元"
6247
- embed query "看一下 token 用量"
6248
- embed query "看一下磁盘空间"
6249
-
6250
- Billing path:
6251
-
6252
- embed billing balance --account <account_id>
6253
- embed billing tokens --account <account_id>
6254
- embed billing ledger --account <account_id>
6255
- embed billing storage --account <account_id>
6256
- embed billing storage settle --account <account_id> --dry-run
6257
- embed billing recharge create --account <account_id> --amount-usd 10 --provider onchain --qr
6258
- embed billing recharge submit-tx <recharge_session_id> --tx-hash <hash>
6259
- embed billing recharge list --account <account_id>
8510
+ embed query "当前支持哪些开发板"
8511
+ embed query "验证开发板状态"
8512
+ embed query "安装泰山派本地开发环境"
6260
8513
 
6261
8514
  Local hardware path:
6262
8515
 
6263
8516
  embed bridge start
8517
+ embed mcp start
6264
8518
  embed run "验证开发板状态"
6265
8519
  embed tool list
6266
8520
  embed tool call debug.tools.scan
6267
- embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22,15301]}'
8521
+ embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22]}'
6268
8522
  embed device list
6269
8523
  embed deploy taishanpi --host 198.19.77.2 --artifact ./artifact.bin --approve --run
6270
8524
  embed flash plan --board <rp2350|taishanpi> --artifact ./artifact.bin
@@ -6280,11 +8534,14 @@ Usage:
6280
8534
  embed query <natural language request> [--account <account_id>] [--qr] [--json]
6281
8535
  embed bridge start [--host 127.0.0.1] [--port 18083]
6282
8536
  embed bridge status [--json]
8537
+ embed mcp start [--bridge-path <path>]
6283
8538
  embed auth login --token <token> [--profile default] [--json]
6284
8539
  embed auth status [--json]
6285
8540
  embed auth logout [--json]
6286
8541
  embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]
6287
8542
  embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]
8543
+ embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
8544
+ embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
6288
8545
  embed service modes [--json]
6289
8546
  embed model list [--json]
6290
8547
  embed model default [--json]
@@ -6292,6 +8549,7 @@ Usage:
6292
8549
  embed board registry show <template_id> [--json]
6293
8550
  embed board methods <template_id> [--json]
6294
8551
  embed board knowledge <template_id> [--json]
8552
+ embed board knowledge search <template_id> --query <text> [--source board_pack|build_template|registry] [--limit 5] [--json]
6295
8553
  embed board knowledge file <template_id> --source board_pack|build_template|registry --path <relative_path> [--output <local_path>] [--json]
6296
8554
  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]
6297
8555
  embed run <natural language request> [--provider stub|openai|bai|cc|claude-code] [--approve] [--json]
@@ -6302,23 +8560,6 @@ Usage:
6302
8560
  embed account keys create --account <account_id> [--name <name>] [--json]
6303
8561
  embed account keys list --account <account_id> [--json]
6304
8562
  embed account keys revoke <api_key_id> [--json]
6305
- embed usage record [--api-key <key>|--api-key-id <api_key_id>] --model <model> --input-tokens <n> --output-tokens <n> [--provider <name>] [--operation <name>] [--task <task_id>] [--request-id <id>] [--json]
6306
- embed usage summary --account <account_id>|--api-key-id <api_key_id> [--from <iso>] [--to <iso>] [--json]
6307
- embed usage events --account <account_id> [--api-key-id <api_key_id>] [--from <iso>] [--to <iso>] [--limit 100] [--json]
6308
- embed billing statement --account <account_id> [--from <iso>] [--to <iso>] [--json]
6309
- embed billing balance --account <account_id> [--json]
6310
- embed billing tokens --account <account_id> [--json]
6311
- embed billing ledger --account <account_id> [--json]
6312
- embed billing storage --account <account_id> [--json]
6313
- embed billing storage settle --account <account_id> [--dry-run] [--json]
6314
- embed billing recharge create --account <account_id> --amount-usd <amount> [--provider mock|stripe|onchain] [--chain <chain>] [--token <symbol>] [--success-url <url>] [--cancel-url <url>] [--qr] [--json]
6315
- embed billing recharge list --account <account_id> [--json]
6316
- embed billing recharge show <recharge_session_id> [--json]
6317
- embed billing recharge submit-tx <recharge_session_id> --tx-hash <hash> [--json]
6318
- embed billing recharge confirm <recharge_session_id> [--json]
6319
- embed billing snapshot create --account <account_id> [--from <iso>] [--to <iso>] [--json]
6320
- embed billing snapshot list --account <account_id> [--json]
6321
- embed billing snapshot show <billing_snapshot_id> [--json]
6322
8563
  embed service modes [--json]
6323
8564
  embed build template list [--json]
6324
8565
  embed build template show <template_id> [--json]
@@ -6340,13 +8581,16 @@ Usage:
6340
8581
  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]
6341
8582
  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]
6342
8583
  embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]
6343
- embed local toolchain latest [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--json]
8584
+ embed local toolchain list [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
8585
+ embed local toolchain installed [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
8586
+ embed local toolchain latest [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--json]
6344
8587
  embed local toolchain current [--install-root <path>] [--json]
6345
- 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]
8588
+ 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]
6346
8589
  Defaults to the production download channel at download.embedboard.com.
6347
- embed local toolchain validate [--release-root <path>] [--json]
8590
+ embed local toolchain uninstall --board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor [--install-root <path>] [--yes|--force] [--json]
8591
+ 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]
6348
8592
  embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]
6349
- embed local build qt-smoke --build-dir <dir> [--source <qt-smoke-dir>] [--release-root <path>] [--account <account_id>] [--json]
8593
+ embed local build qt-smoke --build-dir <dir> [--source <qt-cmake-dir>] [--target-name <executable>] [--release-root <path>] [--account <account_id>] [--json]
6350
8594
  embed debug tools [--json]
6351
8595
  embed tool list [--json]
6352
8596
  embed tool call wifi.scan --input-json '{"host":"198.19.77.2","user":"root"}' [--json]
@@ -6355,6 +8599,18 @@ Usage:
6355
8599
  embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}' [--json]
6356
8600
  embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
6357
8601
  embed tool call qml.runtime.start --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
8602
+ embed tool call rp2350.monitor.capabilities [--json]
8603
+ embed tool call rp2350.monitor.status [--json]
8604
+ embed tool call rp2350.monitor.gpio.read --input-json '{"pins":[16,17]}' --approve [--json]
8605
+ embed tool call rp2350.monitor.gpio.write --input-json '{"pin":16,"level":true}' --approve [--json]
8606
+ embed tool call rp2350.monitor.uart.write --input-json '{"baud":115200,"text":"hello","line_ending":"lf"}' --approve [--json]
8607
+ embed tool call rp2350.monitor.i2c.transfer --input-json '{"address":"0x50","write":"00","read_len":4}' --approve [--json]
8608
+ embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve [--json]
8609
+ embed tool call rp2350.monitor.logic.capture --input-json '{"pin_base":16,"pin_count":4,"sample_rate":1000000,"samples":4096}' --approve [--json]
8610
+ embed tool call rp2350.monitor.logic.decode --input-json '{"input_path":".embed-labs/rp2350-monitor/captures/logic.jsonl","decoder":"summary"}' [--json]
8611
+ embed tool call rp2350.monitor.wifi.manage --input-json '{"action":"scan"}' --approve [--json]
8612
+ embed tool call rp2350.monitor.probe.debug --input-json '{"action":"status"}' --approve [--json]
8613
+ embed tool call rp2350.monitor.operation --input-json '{"action":"logic.stop","params":{}}' --approve [--json]
6358
8614
  embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
6359
8615
  embed board deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
6360
8616
  embed device list [--json]
@@ -6388,7 +8644,7 @@ Usage:
6388
8644
 
6389
8645
  Environment:
6390
8646
  EMBED_BRIDGE_URL=http://127.0.0.1:18083
6391
- EMBED_CLOUD_API_URL=http://127.0.0.1:18100
8647
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
6392
8648
  EMBED_API_TOKEN=<token>
6393
8649
  CODEX_HOME=~/.codex
6394
8650
  `);