@kvell007/embed-labs-cli 0.1.0-alpha.9 → 0.1.0-alpha.91

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";
3
- import { constants } from "node:fs";
2
+ import { createHash, createHmac, generateKeyPairSync, randomBytes, sign as signCrypto } from "node:crypto";
3
+ import { constants, existsSync } 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,21 @@ 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]";
30
- 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]";
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 MCP_CONFIG_USAGE = "Usage: embed mcp config [--client generic|trae|cursor|claude|windsurf] [--absolute-command] [--cloud-url <url>] [--json]";
38
+ const LEGACY_CODEX_PLUGIN_NAMES = [
39
+ "dbt-agent",
40
+ "Dbt Agent",
41
+ "development-board-toolchain",
42
+ "development-board-toolchain-dev",
43
+ "deve"
44
+ ];
45
+ const LEGACY_CODEX_MARKETPLACE_NAMES = new Set(["embed-labs-plugins", "plugins", "Plugins", "deve"]);
46
+ const PLUGIN_INSTALL_USAGE = "Usage: embed plugin install <codex|opencode|trae|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--trae-target <dir>] [--force] [--json]";
47
+ const PLUGIN_UPDATE_CHECK_USAGE = "Usage: embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
48
+ const PLUGIN_UPDATE_USAGE = "Usage: embed plugin update <codex|opencode|trae|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--trae-target <dir>] [--json]";
31
49
  const CLOUD_TASK_ARTIFACTS_USAGE = "Usage: embed cloud task artifacts <task_id> [--json]";
32
50
  const CLOUD_TASK_EVIDENCE_USAGE = "Usage: embed cloud task evidence <task_id> [--json]";
33
51
  const ARTIFACT_STATUS_USAGE = "Usage: embed artifact status <artifact_id> [--json]";
@@ -81,25 +99,36 @@ const BUILD_IMAGE_BOOT_LOGO_USAGE = "Usage: embed build image boot-logo --logo <
81
99
  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
100
  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
101
  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]";
102
+ 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]";
103
+ 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]";
104
+ 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
105
  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]";
106
+ 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.";
107
+ 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]";
108
+ 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]";
109
+ const LOCAL_WSL_STATUS_USAGE = "Usage: embed local wsl status [--json]";
110
+ 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
111
  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]";
112
+ 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]";
113
+ const AUTH_DEVICE_STATUS_USAGE = "Usage: embed auth device status [--json]";
114
+ const AUTH_DEVICE_LIST_USAGE = "Usage: embed auth device list [--json]";
115
+ const AUTH_DEVICE_REVOKE_USAGE = "Usage: embed auth device revoke <device_id> [--json]";
116
+ const AUTH_DEVICE_RENAME_USAGE = "Usage: embed auth device rename <device_id> --label <name> [--json]";
90
117
  const BOARD_REGISTRY_LIST_USAGE = "Usage: embed board registry list [--json]";
91
118
  const BOARD_REGISTRY_SHOW_USAGE = "Usage: embed board registry show <template_id> [--json]";
92
119
  const BOARD_METHODS_USAGE = "Usage: embed board methods <template_id> [--json]";
93
120
  const BOARD_KNOWLEDGE_USAGE = "Usage: embed board knowledge <template_id> [--json]";
121
+ const BOARD_KNOWLEDGE_SEARCH_USAGE = "Usage: embed board knowledge search <template_id> --query <text> [--source board_pack|build_template|registry] [--limit 5] [--json]";
94
122
  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
123
  const MODEL_LIST_USAGE = "Usage: embed model list [--json]";
96
124
  const MODEL_DEFAULT_USAGE = "Usage: embed model default [--json]";
97
125
  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]";
126
+ 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]";
127
+ let cachedLocalHardwareFingerprint;
99
128
  const TOOL_LIST_USAGE = "Usage: embed tool list [--json]";
100
129
  const TOOL_CALL_USAGE = "Usage: embed tool call <capability_id> [--input-json '<json>'] [--approve] [--json]";
101
- const MCP_TOOL_EVENT_USAGE = "Usage: embed mcp log --tool <tool_name> [--client codex|opencode] [--mode local_ai|server_ai] [--server-model-used true|false] [--success true|false] [--request-id <id>] [--duration-ms <ms>] [--input-summary <text>] [--output-summary <text>] [--json]";
102
- 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]";
130
+ 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]";
131
+ 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]";
103
132
  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]";
104
133
  const TASK_STATES = new Set([
105
134
  "created",
@@ -120,6 +149,10 @@ async function main(argv) {
120
149
  const parsed = parseArgs(argv);
121
150
  const [area, action] = parsed.command;
122
151
  try {
152
+ if (parsed.flags.version || area === "version") {
153
+ console.log(`embedlabs ${EMBED_CLIENT_VERSION}`);
154
+ return 0;
155
+ }
123
156
  if (!area || parsed.flags.help) {
124
157
  printHelp();
125
158
  return 0;
@@ -148,15 +181,28 @@ async function main(argv) {
148
181
  return output(parsed, await bridgePost("/v1/board/taishanpi/deploy", request), renderBoardDeployResult);
149
182
  }
150
183
  if (area === "bridge" && action === "start") {
151
- startServer({
152
- host: stringFlag(parsed, "host"),
153
- port: numberFlag(parsed, "port")
154
- });
155
- return await waitForever();
184
+ return await runBridgeStart(parsed);
156
185
  }
157
186
  if (area === "bridge" && action === "status") {
158
187
  return output(parsed, await bridgeGet("/healthz"), renderBridgeStatus);
159
188
  }
189
+ if (area === "mcp" && action === "start") {
190
+ return await runMcpStart(parsed);
191
+ }
192
+ if (area === "mcp" && (action === "config" || action === "configuration")) {
193
+ const result = await mcpConfig(parsed);
194
+ return output(parsed, result, renderMcpConfig, result.ok ? 0 : 2);
195
+ }
196
+ if (area === "mcp" && (action === "log" || action === "tool-event")) {
197
+ const body = mcpToolEventBody(parsed);
198
+ if (typeof body === "string") {
199
+ return output(parsed, fail("invalid_args", body), undefined, 2);
200
+ }
201
+ return output(parsed, await cloudPost("/v1/mcp/tool-events", body), renderMcpToolEvent);
202
+ }
203
+ if (area === "mcp") {
204
+ return output(parsed, fail("invalid_args", [MCP_START_USAGE, MCP_CONFIG_USAGE, MCP_TOOL_EVENT_USAGE].join("\n")), undefined, 2);
205
+ }
160
206
  if (area === "cloud" && action === "status") {
161
207
  return output(parsed, await cloudGet("/healthz"), renderCloudStatus);
162
208
  }
@@ -217,6 +263,17 @@ async function main(argv) {
217
263
  return output(parsed, await cloudGet(`/v1/board-registry/templates/${encodeURIComponent(idResult.value)}/methods`), renderBoardMethods);
218
264
  }
219
265
  if (action === "knowledge") {
266
+ if (parsed.command[2] === "search") {
267
+ const request = boardKnowledgeSearchRequest(parsed);
268
+ if (typeof request === "string") {
269
+ return output(parsed, fail("invalid_args", request), undefined, 2);
270
+ }
271
+ const params = new URLSearchParams({ q: request.query, limit: String(request.limit) });
272
+ if (request.source) {
273
+ params.set("source", request.source);
274
+ }
275
+ return output(parsed, await cloudGet(`/v1/board-registry/templates/${encodeURIComponent(request.templateId)}/knowledge-search?${params.toString()}`), renderBoardKnowledgeSearch);
276
+ }
220
277
  if (parsed.command[2] === "file") {
221
278
  const request = boardKnowledgeFileRequest(parsed);
222
279
  if (typeof request === "string") {
@@ -242,6 +299,7 @@ async function main(argv) {
242
299
  BOARD_REGISTRY_SHOW_USAGE,
243
300
  BOARD_METHODS_USAGE,
244
301
  BOARD_KNOWLEDGE_USAGE,
302
+ BOARD_KNOWLEDGE_SEARCH_USAGE,
245
303
  BOARD_KNOWLEDGE_FILE_USAGE
246
304
  ].join("\n")), undefined, 2);
247
305
  }
@@ -280,7 +338,18 @@ async function main(argv) {
280
338
  const result = await pluginInstall(parsed);
281
339
  return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
282
340
  }
283
- return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE].join("\n")), undefined, 2);
341
+ if (action === "update") {
342
+ if (parsed.command[2] === "check") {
343
+ const result = await pluginUpdateCheck(parsed);
344
+ return output(parsed, result, renderPluginUpdateCheck, result.ok ? 0 : 2);
345
+ }
346
+ if (["codex", "opencode", "trae", "all"].includes(parsed.command[2] ?? "")) {
347
+ const result = await pluginUpdate(parsed);
348
+ return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
349
+ }
350
+ return output(parsed, fail("invalid_args", [PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
351
+ }
352
+ return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE, PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
284
353
  }
285
354
  if (area === "auth" && action === "login") {
286
355
  const result = await authLogin(parsed);
@@ -289,6 +358,31 @@ async function main(argv) {
289
358
  if (area === "auth" && action === "status") {
290
359
  return output(parsed, ok(await authStatus()), renderAuthStatus);
291
360
  }
361
+ if (area === "auth" && action === "device") {
362
+ const deviceAction = parsed.command[2] ?? "status";
363
+ if (deviceAction === "status") {
364
+ const result = await authDeviceStatus(parsed);
365
+ return output(parsed, result, renderAuthDeviceStatus, result.ok ? 0 : 2);
366
+ }
367
+ if (deviceAction === "list") {
368
+ const result = await authDeviceList(parsed);
369
+ return output(parsed, result, renderAuthDeviceList, result.ok ? 0 : 2);
370
+ }
371
+ if (deviceAction === "revoke") {
372
+ const result = await authDeviceRevoke(parsed);
373
+ return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
374
+ }
375
+ if (deviceAction === "rename") {
376
+ const result = await authDeviceRename(parsed);
377
+ return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
378
+ }
379
+ return output(parsed, fail("invalid_args", [
380
+ AUTH_DEVICE_STATUS_USAGE,
381
+ AUTH_DEVICE_LIST_USAGE,
382
+ AUTH_DEVICE_REVOKE_USAGE,
383
+ AUTH_DEVICE_RENAME_USAGE
384
+ ].join("\n")), undefined, 2);
385
+ }
292
386
  if (area === "auth" && action === "logout") {
293
387
  await rm(DEFAULT_AUTH_FILE, { force: true });
294
388
  return output(parsed, ok(await authStatus()), renderAuthStatus);
@@ -340,6 +434,9 @@ async function main(argv) {
340
434
  ACCOUNT_KEY_REVOKE_USAGE
341
435
  ].join("\n")), undefined, 2);
342
436
  }
437
+ if (area === "usage" || area === "billing") {
438
+ return output(parsed, quotaAndBillingDisabled(), undefined, 2);
439
+ }
343
440
  if (area === "usage") {
344
441
  if (action === "record") {
345
442
  const body = usageRecordBody(parsed);
@@ -517,6 +614,20 @@ async function main(argv) {
517
614
  return output(parsed, fail("invalid_args", [IMAGE_BOOT_LOGO_COMPOSE_USAGE, IMAGE_DTB_COMPOSE_USAGE].join("\n")), undefined, 2);
518
615
  }
519
616
  if (area === "local") {
617
+ if (action === "toolchain" && parsed.command[2] === "list") {
618
+ const request = localToolchainListRequest(parsed);
619
+ if (typeof request === "string") {
620
+ return output(parsed, fail("invalid_args", request), undefined, 2);
621
+ }
622
+ return output(parsed, ok(await listLocalToolchainEnvironments(request)), renderLocalToolchainList);
623
+ }
624
+ if (action === "toolchain" && parsed.command[2] === "installed") {
625
+ const request = localToolchainListRequest(parsed, LOCAL_TOOLCHAIN_INSTALLED_USAGE);
626
+ if (typeof request === "string") {
627
+ return output(parsed, fail("invalid_args", request), undefined, 2);
628
+ }
629
+ return output(parsed, ok(await listLocalToolchainEnvironments({ ...request, installedOnly: true })), renderLocalToolchainList);
630
+ }
520
631
  if (action === "toolchain" && parsed.command[2] === "latest") {
521
632
  const request = localToolchainLatestRequest(parsed);
522
633
  if (typeof request === "string") {
@@ -538,12 +649,34 @@ async function main(argv) {
538
649
  }
539
650
  return output(parsed, ok(await installLocalToolchain(request)), renderLocalToolchainInstall);
540
651
  }
652
+ if (action === "toolchain" && (parsed.command[2] === "uninstall" || parsed.command[2] === "remove")) {
653
+ const request = localToolchainUninstallRequest(parsed);
654
+ if (typeof request === "string") {
655
+ return output(parsed, fail("invalid_args", request), undefined, 2);
656
+ }
657
+ return output(parsed, ok(await uninstallLocalToolchain(request)), renderLocalToolchainUninstall);
658
+ }
541
659
  if (action === "toolchain" && parsed.command[2] === "validate") {
542
660
  const request = localToolchainValidateRequest(parsed);
543
661
  if (typeof request === "string") {
544
662
  return output(parsed, fail("invalid_args", request), undefined, 2);
545
663
  }
546
- return output(parsed, ok(await validateLocalToolchain(request.releaseRoot)), renderLocalToolchainValidation);
664
+ return output(parsed, ok(await validateLocalToolchain(request)), renderLocalToolchainValidation);
665
+ }
666
+ if (action === "wsl" && parsed.command[2] === "status") {
667
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
668
+ const extra = parsed.command.slice(3);
669
+ if (unknownFlag || extra.length > 0) {
670
+ return output(parsed, fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${LOCAL_WSL_STATUS_USAGE}` : LOCAL_WSL_STATUS_USAGE), undefined, 2);
671
+ }
672
+ return output(parsed, ok(await windowsWslStatus()), renderWindowsWslStatus);
673
+ }
674
+ if (action === "wsl" && parsed.command[2] === "install") {
675
+ const request = localWslInstallRequest(parsed);
676
+ if (typeof request === "string") {
677
+ return output(parsed, fail("invalid_args", request), undefined, 2);
678
+ }
679
+ return output(parsed, ok(await windowsWslInstall(request)), renderWindowsWslInstall);
547
680
  }
548
681
  if (action === "compile" && parsed.command[2] === "taishanpi") {
549
682
  const request = localCompileTaishanPiRequest(parsed, await authStatus());
@@ -560,10 +693,15 @@ async function main(argv) {
560
693
  return output(parsed, ok(await buildTaishanPiQtSmoke(request)), renderLocalCompileResult);
561
694
  }
562
695
  return output(parsed, fail("invalid_args", [
696
+ LOCAL_TOOLCHAIN_LIST_USAGE,
697
+ LOCAL_TOOLCHAIN_INSTALLED_USAGE,
563
698
  LOCAL_TOOLCHAIN_LATEST_USAGE,
564
699
  LOCAL_TOOLCHAIN_CURRENT_USAGE,
565
700
  LOCAL_TOOLCHAIN_INSTALL_USAGE,
701
+ LOCAL_TOOLCHAIN_UNINSTALL_USAGE,
566
702
  LOCAL_TOOLCHAIN_VALIDATE_USAGE,
703
+ LOCAL_WSL_STATUS_USAGE,
704
+ LOCAL_WSL_INSTALL_USAGE,
567
705
  LOCAL_COMPILE_TAISHANPI_USAGE,
568
706
  LOCAL_BUILD_QT_SMOKE_USAGE
569
707
  ].join("\n")), undefined, 2);
@@ -995,10 +1133,22 @@ async function main(argv) {
995
1133
  return output(parsed, fail("unknown_command", `Unknown command: ${parsed.command.join(" ")}`), undefined, 2);
996
1134
  }
997
1135
  catch (error) {
998
- return output(parsed, fail("command_failed", error instanceof Error ? error.message : String(error), {
999
- remediation: `Check that embed-local-bridge is running at ${DEFAULT_BRIDGE_URL}. Start it with: embed bridge start`
1000
- }), undefined, 1);
1136
+ const message = error instanceof Error ? error.message : String(error);
1137
+ const remediation = commandFailureRemediation(message);
1138
+ return output(parsed, fail("command_failed", message, remediation ? { remediation } : undefined), undefined, 1);
1139
+ }
1140
+ }
1141
+ function commandFailureRemediation(message) {
1142
+ if (message.includes("TaishanPi") && message.includes("WSL2")) {
1143
+ return "Run: embedlabs local wsl status";
1001
1144
  }
1145
+ if (message.includes(DEFAULT_BRIDGE_URL) || message.includes("embed-local-bridge") || message.includes("bridge_unavailable")) {
1146
+ return `Check that embed-local-bridge is running at ${DEFAULT_BRIDGE_URL}. Start it with: embed bridge start`;
1147
+ }
1148
+ if (message.includes("Local toolchain download")) {
1149
+ return "Check the download URL and network connection, then retry. Partial downloads are resumed automatically.";
1150
+ }
1151
+ return undefined;
1002
1152
  }
1003
1153
  function parseArgs(argv) {
1004
1154
  const command = [];
@@ -1109,6 +1259,7 @@ function flashBody(parsed, includeApproval) {
1109
1259
  variant_id: stringFlag(parsed, "variant"),
1110
1260
  hardware_profile_id: stringFlag(parsed, "hardware-profile"),
1111
1261
  profile_id: stringFlag(parsed, "profile"),
1262
+ local_device_id: stringFlag(parsed, "local-device-id") ?? stringFlag(parsed, "device-id"),
1112
1263
  image_dir: stringFlag(parsed, "image-dir"),
1113
1264
  artifact_path: stringFlag(parsed, "artifact"),
1114
1265
  target_volume_path: stringFlag(parsed, "target-volume")
@@ -1137,11 +1288,11 @@ async function doctor() {
1137
1288
  const bridgeHealth = await apiDoctorCheck("bridge_health", "Local Bridge health", `${bridgeBaseUrl}/healthz`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderHealthSummary, healthStatus);
1138
1289
  checks.push(bridgeHealth);
1139
1290
  if (isUsableDoctorCheck(bridgeHealth)) {
1140
- checks.push(await apiDoctorCheck("device_scan", "Device scan", `${bridgeBaseUrl}/v1/devices`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDeviceScanSummary, warningIfWarnings));
1291
+ checks.push(await apiDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDeviceScanSummary, warningIfWarnings));
1141
1292
  checks.push(await apiDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDebugToolScanSummary, warningIfWarnings));
1142
1293
  }
1143
1294
  else {
1144
- checks.push(dependentDoctorCheck("device_scan", "Device scan", `${bridgeBaseUrl}/v1/devices`, "Device scan requires a reachable Local Bridge."));
1295
+ checks.push(dependentDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "Device inventory requires a reachable Local Bridge."));
1145
1296
  checks.push(dependentDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "Debug tool scan requires a reachable Local Bridge."));
1146
1297
  }
1147
1298
  checks.push(await apiDoctorCheck("cloud_api_health", "Cloud API health", `${cloudBaseUrl}/healthz`, "cloud_api_unreachable", `Cloud API is unreachable at ${cloudBaseUrl}.`, renderHealthSummary, healthStatus));
@@ -1200,7 +1351,8 @@ function authDoctorCheck(status) {
1200
1351
  : {
1201
1352
  code: "auth_not_ready",
1202
1353
  message: "No CLI auth token is configured.",
1203
- remediation: "Run: embed auth login --token <token>"
1354
+ remediation: cloudAuthSetupRemediation(),
1355
+ details: cloudAuthSetupDetails()
1204
1356
  }
1205
1357
  };
1206
1358
  }
@@ -1333,7 +1485,7 @@ function warningIfWarnings(data) {
1333
1485
  }
1334
1486
  function renderDeviceScanSummary(result) {
1335
1487
  const warningText = result.warnings?.length ? ` ${result.warnings.length} warning(s).` : "";
1336
- return `Device scan completed: ${result.devices.length} device(s), ${result.usb.length} USB item(s), ${result.serial.length} serial port(s).${warningText}`;
1488
+ return `Device inventory snapshot: ${result.devices.length} device(s), ${result.usb.length} USB item(s), ${result.serial.length} serial port(s).${warningText}`;
1337
1489
  }
1338
1490
  function renderDebugToolScanSummary(result) {
1339
1491
  const available = result.tools.filter((tool) => tool.available).length;
@@ -1351,16 +1503,142 @@ function isApiResponse(value) {
1351
1503
  return isJsonObject(error) && typeof error.code === "string" && typeof error.message === "string";
1352
1504
  }
1353
1505
  async function bridgeGet(path) {
1354
- const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`);
1355
- return await response.json();
1506
+ return await bridgeRequest("GET", path);
1356
1507
  }
1357
1508
  async function bridgePost(path, body) {
1358
- const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
1359
- method: "POST",
1360
- headers: { "content-type": "application/json" },
1361
- body: JSON.stringify(body)
1509
+ return await bridgeRequest("POST", path, body);
1510
+ }
1511
+ async function bridgeRequest(method, path, body) {
1512
+ const bodyText = body === undefined ? "" : JSON.stringify(body);
1513
+ const makeRequest = async () => {
1514
+ const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
1515
+ method,
1516
+ headers: bridgeHeaders(method, path, method === "POST" ? bodyText : "", method === "POST" ? { "content-type": "application/json" } : {}),
1517
+ body: method === "POST" ? bodyText : undefined
1518
+ });
1519
+ return await response.json();
1520
+ };
1521
+ try {
1522
+ return await makeRequest();
1523
+ }
1524
+ catch (error) {
1525
+ if (!isBridgeConnectionFailure(error)) {
1526
+ throw error;
1527
+ }
1528
+ const started = await ensureBridgeStartedForRequest();
1529
+ if (!started.ok) {
1530
+ return started;
1531
+ }
1532
+ return await makeRequest();
1533
+ }
1534
+ }
1535
+ function bridgeHeaders(method, path, bodyText, base = {}) {
1536
+ const token = process.env.EMBED_BRIDGE_TOKEN?.trim();
1537
+ if (!token) {
1538
+ return base;
1539
+ }
1540
+ const headers = {
1541
+ ...base,
1542
+ authorization: `Bearer ${token}`
1543
+ };
1544
+ addBridgeRequestSignature(headers, method, path, bodyText, token);
1545
+ return headers;
1546
+ }
1547
+ function addBridgeRequestSignature(headers, method, pathWithQuery, bodyText, token) {
1548
+ if (process.env.EMBED_BRIDGE_SIGNING === "0") {
1549
+ return;
1550
+ }
1551
+ const timestamp = String(Math.floor(Date.now() / 1000));
1552
+ const nonce = randomBytes(16).toString("hex");
1553
+ const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
1554
+ const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
1555
+ const canonical = cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1556
+ headers["x-embed-key-id"] = keyId;
1557
+ headers["x-embed-timestamp"] = timestamp;
1558
+ headers["x-embed-nonce"] = nonce;
1559
+ headers["x-embed-body-sha256"] = bodySha256;
1560
+ headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
1561
+ }
1562
+ function isBridgeConnectionFailure(error) {
1563
+ const message = error instanceof Error ? error.message : String(error);
1564
+ return message.includes("fetch failed") ||
1565
+ message.includes("ECONNREFUSED") ||
1566
+ message.includes("ECONNRESET") ||
1567
+ message.includes("UND_ERR_SOCKET");
1568
+ }
1569
+ async function ensureBridgeStartedForRequest() {
1570
+ if (process.env.EMBED_BRIDGE_AUTO_START === "0") {
1571
+ return fail("bridge_unavailable", `embed-local-bridge is not running at ${DEFAULT_BRIDGE_URL}.`, {
1572
+ remediation: `Start it with: embed bridge start`
1573
+ });
1574
+ }
1575
+ let bridgeURL;
1576
+ try {
1577
+ bridgeURL = new URL(DEFAULT_BRIDGE_URL);
1578
+ }
1579
+ catch {
1580
+ return fail("bridge_url_invalid", `EMBED_BRIDGE_URL is not a valid URL: ${DEFAULT_BRIDGE_URL}`);
1581
+ }
1582
+ if (!isLocalBridgeURL(bridgeURL)) {
1583
+ return fail("bridge_unavailable", `embed-local-bridge is not reachable at ${DEFAULT_BRIDGE_URL}.`, {
1584
+ remediation: `Start the bridge for that host, or set EMBED_BRIDGE_URL to a local bridge URL.`
1585
+ });
1586
+ }
1587
+ const launcher = await resolveBridgeLauncher();
1588
+ const host = bridgeURL.hostname === "::1" ? "::1" : bridgeURL.hostname || "127.0.0.1";
1589
+ const port = bridgeURL.port || "18083";
1590
+ const env = {
1591
+ ...process.env,
1592
+ EMBED_BRIDGE_HOST: host,
1593
+ EMBED_BRIDGE_PORT: port
1594
+ };
1595
+ const child = spawn(launcher.command, [...launcher.args, "--host", host, "--port", port], {
1596
+ cwd: process.cwd(),
1597
+ detached: true,
1598
+ stdio: "ignore",
1599
+ env
1600
+ });
1601
+ child.unref();
1602
+ const ready = await waitForBridgeHealth(bridgeURL, 8000);
1603
+ if (!ready.ok) {
1604
+ return ready;
1605
+ }
1606
+ return ok({
1607
+ started: true,
1608
+ bridge_url: DEFAULT_BRIDGE_URL,
1609
+ command: launcher.command
1362
1610
  });
1363
- return await response.json();
1611
+ }
1612
+ function isLocalBridgeURL(url) {
1613
+ const host = url.hostname.toLowerCase();
1614
+ return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
1615
+ }
1616
+ async function waitForBridgeHealth(bridgeURL, timeoutMs) {
1617
+ const deadline = Date.now() + timeoutMs;
1618
+ let lastError = "";
1619
+ while (Date.now() < deadline) {
1620
+ try {
1621
+ const response = await fetch(new URL("/healthz", bridgeURL), {
1622
+ headers: bridgeHeaders("GET", "/healthz", "")
1623
+ });
1624
+ const parsed = await response.json();
1625
+ if (parsed.ok) {
1626
+ return parsed;
1627
+ }
1628
+ lastError = parsed.error?.message ?? `HTTP ${response.status}`;
1629
+ }
1630
+ catch (error) {
1631
+ lastError = error instanceof Error ? error.message : String(error);
1632
+ }
1633
+ await delay(100);
1634
+ }
1635
+ return fail("bridge_start_failed", `embed-local-bridge did not become healthy at ${DEFAULT_BRIDGE_URL}.`, {
1636
+ remediation: `Run embed bridge start in a separate terminal and retry.`,
1637
+ details: { last_error: lastError }
1638
+ });
1639
+ }
1640
+ function delay(ms) {
1641
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
1364
1642
  }
1365
1643
  async function cloudGet(path) {
1366
1644
  return await cloudRequest("GET", path);
@@ -1371,16 +1649,23 @@ async function cloudPost(path, body) {
1371
1649
  async function cloudDownloadArtifact(artifactId, outputPath) {
1372
1650
  try {
1373
1651
  const headers = {};
1374
- const token = await cloudAuthToken();
1375
- if (token) {
1376
- headers.authorization = `Bearer ${token}`;
1652
+ const auth = await cloudAuthConfig();
1653
+ if (auth.token) {
1654
+ if (auth.device) {
1655
+ const integrity = await validateLocalDeviceIntegrity(auth.device);
1656
+ if (!integrity.ok) {
1657
+ return integrity;
1658
+ }
1659
+ }
1660
+ headers.authorization = `Bearer ${auth.token}`;
1661
+ addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", auth.token, auth.device);
1377
1662
  }
1378
1663
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/artifacts/${encodeURIComponent(artifactId)}/download`, {
1379
1664
  headers: Object.keys(headers).length > 0 ? headers : undefined
1380
1665
  });
1381
1666
  if (!response.ok) {
1382
1667
  const parsed = await parseErrorResponse(response);
1383
- return parsed ?? fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
1668
+ return parsed ? enrichCloudAuthFailure(parsed, Boolean(auth.token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
1384
1669
  }
1385
1670
  const bytes = Buffer.from(await response.arrayBuffer());
1386
1671
  const expectedSha256 = response.headers.get("x-embed-artifact-sha256")?.trim();
@@ -1409,19 +1694,28 @@ async function cloudDownloadArtifact(artifactId, outputPath) {
1409
1694
  async function cloudRequest(method, path, body) {
1410
1695
  try {
1411
1696
  const headers = {};
1412
- if (body !== undefined) {
1697
+ const bodyText = body === undefined ? "" : JSON.stringify(body);
1698
+ if (bodyText) {
1413
1699
  headers["content-type"] = "application/json";
1414
1700
  }
1415
- const token = await cloudAuthToken();
1416
- if (token) {
1417
- headers.authorization = `Bearer ${token}`;
1701
+ const auth = await cloudAuthConfig();
1702
+ if (auth.token) {
1703
+ if (auth.device) {
1704
+ const integrity = await validateLocalDeviceIntegrity(auth.device);
1705
+ if (!integrity.ok) {
1706
+ return integrity;
1707
+ }
1708
+ }
1709
+ headers.authorization = `Bearer ${auth.token}`;
1710
+ addCloudRequestSignature(headers, method, path, bodyText, auth.token, auth.device);
1418
1711
  }
1419
1712
  const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}${path}`, {
1420
1713
  method,
1421
1714
  headers: Object.keys(headers).length > 0 ? headers : undefined,
1422
- body: body === undefined ? undefined : JSON.stringify(body)
1715
+ body: body === undefined ? undefined : bodyText
1423
1716
  });
1424
- return await response.json();
1717
+ const parsed = await response.json();
1718
+ return enrichCloudAuthFailure(parsed, Boolean(auth.token));
1425
1719
  }
1426
1720
  catch (error) {
1427
1721
  return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
@@ -1429,6 +1723,129 @@ async function cloudRequest(method, path, body) {
1429
1723
  });
1430
1724
  }
1431
1725
  }
1726
+ async function validateLocalDeviceIntegrity(device) {
1727
+ const current = await localHardwareFingerprint();
1728
+ if (current.fingerprint_hash === device.fingerprint_hash) {
1729
+ return ok(undefined);
1730
+ }
1731
+ return fail("tool_integrity_check_failed", TOOL_INTEGRITY_RELOGIN_MESSAGE, {
1732
+ remediation: [
1733
+ "当前 Embed Labs CLI/插件配置绑定的电脑与本机硬件唯一码不一致。",
1734
+ TOOL_INTEGRITY_RELOGIN_MESSAGE,
1735
+ "如果账号设备数量已达上限,请先在原电脑或用户后台撤销旧设备。"
1736
+ ].join("\n"),
1737
+ details: {
1738
+ expected_fingerprint_hash: device.fingerprint_hash,
1739
+ current_fingerprint_hash: current.fingerprint_hash,
1740
+ platform: current.platform,
1741
+ arch: current.arch,
1742
+ fingerprint_source: current.source
1743
+ }
1744
+ });
1745
+ }
1746
+ function enrichCloudAuthFailure(response, hadToken) {
1747
+ if (response.ok) {
1748
+ return response;
1749
+ }
1750
+ if (response.error.code.startsWith("device_") || response.error.code.startsWith("request_signature_")) {
1751
+ return fail(response.error.code, response.error.message, {
1752
+ remediation: [
1753
+ "This computer is not fully registered for the configured Embed Labs API Token.",
1754
+ "Run: embedlabs auth login --token <your_token>",
1755
+ "Then verify with: embedlabs auth device status",
1756
+ "If the account already has too many devices, revoke one with: embedlabs auth device revoke <device_id>"
1757
+ ].join("\n"),
1758
+ details: response.error.details
1759
+ });
1760
+ }
1761
+ if (response.error.code !== "unauthorized") {
1762
+ return response;
1763
+ }
1764
+ if (!hadToken) {
1765
+ 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.", {
1766
+ remediation: cloudAuthSetupRemediation(),
1767
+ details: cloudAuthSetupDetails()
1768
+ });
1769
+ }
1770
+ 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.", {
1771
+ remediation: cloudAuthSetupRemediation(),
1772
+ details: cloudAuthSetupDetails()
1773
+ });
1774
+ }
1775
+ function cloudAuthSetupRemediation() {
1776
+ return [
1777
+ `1. Open ${DEFAULT_DASHBOARD_URL} and register or sign in.`,
1778
+ "2. Create or copy your Embed Labs API Token from the user dashboard.",
1779
+ "3. Run: embedlabs auth login --token <your_token>",
1780
+ "4. For automation, set: EMBED_API_TOKEN=<your_token>",
1781
+ "5. Verify with: embedlabs auth status"
1782
+ ].join("\n");
1783
+ }
1784
+ function cloudAuthSetupDetails() {
1785
+ return {
1786
+ dashboard_url: DEFAULT_DASHBOARD_URL,
1787
+ login_command: "embedlabs auth login --token <your_token>",
1788
+ env_var: "EMBED_API_TOKEN",
1789
+ auth_status_command: "embedlabs auth status",
1790
+ auth_file: DEFAULT_AUTH_FILE
1791
+ };
1792
+ }
1793
+ function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token, device) {
1794
+ if (process.env.EMBED_CLOUD_API_SIGNING === "0") {
1795
+ return;
1796
+ }
1797
+ const timestamp = String(Math.floor(Date.now() / 1000));
1798
+ const nonce = randomBytes(16).toString("hex");
1799
+ const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
1800
+ const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
1801
+ const canonical = device
1802
+ ? cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, EMBED_CLIENT_NAME, EMBED_CLIENT_VERSION)
1803
+ : cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
1804
+ headers["x-embed-key-id"] = keyId;
1805
+ headers["x-embed-timestamp"] = timestamp;
1806
+ headers["x-embed-nonce"] = nonce;
1807
+ headers["x-embed-body-sha256"] = bodySha256;
1808
+ if (device) {
1809
+ headers["x-embed-signature-version"] = "v2";
1810
+ headers["x-embed-device-id"] = device.device_id;
1811
+ headers["x-embed-device-fingerprint-sha256"] = device.fingerprint_hash;
1812
+ headers["x-embed-client-name"] = EMBED_CLIENT_NAME;
1813
+ headers["x-embed-client-version"] = EMBED_CLIENT_VERSION;
1814
+ headers["x-embed-device-signature"] = signCrypto(null, Buffer.from(canonical), device.private_key_pem).toString("base64url");
1815
+ }
1816
+ headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
1817
+ }
1818
+ function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256) {
1819
+ return [
1820
+ method.toUpperCase(),
1821
+ normalizeCloudPathForSignature(pathWithQuery),
1822
+ timestamp,
1823
+ nonce,
1824
+ bodySha256
1825
+ ].join("\n");
1826
+ }
1827
+ function cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, clientName, clientVersion) {
1828
+ return [
1829
+ method.toUpperCase(),
1830
+ normalizeCloudPathForSignature(pathWithQuery),
1831
+ timestamp,
1832
+ nonce,
1833
+ bodySha256,
1834
+ device.device_id,
1835
+ device.fingerprint_hash,
1836
+ clientName,
1837
+ clientVersion
1838
+ ].join("\n");
1839
+ }
1840
+ function normalizeCloudPathForSignature(pathWithQuery) {
1841
+ try {
1842
+ const parsed = new URL(pathWithQuery, "http://embed.local");
1843
+ return `${parsed.pathname}${parsed.search}`;
1844
+ }
1845
+ catch {
1846
+ return pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`;
1847
+ }
1848
+ }
1432
1849
  async function pluginList(parsed) {
1433
1850
  const releaseDir = stringFlag(parsed, "release-dir");
1434
1851
  const manifest = releaseDir ? await readPluginReleaseManifest(releaseDir) : undefined;
@@ -1464,17 +1881,25 @@ async function pluginList(parsed) {
1464
1881
  release_file: effectiveOpenCodePackage?.file,
1465
1882
  default_target: defaultOpenCodeRoot(),
1466
1883
  install_command: "embed plugin install opencode"
1884
+ },
1885
+ {
1886
+ id: "trae",
1887
+ display_name: "Embed Labs Trae MCP integration",
1888
+ source: "source_checkout",
1889
+ version: effectiveManifest?.version ?? await localPluginVersion("codex"),
1890
+ default_target: defaultTraeUserRoot(),
1891
+ install_command: "embed plugin install trae"
1467
1892
  }
1468
1893
  ]
1469
1894
  });
1470
1895
  }
1471
1896
  async function pluginInstall(parsed) {
1472
- const unknownFlag = firstUnknownFlag(parsed, ["release-dir", "release-url", "target", "codex-target", "opencode-target", "force", "json"]);
1897
+ const unknownFlag = firstUnknownFlag(parsed, ["release-dir", "release-url", "target", "codex-target", "opencode-target", "trae-target", "force", "json"]);
1473
1898
  if (unknownFlag) {
1474
1899
  return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_INSTALL_USAGE}`);
1475
1900
  }
1476
1901
  const target = parsed.command[2];
1477
- if (!target || !["codex", "opencode", "all"].includes(target)) {
1902
+ if (!target || !["codex", "opencode", "trae", "all"].includes(target)) {
1478
1903
  return fail("invalid_args", PLUGIN_INSTALL_USAGE);
1479
1904
  }
1480
1905
  const unexpected = parsed.command.slice(3);
@@ -1522,19 +1947,166 @@ async function pluginInstall(parsed) {
1522
1947
  }
1523
1948
  installed.push(installedOpenCode.data);
1524
1949
  }
1950
+ if (target === "trae" || target === "all") {
1951
+ const installedTrae = await installTraeMcpIntegration(parsed, installingAll);
1952
+ if (!installedTrae.ok) {
1953
+ return installedTrae;
1954
+ }
1955
+ installed.push(installedTrae.data);
1956
+ }
1525
1957
  return ok({ installed });
1526
1958
  }
1527
1959
  finally {
1528
1960
  await rm(tempDir, { recursive: true, force: true });
1529
1961
  }
1530
1962
  }
1963
+ async function pluginUpdateCheck(parsed) {
1964
+ const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "trae-target", "json"]);
1965
+ if (unknownFlag) {
1966
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
1967
+ }
1968
+ const unexpected = parsed.command.slice(3);
1969
+ if (unexpected.length > 0) {
1970
+ return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
1971
+ }
1972
+ const remoteManifest = await fetchRemotePluginManifest(parsed);
1973
+ if (!remoteManifest.ok) {
1974
+ return remoteManifest;
1975
+ }
1976
+ const manifest = remoteManifest.data;
1977
+ const codexPackage = manifest.packages?.find((item) => item.id === "codex-embed-labs");
1978
+ const opencodePackage = manifest.packages?.find((item) => item.id === "opencode-embed-labs");
1979
+ const codexTarget = join(codexPluginTargetRoot(parsed, true), CODEX_PLUGIN_NAME);
1980
+ const openCodeTarget = openCodePluginTargetRoot(parsed, true);
1981
+ return ok({
1982
+ release_url: pluginReleaseBaseUrl(parsed),
1983
+ latest_version: manifest.version,
1984
+ release_notes: normalizedReleaseNotes(manifest.release_notes),
1985
+ plugins: [
1986
+ await pluginUpdateItem({
1987
+ id: "codex",
1988
+ displayName: "Embed Labs Codex plugin",
1989
+ targetPath: codexTarget,
1990
+ installedVersion: await installedCodexPluginVersion(codexTarget),
1991
+ latestVersion: codexPackage?.version ?? manifest.version,
1992
+ releaseFile: codexPackage?.file,
1993
+ updateCommand: "embedlabs plugin update codex"
1994
+ }),
1995
+ await pluginUpdateItem({
1996
+ id: "opencode",
1997
+ displayName: "Embed Labs OpenCode plugin",
1998
+ targetPath: openCodeTarget,
1999
+ installedVersion: await installedOpenCodePluginVersion(openCodeTarget),
2000
+ latestVersion: opencodePackage?.version ?? manifest.version,
2001
+ releaseFile: opencodePackage?.file,
2002
+ updateCommand: "embedlabs plugin update opencode"
2003
+ }),
2004
+ await pluginUpdateItem({
2005
+ id: "trae",
2006
+ displayName: "Embed Labs Trae MCP integration",
2007
+ targetPath: defaultTraeUserRoot(),
2008
+ installedVersion: undefined,
2009
+ latestVersion: manifest.version,
2010
+ updateCommand: "embedlabs plugin update trae"
2011
+ })
2012
+ ]
2013
+ });
2014
+ }
2015
+ function normalizedReleaseNotes(notes) {
2016
+ if (!Array.isArray(notes)) {
2017
+ return [];
2018
+ }
2019
+ return notes
2020
+ .filter((item) => typeof item === "string" && item.trim().length > 0)
2021
+ .map((item) => item.trim());
2022
+ }
2023
+ async function pluginUpdate(parsed) {
2024
+ const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "trae-target", "json"]);
2025
+ if (unknownFlag) {
2026
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_USAGE}`);
2027
+ }
2028
+ const target = parsed.command[2];
2029
+ if (!target || !["codex", "opencode", "trae", "all"].includes(target)) {
2030
+ return fail("invalid_args", PLUGIN_UPDATE_USAGE);
2031
+ }
2032
+ const unexpected = parsed.command.slice(3);
2033
+ if (unexpected.length > 0) {
2034
+ return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_USAGE}`);
2035
+ }
2036
+ const installParsed = {
2037
+ ...parsed,
2038
+ command: ["plugin", "install", target],
2039
+ flags: { ...parsed.flags, force: true }
2040
+ };
2041
+ return await pluginInstall(installParsed);
2042
+ }
2043
+ async function pluginUpdateItem(input) {
2044
+ const installed = !!input.installedVersion;
2045
+ const versionOrder = input.installedVersion && input.latestVersion
2046
+ ? comparePluginVersionLike(input.installedVersion, input.latestVersion)
2047
+ : 0;
2048
+ const updateAvailable = installed && !!input.latestVersion && versionOrder < 0;
2049
+ const updateStatus = !installed
2050
+ ? "not_installed"
2051
+ : updateAvailable
2052
+ ? "update_available"
2053
+ : versionOrder > 0
2054
+ ? "ahead_of_channel"
2055
+ : "up_to_date";
2056
+ const notes = [];
2057
+ if (!installed) {
2058
+ notes.push("Plugin is not installed in the selected target.");
2059
+ }
2060
+ else if (updateAvailable) {
2061
+ notes.push("A newer plugin release is available. Run the update command, then restart Codex/OpenCode.");
2062
+ }
2063
+ else if (versionOrder > 0) {
2064
+ notes.push("Installed plugin is newer than the selected release channel; no update is needed.");
2065
+ }
2066
+ else {
2067
+ notes.push("Plugin is up to date for the selected release channel.");
2068
+ }
2069
+ return {
2070
+ id: input.id,
2071
+ display_name: input.displayName,
2072
+ installed,
2073
+ installed_version: input.installedVersion,
2074
+ latest_version: input.latestVersion,
2075
+ update_available: updateAvailable,
2076
+ update_status: updateStatus,
2077
+ target_path: input.targetPath,
2078
+ release_file: input.releaseFile,
2079
+ update_command: input.updateCommand,
2080
+ notes
2081
+ };
2082
+ }
2083
+ function comparePluginVersionLike(left, right) {
2084
+ const normalize = (value) => value
2085
+ .trim()
2086
+ .replace(/^v/i, "")
2087
+ .split(/[.+\-]/g)
2088
+ .map((part) => {
2089
+ const parsed = Number.parseInt(part, 10);
2090
+ return Number.isFinite(parsed) ? parsed : 0;
2091
+ });
2092
+ const a = normalize(left);
2093
+ const b = normalize(right);
2094
+ const length = Math.max(a.length, b.length, 3);
2095
+ for (let index = 0; index < length; index += 1) {
2096
+ const delta = (a[index] ?? 0) - (b[index] ?? 0);
2097
+ if (delta !== 0)
2098
+ return delta;
2099
+ }
2100
+ return 0;
2101
+ }
1531
2102
  async function installCodexPlugin(parsed, context) {
1532
2103
  const source = await resolveCodexPluginSource(context);
1533
2104
  if (!source.ok) {
1534
2105
  return source;
1535
2106
  }
1536
2107
  const targetRoot = codexPluginTargetRoot(parsed, context.installingAll);
1537
- const targetPath = join(targetRoot, "embed-labs");
2108
+ const targetPath = join(targetRoot, CODEX_PLUGIN_NAME);
2109
+ const legacyCleanup = await cleanupLegacyCodexPluginRemnants(targetRoot);
1538
2110
  if (await pathExists(targetPath) && !booleanFlag(parsed, "force")) {
1539
2111
  return fail("plugin_already_installed", `Codex plugin already exists at ${targetPath}.`, {
1540
2112
  remediation: "Pass --force to replace it, or pass --codex-target/--target to install into a different directory."
@@ -1544,16 +2116,24 @@ async function installCodexPlugin(parsed, context) {
1544
2116
  await mkdir(targetRoot, { recursive: true });
1545
2117
  await cp(source.data.sourcePath, targetPath, { recursive: true });
1546
2118
  const mcpRegistration = await maybeRegisterCodexMcp(parsed, targetRoot, targetPath);
2119
+ const marketplaceRegistration = await maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath);
1547
2120
  return ok({
1548
2121
  id: "codex",
1549
2122
  target_path: targetPath,
1550
2123
  source: source.data.sourceLabel,
1551
2124
  version: source.data.version,
1552
2125
  command_hint: mcpRegistration.registered
1553
- ? "Codex MCP was registered. Start a new Codex session to reload tools."
2126
+ ? (marketplaceRegistration.registered
2127
+ ? "Codex MCP and plugin marketplace entry were registered. Fully restart Codex to reload @Embed Labs."
2128
+ : "Codex MCP was registered. Start a new Codex session to reload tools.")
1554
2129
  : mcpRegistration.hint,
2130
+ warning: legacyCodexCleanupWarning(legacyCleanup),
1555
2131
  mcp_registered: mcpRegistration.registered,
1556
- mcp_warning: mcpRegistration.warning
2132
+ mcp_warning: mcpRegistration.warning,
2133
+ marketplace_registered: marketplaceRegistration.registered,
2134
+ marketplace_path: marketplaceRegistration.marketplacePath,
2135
+ marketplace_warning: marketplaceRegistration.warning,
2136
+ cleanup: legacyCleanup
1557
2137
  });
1558
2138
  }
1559
2139
  async function installOpenCodePlugin(parsed, context) {
@@ -1562,15 +2142,17 @@ async function installOpenCodePlugin(parsed, context) {
1562
2142
  return source;
1563
2143
  }
1564
2144
  const targetRoot = openCodePluginTargetRoot(parsed, context.installingAll);
1565
- const wrapperPath = join(targetRoot, "plugins", "development-board-toolchain.js");
1566
- if (await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
2145
+ const globalInstall = isGlobalOpenCodeRoot(targetRoot);
2146
+ const wrapperPath = join(targetRoot, "plugins", "embed-labs.js");
2147
+ const legacyCleanup = await cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall);
2148
+ if (!globalInstall && await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
1567
2149
  return fail("plugin_already_installed", `OpenCode plugin wrapper already exists at ${wrapperPath}.`, {
1568
2150
  remediation: "Pass --force to replace it, or pass --opencode-target/--target to install into a different directory."
1569
2151
  });
1570
2152
  }
1571
2153
  await mkdir(join(targetRoot, "plugins"), { recursive: true });
1572
2154
  const packagePath = await prepareOpenCodePackageForInstall(targetRoot, source.data.packagePath);
1573
- const npmResult = await runLocalProcess("npm", [
2155
+ const npmResult = await runLocalProcess(npmCommand(), [
1574
2156
  "install",
1575
2157
  "--prefix",
1576
2158
  targetRoot,
@@ -1589,17 +2171,134 @@ async function installOpenCodePlugin(parsed, context) {
1589
2171
  });
1590
2172
  }
1591
2173
  await ensureOpenCodeInstallPackageJson(targetRoot);
1592
- await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
2174
+ if (globalInstall) {
2175
+ await rm(wrapperPath, { force: true });
2176
+ legacyCleanup.legacy_removed_config_entries?.push(...await ensureOpenCodeGlobalPluginConfig());
2177
+ }
2178
+ else {
2179
+ await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
2180
+ }
1593
2181
  const duplicateWarning = await openCodeDuplicatePluginWarning(targetRoot);
2182
+ const cleanupWarning = legacyOpenCodeCleanupWarning(legacyCleanup);
1594
2183
  return ok({
1595
2184
  id: "opencode",
1596
2185
  target_path: targetRoot,
1597
2186
  source: source.data.sourceLabel,
1598
2187
  version: source.data.version,
1599
- command_hint: "Start OpenCode from the project containing this .opencode directory.",
1600
- warning: duplicateWarning
2188
+ command_hint: globalInstall
2189
+ ? "Restart OpenCode so the global embed-labs package plugin is reloaded."
2190
+ : "Start OpenCode from the project containing this .opencode directory.",
2191
+ warning: combineWarnings(cleanupWarning, duplicateWarning),
2192
+ cleanup: legacyCleanup
2193
+ });
2194
+ }
2195
+ async function installTraeMcpIntegration(parsed, installingAll) {
2196
+ const targetRoot = traePluginTargetRoot(parsed, installingAll);
2197
+ const configPath = join(targetRoot, "mcp.json");
2198
+ const cacheRoot = join(targetRoot, "globalStorage", ".mcp_gallery_cache");
2199
+ const galleryId = "local.embed-labs.mcp_server";
2200
+ const iconSource = await resolveEmbedLabsIconForTrae();
2201
+ if (!iconSource) {
2202
+ return fail("trae_icon_not_found", "Embed Labs icon asset was not found for Trae MCP registration.", {
2203
+ remediation: "Reinstall the embedlabs npm package or run from a source checkout containing platform_plugins/codex_plugin/plugins/embed-labs/assets/embed-labs-icon-dark.png."
2204
+ });
2205
+ }
2206
+ const iconPath = join(cacheRoot, "embed-labs-icon-dark.png");
2207
+ await mkdir(cacheRoot, { recursive: true });
2208
+ await cp(iconSource, iconPath);
2209
+ const launcher = await resolveEmbedCliMcpLauncher(process.env.EMBED_CLI_BIN?.trim() || await resolveExecutableOnPath("embedlabs") || await resolveExecutableOnPath("embed") || "");
2210
+ const cloudUrl = pluginMcpCloudApiUrl(parsed);
2211
+ const gallery = traeGalleryMetadata(galleryId, iconPath, launcher, cloudUrl);
2212
+ const galleryPath = join(cacheRoot, `${galleryId}.json`);
2213
+ await writeFile(galleryPath, `${JSON.stringify(gallery, null, 2)}\n`, "utf8");
2214
+ await mkdir(targetRoot, { recursive: true });
2215
+ const config = await readJsonFileLenient(configPath, { mcpServers: {} });
2216
+ config.mcpServers = typeof config.mcpServers === "object" && config.mcpServers ? config.mcpServers : {};
2217
+ config.mcpServers["embed-labs"] = {
2218
+ command: launcher.command,
2219
+ args: launcher.args,
2220
+ env: {
2221
+ EMBED_CLOUD_API_URL: cloudUrl
2222
+ },
2223
+ fromGalleryId: galleryId
2224
+ };
2225
+ await writeFile(configPath, `${JSON.stringify(config, null, 4)}\n`, "utf8");
2226
+ return ok({
2227
+ id: "trae",
2228
+ target_path: configPath,
2229
+ source: "embedlabs mcp start",
2230
+ version: typeof gallery.version === "string" ? gallery.version : undefined,
2231
+ command_hint: "Restart Trae or reload MCP servers so Embed Labs appears with its MCP tools and local icon.",
2232
+ marketplace_registered: true,
2233
+ marketplace_path: galleryPath
1601
2234
  });
1602
2235
  }
2236
+ async function resolveEmbedLabsIconForTrae() {
2237
+ const candidates = [
2238
+ join(CLI_MODULE_DIR, "assets", "embed-labs-icon-dark.png"),
2239
+ sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", "assets", "embed-labs-icon-dark.png"),
2240
+ join(defaultCodexPluginRoot(), CODEX_PLUGIN_NAME, "assets", "embed-labs-icon-dark.png")
2241
+ ];
2242
+ for (const candidate of candidates) {
2243
+ try {
2244
+ await access(candidate, constants.R_OK);
2245
+ return candidate;
2246
+ }
2247
+ catch {
2248
+ // Keep looking.
2249
+ }
2250
+ }
2251
+ return undefined;
2252
+ }
2253
+ function traeGalleryMetadata(galleryId, iconPath, launcher, cloudUrl) {
2254
+ return {
2255
+ id: galleryId,
2256
+ name: "embed-labs",
2257
+ namespaceID: "local",
2258
+ displayName: "Embed Labs",
2259
+ description: "Embed Labs MCP service for local-first embedded development boards, board knowledge, SDK/toolchain metadata, Local Bridge hardware access, and account/device-bound service logging.",
2260
+ icon: traeVscodeFileUri(iconPath),
2261
+ language: "Node.js",
2262
+ license: "Proprietary",
2263
+ targetPlatforms: ["universal"],
2264
+ commands: {
2265
+ universal: {
2266
+ run: [
2267
+ {
2268
+ command: launcher.command,
2269
+ args: launcher.args,
2270
+ env: {
2271
+ EMBED_CLOUD_API_URL: cloudUrl
2272
+ },
2273
+ mcp_server_type: "stdio"
2274
+ }
2275
+ ]
2276
+ }
2277
+ },
2278
+ version: "0.2.40-local",
2279
+ repository: "https://github.com/kkwell/Embed-Labs-Cloud",
2280
+ mcpServerType: "stdio",
2281
+ categories: ["开发者工具"],
2282
+ tags: ["嵌入式开发", "MCP", "开发板", "硬件调试"],
2283
+ provider: {
2284
+ displayTextKey: "local",
2285
+ providerHomepageUrl: "https://embedboard.com/"
2286
+ },
2287
+ isCertified: false
2288
+ };
2289
+ }
2290
+ function traeVscodeFileUri(filePath) {
2291
+ const normalized = resolve(filePath).replace(/\\/g, "/");
2292
+ return `vscode-file://vscode-app${normalized.startsWith("/") ? normalized : `/${normalized}`}`;
2293
+ }
2294
+ async function readJsonFileLenient(filePath, fallback) {
2295
+ try {
2296
+ return JSON.parse(await readFile(filePath, "utf8"));
2297
+ }
2298
+ catch {
2299
+ return fallback;
2300
+ }
2301
+ }
1603
2302
  async function resolveCodexPluginSource(context) {
1604
2303
  if (context.releaseDir) {
1605
2304
  const item = context.manifest?.packages?.find((entry) => entry.id === "codex-embed-labs");
@@ -1652,7 +2351,7 @@ async function resolveOpenCodePluginSource(context) {
1652
2351
  remediation: "Run from the Embed-Labs-Cloud repo root or pass --release-dir pointing to a plugin release directory."
1653
2352
  });
1654
2353
  }
1655
- const packed = await runLocalProcess("npm", ["pack", packagePath, "--pack-destination", context.tempDir, "--json"]);
2354
+ const packed = await runLocalProcess(npmCommand(), ["pack", packagePath, "--pack-destination", context.tempDir, "--json"]);
1656
2355
  if (packed.code !== 0) {
1657
2356
  return fail("opencode_plugin_pack_failed", "npm pack failed while preparing the OpenCode plugin source package.", {
1658
2357
  details: {
@@ -1810,76 +2509,654 @@ function openCodePluginTargetRoot(parsed, installingAll) {
1810
2509
  const target = stringFlag(parsed, "opencode-target") ?? (installingAll && sharedTarget ? join(sharedTarget, "opencode") : sharedTarget);
1811
2510
  return resolve(target ?? defaultOpenCodeRoot());
1812
2511
  }
2512
+ function traePluginTargetRoot(parsed, installingAll) {
2513
+ const sharedTarget = stringFlag(parsed, "target");
2514
+ const target = stringFlag(parsed, "trae-target") ?? (installingAll && sharedTarget ? join(sharedTarget, "trae") : sharedTarget);
2515
+ return resolve(target ?? defaultTraeUserRoot());
2516
+ }
1813
2517
  function defaultCodexPluginRoot() {
1814
- return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "plugins");
2518
+ return join(defaultCodexHome(), "plugins");
1815
2519
  }
1816
- async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
1817
- const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
1818
- if (explicitTarget && process.env.EMBED_CODEX_MCP_REGISTER !== "1") {
1819
- return {
1820
- registered: false,
1821
- hint: `Installed into a custom target. Register manually with: codex mcp add embed-labs -- node ${join(targetPath, "scripts", "embed-labs-mcp-bridge.mjs")}`
1822
- };
2520
+ function defaultCodexHome() {
2521
+ return resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
2522
+ }
2523
+ function codexConfigPath() {
2524
+ return join(defaultCodexHome(), "config.toml");
2525
+ }
2526
+ async function cleanupLegacyCodexPluginRemnants(targetRoot) {
2527
+ const removedPaths = [];
2528
+ const removedConfigTables = [];
2529
+ const warnings = [];
2530
+ const stoppedProcesses = await stopLegacyCodexPluginProcesses(warnings);
2531
+ const legacyPaths = [
2532
+ join(targetRoot, "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME)
2533
+ ];
2534
+ for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
2535
+ legacyPaths.push(join(targetRoot, "cache", marketplaceName, CODEX_PLUGIN_NAME));
1823
2536
  }
1824
- const bridgePath = join(targetPath, "scripts", "embed-labs-mcp-bridge.mjs");
1825
- if (!await pathExists(bridgePath)) {
1826
- return {
1827
- registered: false,
1828
- hint: "Restart Codex or reload plugins after installing.",
1829
- warning: `Codex MCP bridge was not found at ${bridgePath}.`
1830
- };
2537
+ for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
2538
+ legacyPaths.push(join(targetRoot, pluginName));
2539
+ legacyPaths.push(join(targetRoot, "cache", pluginName));
2540
+ for (const marketplaceName of [CODEX_MARKETPLACE_NAME, ...LEGACY_CODEX_MARKETPLACE_NAMES]) {
2541
+ legacyPaths.push(join(targetRoot, "cache", marketplaceName, pluginName));
2542
+ }
1831
2543
  }
1832
- const codexBin = await resolveExecutableOnPath("codex");
1833
- if (!codexBin) {
1834
- return {
1835
- registered: false,
1836
- hint: `Codex CLI was not found on PATH. Register manually with: codex mcp add embed-labs -- node ${bridgePath}`
1837
- };
2544
+ legacyPaths.push(...await discoverLegacyCodexCachePaths(targetRoot));
2545
+ if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
2546
+ 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"));
2547
+ legacyPaths.push(...legacyCodexLocalMarketplacePaths(), ...await discoverLegacyHomeAgentsMarketplacePaths(warnings), ...await discoverLegacyCodexProjectMarketplacePaths(warnings), ...legacyDevelopmentBoardRuntimePluginPaths());
1838
2548
  }
1839
- const embedCliBin = process.env.EMBED_CLI_BIN?.trim() || await resolveExecutableOnPath("embedlabs") || await resolveExecutableOnPath("embed") || "";
1840
- const authFile = resolve(process.env.EMBED_AUTH_FILE?.trim() || DEFAULT_AUTH_FILE);
1841
- const cloudUrl = pluginMcpCloudApiUrl(parsed);
1842
- const existing = await runLocalProcess(codexBin, ["mcp", "get", "embed-labs", "--json"]);
1843
- if (existing.code === 0 && codexMcpAlreadyRegistered(existing.stdout, bridgePath, cloudUrl, authFile, embedCliBin)) {
1844
- const warning = await upsertCodexMcpRuntimeConfig(bridgePath);
1845
- if (warning) {
1846
- return { registered: true, warning };
2549
+ for (const candidate of legacyPaths) {
2550
+ try {
2551
+ if (await pathExists(candidate)) {
2552
+ await rm(candidate, { recursive: true, force: true });
2553
+ removedPaths.push(candidate);
2554
+ }
2555
+ }
2556
+ catch (error) {
2557
+ warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
1847
2558
  }
1848
- return { registered: true };
1849
2559
  }
1850
- await runLocalProcess(codexBin, ["mcp", "remove", "embed-labs"]);
1851
- const args = [
1852
- "mcp",
1853
- "add",
1854
- "embed-labs",
1855
- "--env",
1856
- `EMBED_CLOUD_API_URL=${cloudUrl}`,
1857
- "--env",
1858
- `EMBED_AUTH_FILE=${authFile}`
1859
- ];
1860
- if (embedCliBin) {
1861
- args.push("--env", `EMBED_CLI_BIN=${embedCliBin}`);
2560
+ if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
2561
+ const configPath = codexConfigPath();
2562
+ try {
2563
+ if (await pathExists(configPath)) {
2564
+ const current = await readFile(configPath, "utf8");
2565
+ const updated = removeLegacyCodexConfigTables(current);
2566
+ if (updated.text !== current) {
2567
+ await writeFile(configPath, updated.text, "utf8");
2568
+ }
2569
+ removedConfigTables.push(...updated.removedTables);
2570
+ }
2571
+ }
2572
+ catch (error) {
2573
+ warnings.push(`Could not update ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
2574
+ }
1862
2575
  }
1863
- args.push("--", process.execPath, bridgePath);
2576
+ const removedHistoryEntries = await cleanupLegacyCodexTextState(warnings);
2577
+ return {
2578
+ legacy_removed_paths: Array.from(new Set(removedPaths)),
2579
+ legacy_removed_config_tables: removedConfigTables,
2580
+ legacy_removed_history_entries: removedHistoryEntries,
2581
+ legacy_stopped_processes: stoppedProcesses,
2582
+ ...(warnings.length > 0 ? { warnings } : {})
2583
+ };
2584
+ }
2585
+ async function stopLegacyCodexPluginProcesses(warnings) {
2586
+ if (process.platform === "win32")
2587
+ return 0;
2588
+ try {
2589
+ const ps = await runLocalProcess("ps", ["-axo", "pid=,command="]);
2590
+ if (ps.code !== 0)
2591
+ return 0;
2592
+ let stopped = 0;
2593
+ for (const line of ps.stdout.split("\n")) {
2594
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
2595
+ if (!match)
2596
+ continue;
2597
+ const pid = Number(match[1]);
2598
+ const command = match[2] || "";
2599
+ if (!isLegacyCodexPluginProcess(command))
2600
+ continue;
2601
+ try {
2602
+ process.kill(pid, "SIGTERM");
2603
+ stopped += 1;
2604
+ }
2605
+ catch (error) {
2606
+ warnings.push(`Could not stop legacy Codex plugin process ${pid}: ${error instanceof Error ? error.message : String(error)}`);
2607
+ }
2608
+ }
2609
+ return stopped;
2610
+ }
2611
+ catch (error) {
2612
+ warnings.push(`Could not scan legacy Codex plugin processes: ${error instanceof Error ? error.message : String(error)}`);
2613
+ return 0;
2614
+ }
2615
+ }
2616
+ function isLegacyCodexPluginProcess(command) {
2617
+ const trimmed = command.trim();
2618
+ return /^\/.*\/dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
2619
+ || /^dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
2620
+ || legacyDevelopmentBoardRuntimeProcessPatterns().some((pattern) => pattern.test(trimmed));
2621
+ }
2622
+ function legacyDevelopmentBoardRuntimeProcessPatterns() {
2623
+ const home = escapeRegExp(homedir());
2624
+ return [
2625
+ new RegExp(`^${home}/Library/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
2626
+ new RegExp(`^${home}/Library/Application Support/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
2627
+ new RegExp(`^${home}/Library/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
2628
+ new RegExp(`^${home}/Library/Application Support/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
2629
+ new RegExp(`^${home}/.*?/DBT-Agent\\.app/Contents/MacOS/DBT-Agent(?:\\s|$)`)
2630
+ ];
2631
+ }
2632
+ function legacyDevelopmentBoardRuntimePluginPaths() {
2633
+ return [
2634
+ join(homedir(), "Library", "development-board-toolchain", "runtime", "editor_plugins"),
2635
+ join(homedir(), "Library", "development-board-toolchain", "runtime", "opencode_plugin"),
2636
+ join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "editor_plugins"),
2637
+ join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "opencode_plugin")
2638
+ ];
2639
+ }
2640
+ function legacyCodexLocalMarketplacePaths() {
2641
+ return Array.from(LEGACY_CODEX_MARKETPLACE_NAMES)
2642
+ .filter((name) => name !== CODEX_MARKETPLACE_NAME)
2643
+ .map((name) => join(defaultCodexHome(), "local-marketplaces", name));
2644
+ }
2645
+ async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
2646
+ const paths = [];
2647
+ const pluginRoot = join(homedir(), ".agents", "plugins");
2648
+ try {
2649
+ const entries = await readdir(pluginRoot, { withFileTypes: true });
2650
+ for (const entry of entries) {
2651
+ if (!entry.isFile() || !entry.name.startsWith("marketplace.json"))
2652
+ continue;
2653
+ const filePath = join(pluginRoot, entry.name);
2654
+ try {
2655
+ const current = await readFile(filePath, "utf8");
2656
+ if (isLegacyHomeAgentsMarketplace(current)) {
2657
+ paths.push(filePath);
2658
+ }
2659
+ }
2660
+ catch (error) {
2661
+ warnings.push(`Could not inspect legacy Codex home marketplace ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2662
+ }
2663
+ }
2664
+ }
2665
+ catch {
2666
+ return paths;
2667
+ }
2668
+ return paths;
2669
+ }
2670
+ function isLegacyHomeAgentsMarketplace(text) {
2671
+ return marketplaceTextHasLegacyCodexPlugin(text);
2672
+ }
2673
+ async function discoverLegacyCodexProjectMarketplacePaths(warnings) {
2674
+ const configPath = codexConfigPath();
2675
+ let text = "";
2676
+ try {
2677
+ text = await readFile(configPath, "utf8");
2678
+ }
2679
+ catch {
2680
+ return [];
2681
+ }
2682
+ const paths = new Set();
2683
+ for (const projectPath of legacyCodexProjectPathsFromConfig(text)) {
2684
+ for (const candidate of [
2685
+ join(projectPath, ".agents", "plugins", "marketplace.json"),
2686
+ join(projectPath, "platform_plugin", ".agents", "plugins", "marketplace.json"),
2687
+ join(projectPath, "platform_plugins", "codex_plugin", ".agents", "plugins", "marketplace.json")
2688
+ ]) {
2689
+ try {
2690
+ if (!await pathExists(candidate))
2691
+ continue;
2692
+ const current = await readFile(candidate, "utf8");
2693
+ if (marketplaceTextHasLegacyCodexPlugin(current)) {
2694
+ paths.add(candidate);
2695
+ }
2696
+ }
2697
+ catch (error) {
2698
+ warnings.push(`Could not inspect legacy Codex project marketplace ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2699
+ }
2700
+ }
2701
+ }
2702
+ return Array.from(paths);
2703
+ }
2704
+ function legacyCodexProjectPathsFromConfig(text) {
2705
+ const paths = [];
2706
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2707
+ for (const line of lines) {
2708
+ const table = parseTomlTableHeader(line);
2709
+ const match = table ? /^projects\."([^"]+)"$/.exec(table) : undefined;
2710
+ if (match?.[1] && /DBT-Agent-Project|development-board-toolchain|dbt-agent/i.test(match[1])) {
2711
+ paths.push(match[1].replace(/\\"/g, '"'));
2712
+ }
2713
+ }
2714
+ return paths;
2715
+ }
2716
+ function marketplaceTextHasLegacyCodexPlugin(text) {
2717
+ try {
2718
+ const parsed = JSON.parse(text);
2719
+ const marketplaceName = typeof parsed.name === "string" ? parsed.name : "";
2720
+ const marketplaceDisplayName = typeof parsed.interface?.displayName === "string" ? parsed.interface.displayName : "";
2721
+ const marketplaceLooksLegacy = legacyTextHasCodexPluginResidue(marketplaceName)
2722
+ || legacyTextHasCodexPluginResidue(marketplaceDisplayName)
2723
+ || LEGACY_CODEX_MARKETPLACE_NAMES.has(marketplaceName);
2724
+ return (parsed.plugins ?? []).some((plugin) => {
2725
+ const values = [
2726
+ plugin.name,
2727
+ plugin.category,
2728
+ plugin.source?.path,
2729
+ plugin.interface?.displayName
2730
+ ].filter((value) => typeof value === "string");
2731
+ return values.some(legacyTextHasCodexPluginResidue) || marketplaceLooksLegacy && values.some((value) => /embed-labs|dbt|development-board/i.test(value));
2732
+ });
2733
+ }
2734
+ catch {
2735
+ return legacyTextHasCodexPluginResidue(text);
2736
+ }
2737
+ }
2738
+ async function discoverLegacyCodexCachePaths(targetRoot) {
2739
+ const paths = [];
2740
+ const cacheRoot = join(targetRoot, "cache");
2741
+ try {
2742
+ const marketplaces = await readdir(cacheRoot, { withFileTypes: true });
2743
+ for (const entry of marketplaces) {
2744
+ if (!entry.isDirectory())
2745
+ continue;
2746
+ if (LEGACY_CODEX_MARKETPLACE_NAMES.has(entry.name)) {
2747
+ paths.push(join(cacheRoot, entry.name, CODEX_PLUGIN_NAME));
2748
+ }
2749
+ for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
2750
+ paths.push(join(cacheRoot, entry.name, pluginName));
2751
+ }
2752
+ }
2753
+ }
2754
+ catch {
2755
+ return paths;
2756
+ }
2757
+ return paths;
2758
+ }
2759
+ async function cleanupLegacyCodexTextState(warnings) {
2760
+ let removed = 0;
2761
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "history.jsonl"), warnings);
2762
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "session_index.jsonl"), warnings);
2763
+ removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "rules", "default.rules"), warnings);
2764
+ return removed;
2765
+ }
2766
+ async function cleanupLegacyCodexTextFile(filePath, warnings) {
2767
+ try {
2768
+ if (!await pathExists(filePath))
2769
+ return 0;
2770
+ const current = await readFile(filePath, "utf8");
2771
+ const lines = current.split("\n");
2772
+ let removed = 0;
2773
+ const kept = lines.filter((line) => {
2774
+ if (!line)
2775
+ return true;
2776
+ if (isLegacyCodexHistoryMention(line)) {
2777
+ removed += 1;
2778
+ return false;
2779
+ }
2780
+ return true;
2781
+ });
2782
+ if (removed > 0) {
2783
+ await writeFile(filePath, kept.join("\n"), "utf8");
2784
+ }
2785
+ return removed;
2786
+ }
2787
+ catch (error) {
2788
+ warnings.push(`Could not clean Codex legacy text state ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2789
+ return 0;
2790
+ }
2791
+ }
2792
+ function isLegacyCodexHistoryMention(line) {
2793
+ return line.includes("plugin://dbt-agent@plugins")
2794
+ || line.includes("plugin://dbt-agent@embed-labs")
2795
+ || line.includes("development-board-toolchain-dev")
2796
+ || line.includes("development-board-toolchain")
2797
+ || /plugin:\/\/Dbt Agent@/i.test(line)
2798
+ || /plugin:\/\/deve@/i.test(line)
2799
+ || /dbt-agent/i.test(line);
2800
+ }
2801
+ function removeLegacyCodexConfigTables(text) {
2802
+ const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2803
+ const output = [];
2804
+ const removedTables = [];
2805
+ let skipping = false;
2806
+ for (const line of lines) {
2807
+ const table = parseTomlTableHeader(line);
2808
+ if (table) {
2809
+ skipping = isLegacyCodexConfigTable(table);
2810
+ if (skipping) {
2811
+ removedTables.push(table);
2812
+ continue;
2813
+ }
2814
+ }
2815
+ if (!skipping) {
2816
+ output.push(line);
2817
+ }
2818
+ }
2819
+ return { text: output.join("").replace(/\n{3,}/g, "\n\n"), removedTables };
2820
+ }
2821
+ function parseTomlTableHeader(line) {
2822
+ const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line);
2823
+ return match?.[1]?.trim();
2824
+ }
2825
+ function isLegacyCodexConfigTable(table) {
2826
+ return /^plugins\."dbt-agent@[^"]+"$/.test(table)
2827
+ || /^plugins\."Dbt Agent@[^"]+"$/i.test(table)
2828
+ || /^plugins\."deve@[^"]+"$/i.test(table)
2829
+ || isLegacyEmbedLabsCodexMarketplaceConfigTable(table)
2830
+ || table === "mcp_servers.dbt-agent"
2831
+ || table.startsWith("mcp_servers.dbt-agent.")
2832
+ || table === 'mcp_servers."dbt-agent"'
2833
+ || table.startsWith('mcp_servers."dbt-agent".')
2834
+ || table === "mcp_servers.deve"
2835
+ || table.startsWith("mcp_servers.deve.")
2836
+ || /^projects\."[^"]*\/DBT-Agent-Project(?:\/[^"]*)?"$/.test(table);
2837
+ }
2838
+ function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
2839
+ for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
2840
+ if (table === `marketplaces.${marketplaceName}`) {
2841
+ return true;
2842
+ }
2843
+ if (table === `plugins."${CODEX_PLUGIN_NAME}@${marketplaceName}"`) {
2844
+ return true;
2845
+ }
2846
+ }
2847
+ return false;
2848
+ }
2849
+ function legacyTextHasCodexPluginResidue(value) {
2850
+ return /dbt-agent|Dbt Agent|development-board-toolchain|development-board-toolchain-dev/i.test(value)
2851
+ || value === "deve"
2852
+ || value.replace(/\\/g, "/").includes("/plugins/deve")
2853
+ || value.includes("plugin://deve@");
2854
+ }
2855
+ function legacyCodexCleanupWarning(cleanup) {
2856
+ const parts = [];
2857
+ if (cleanup.legacy_removed_paths.length > 0) {
2858
+ parts.push(`removed ${cleanup.legacy_removed_paths.length} stale/legacy Codex plugin path(s)`);
2859
+ }
2860
+ if (cleanup.legacy_removed_config_tables?.length) {
2861
+ parts.push(`removed ${cleanup.legacy_removed_config_tables.length} legacy Codex config table(s)`);
2862
+ }
2863
+ if (cleanup.legacy_removed_history_entries) {
2864
+ parts.push(`removed ${cleanup.legacy_removed_history_entries} legacy Codex text-state mention(s)`);
2865
+ }
2866
+ if (cleanup.legacy_stopped_processes) {
2867
+ parts.push(`stopped ${cleanup.legacy_stopped_processes} legacy Codex plugin process(es)`);
2868
+ }
2869
+ if (cleanup.warnings?.length) {
2870
+ parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
2871
+ }
2872
+ return parts.length > 0 ? `Codex plugin cleanup: ${parts.join(", ")}.` : undefined;
2873
+ }
2874
+ async function cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall) {
2875
+ const removedPaths = [];
2876
+ const warnings = [];
2877
+ const legacyPaths = [
2878
+ join(targetRoot, "plugins", "development-board-toolchain.js"),
2879
+ join(targetRoot, "plugins", "development-board-toolchain-dev.js"),
2880
+ join(targetRoot, "plugins", "dbt-agent.js"),
2881
+ join(targetRoot, "plugins", "Dbt Agent.js"),
2882
+ join(targetRoot, "plugins", "deve.js"),
2883
+ join(targetRoot, "plugins", "deve"),
2884
+ join(targetRoot, "node_modules", "development-board-toolchain"),
2885
+ join(targetRoot, "node_modules", "development-board-toolchain-dev"),
2886
+ join(targetRoot, "node_modules", "dbt-agent")
2887
+ ];
2888
+ if (globalInstall) {
2889
+ legacyPaths.push(join(targetRoot, "plugins", "embed-labs.js"));
2890
+ legacyPaths.push(...await discoverLegacyOpenCodeBackupPaths(targetRoot, warnings));
2891
+ legacyPaths.push(...await discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings));
2892
+ }
2893
+ for (const candidate of legacyPaths) {
2894
+ try {
2895
+ if (await pathExists(candidate)) {
2896
+ await rm(candidate, { recursive: true, force: true });
2897
+ removedPaths.push(candidate);
2898
+ }
2899
+ }
2900
+ catch (error) {
2901
+ warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
2902
+ }
2903
+ }
2904
+ return {
2905
+ legacy_removed_paths: removedPaths,
2906
+ legacy_removed_config_entries: [],
2907
+ ...(warnings.length > 0 ? { warnings } : {})
2908
+ };
2909
+ }
2910
+ async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
2911
+ const paths = [];
2912
+ try {
2913
+ const entries = await readdir(targetRoot, { withFileTypes: true });
2914
+ for (const entry of entries) {
2915
+ if (!entry.isFile() || !entry.name.includes(".bak"))
2916
+ continue;
2917
+ const filePath = join(targetRoot, entry.name);
2918
+ try {
2919
+ const current = await readFile(filePath, "utf8");
2920
+ if (legacyTextHasCodexPluginResidue(current)) {
2921
+ paths.push(filePath);
2922
+ }
2923
+ }
2924
+ catch (error) {
2925
+ warnings.push(`Could not inspect legacy OpenCode backup ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
2926
+ }
2927
+ }
2928
+ }
2929
+ catch {
2930
+ return paths;
2931
+ }
2932
+ return paths;
2933
+ }
2934
+ async function discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings) {
2935
+ const paths = [];
2936
+ const cacheRoot = join(targetRoot, ".embed-labs", "plugin-cache");
2937
+ try {
2938
+ const entries = await readdir(cacheRoot, { withFileTypes: true });
2939
+ for (const entry of entries) {
2940
+ if (!entry.isFile())
2941
+ continue;
2942
+ if (/^(embed-labs|embed-labs-opencode-plugin)-\d+\.\d+\.\d+\.tgz$/.test(entry.name)) {
2943
+ paths.push(join(cacheRoot, entry.name));
2944
+ }
2945
+ }
2946
+ }
2947
+ catch (error) {
2948
+ if (error.code !== "ENOENT") {
2949
+ warnings.push(`Could not inspect OpenCode plugin cache ${cacheRoot}: ${error instanceof Error ? error.message : String(error)}`);
2950
+ }
2951
+ }
2952
+ return paths;
2953
+ }
2954
+ function legacyOpenCodeCleanupWarning(cleanup) {
2955
+ const parts = [];
2956
+ if (cleanup.legacy_removed_paths.length > 0) {
2957
+ parts.push(`removed ${cleanup.legacy_removed_paths.length} legacy OpenCode plugin path(s)`);
2958
+ }
2959
+ if (cleanup.legacy_removed_config_entries?.length) {
2960
+ parts.push(`removed ${cleanup.legacy_removed_config_entries.length} legacy OpenCode config entry(s)`);
2961
+ }
2962
+ if (cleanup.warnings?.length) {
2963
+ parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
2964
+ }
2965
+ return parts.length > 0 ? `Legacy OpenCode cleanup: ${parts.join(", ")}.` : undefined;
2966
+ }
2967
+ function combineWarnings(...warnings) {
2968
+ const actual = warnings.filter((warning) => Boolean(warning));
2969
+ return actual.length > 0 ? actual.join(" ") : undefined;
2970
+ }
2971
+ async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
2972
+ const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
2973
+ if (explicitTarget && process.env.EMBED_CODEX_MCP_REGISTER !== "1") {
2974
+ return {
2975
+ registered: false,
2976
+ hint: "Installed into a custom target. Register manually with: codex mcp add embed-labs -- embedlabs mcp start"
2977
+ };
2978
+ }
2979
+ const bridgePath = join(targetPath, "scripts", "embed-labs-mcp-bridge.mjs");
2980
+ if (!await pathExists(bridgePath)) {
2981
+ return {
2982
+ registered: false,
2983
+ hint: "Restart Codex or reload plugins after installing.",
2984
+ warning: `Codex MCP bridge was not found at ${bridgePath}.`
2985
+ };
2986
+ }
2987
+ const codexBin = await resolveExecutableOnPath("codex");
2988
+ if (!codexBin) {
2989
+ return {
2990
+ registered: false,
2991
+ hint: `Codex CLI was not found on PATH. Register manually with: codex mcp add embed-labs -- node ${bridgePath}`
2992
+ };
2993
+ }
2994
+ const embedCliBin = process.env.EMBED_CLI_BIN?.trim() || await resolveExecutableOnPath("embedlabs") || await resolveExecutableOnPath("embed") || "";
2995
+ const mcpLauncher = await resolveEmbedCliMcpLauncher(embedCliBin);
2996
+ const authFile = resolve(process.env.EMBED_AUTH_FILE?.trim() || DEFAULT_AUTH_FILE);
2997
+ const cloudUrl = pluginMcpCloudApiUrl(parsed);
2998
+ const existing = await runLocalProcess(codexBin, ["mcp", "get", "embed-labs", "--json"]);
2999
+ if (existing.code === 0 && codexMcpAlreadyRegistered(existing.stdout, bridgePath, cloudUrl, authFile, embedCliBin, mcpLauncher.command, mcpLauncher.args)) {
3000
+ const warning = await upsertCodexMcpRuntimeConfig(mcpLauncher.command, mcpLauncher.args);
3001
+ if (warning) {
3002
+ return { registered: true, warning };
3003
+ }
3004
+ return { registered: true };
3005
+ }
3006
+ await runLocalProcess(codexBin, ["mcp", "remove", "embed-labs"]);
3007
+ const args = [
3008
+ "mcp",
3009
+ "add",
3010
+ "embed-labs",
3011
+ "--env",
3012
+ `EMBED_CLOUD_API_URL=${cloudUrl}`,
3013
+ "--env",
3014
+ `EMBED_AUTH_FILE=${authFile}`,
3015
+ "--env",
3016
+ `EMBED_MCP_BRIDGE_PATH=${bridgePath}`
3017
+ ];
3018
+ if (embedCliBin) {
3019
+ args.push("--env", `EMBED_CLI_BIN=${embedCliBin}`);
3020
+ }
3021
+ args.push("--", mcpLauncher.command, ...mcpLauncher.args);
1864
3022
  const addResult = await runLocalProcess(codexBin, args);
1865
3023
  if (addResult.code !== 0) {
1866
3024
  return {
1867
3025
  registered: false,
1868
- hint: `Codex plugin installed. Register manually with: codex mcp add embed-labs -- ${process.execPath} ${bridgePath}`,
3026
+ hint: "Codex plugin installed. Register manually with: codex mcp add embed-labs -- embedlabs mcp start",
1869
3027
  warning: `codex mcp add failed: ${addResult.stderr.trim() || addResult.stdout.trim() || `exit ${addResult.code}`}`
1870
3028
  };
1871
3029
  }
1872
- const warning = await upsertCodexMcpRuntimeConfig(bridgePath);
3030
+ const warning = await upsertCodexMcpRuntimeConfig(mcpLauncher.command, mcpLauncher.args);
1873
3031
  return warning ? { registered: true, warning } : { registered: true };
1874
3032
  }
1875
- function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embedCliBin) {
3033
+ async function maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath) {
3034
+ const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
3035
+ if (explicitTarget && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
3036
+ return {
3037
+ registered: false,
3038
+ warning: "Codex plugin marketplace entry was not registered because a custom target was used. Set EMBED_CODEX_MARKETPLACE_REGISTER=1 to register it anyway."
3039
+ };
3040
+ }
3041
+ if (resolve(targetRoot) !== resolve(defaultCodexPluginRoot()) && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
3042
+ return {
3043
+ registered: false,
3044
+ warning: "Codex plugin marketplace entry was not registered because the install target is not the default Codex plugin root."
3045
+ };
3046
+ }
3047
+ const marketplacePath = defaultCodexLocalMarketplaceRoot();
3048
+ const marketplacePluginPath = join(marketplacePath, "plugins", CODEX_PLUGIN_NAME);
3049
+ try {
3050
+ if (!await pathExists(join(targetPath, ".codex-plugin", "plugin.json"))) {
3051
+ return {
3052
+ registered: false,
3053
+ warning: `Codex plugin manifest was not found at ${join(targetPath, ".codex-plugin", "plugin.json")}.`
3054
+ };
3055
+ }
3056
+ await rm(marketplacePluginPath, { recursive: true, force: true });
3057
+ await mkdir(dirname(marketplacePluginPath), { recursive: true });
3058
+ await cp(targetPath, marketplacePluginPath, { recursive: true });
3059
+ await refreshCodexPluginCache(targetPath);
3060
+ await writeCodexLocalMarketplaceManifest(marketplacePath);
3061
+ const warning = await upsertCodexPluginMarketplaceConfig(marketplacePath);
3062
+ return warning ? { registered: true, marketplacePath, warning } : { registered: true, marketplacePath };
3063
+ }
3064
+ catch (error) {
3065
+ return {
3066
+ registered: false,
3067
+ marketplacePath,
3068
+ warning: `Could not register Codex plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`
3069
+ };
3070
+ }
3071
+ }
3072
+ function defaultCodexLocalMarketplaceRoot() {
3073
+ return join(defaultCodexHome(), "local-marketplaces", CODEX_PLUGIN_NAME);
3074
+ }
3075
+ async function refreshCodexPluginCache(targetPath) {
3076
+ const manifestPath = join(targetPath, ".codex-plugin", "plugin.json");
3077
+ const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
3078
+ const version = typeof manifest.version === "string" && manifest.version.trim()
3079
+ ? manifest.version.trim()
3080
+ : "local";
3081
+ const cachePluginRoot = join(defaultCodexPluginRoot(), "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME);
3082
+ const cacheVersionPath = join(cachePluginRoot, version);
3083
+ await rm(cachePluginRoot, { recursive: true, force: true });
3084
+ await mkdir(dirname(cacheVersionPath), { recursive: true });
3085
+ await cp(targetPath, cacheVersionPath, { recursive: true });
3086
+ }
3087
+ async function writeCodexLocalMarketplaceManifest(marketplacePath) {
3088
+ const manifestPath = join(marketplacePath, ".agents", "plugins", "marketplace.json");
3089
+ const manifest = {
3090
+ name: CODEX_MARKETPLACE_NAME,
3091
+ interface: {
3092
+ displayName: "Embed Labs"
3093
+ },
3094
+ plugins: [
3095
+ {
3096
+ name: CODEX_PLUGIN_NAME,
3097
+ source: {
3098
+ source: "local",
3099
+ path: `./plugins/${CODEX_PLUGIN_NAME}`
3100
+ },
3101
+ policy: {
3102
+ installation: "AVAILABLE",
3103
+ authentication: "ON_USE"
3104
+ },
3105
+ category: "Developer Tools"
3106
+ }
3107
+ ]
3108
+ };
3109
+ await mkdir(dirname(manifestPath), { recursive: true });
3110
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
3111
+ }
3112
+ async function upsertCodexPluginMarketplaceConfig(marketplacePath) {
3113
+ const configPath = codexConfigPath();
3114
+ try {
3115
+ await mkdir(dirname(configPath), { recursive: true });
3116
+ let text = "";
3117
+ try {
3118
+ text = await readFile(configPath, "utf8");
3119
+ }
3120
+ catch {
3121
+ text = "";
3122
+ }
3123
+ const original = text;
3124
+ const cleaned = removeLegacyCodexConfigTables(text).text;
3125
+ let updated = upsertTomlTableKeys(cleaned, `marketplaces.${CODEX_MARKETPLACE_NAME}`, {
3126
+ source_type: tomlString("local"),
3127
+ source: tomlString(marketplacePath),
3128
+ last_updated: tomlString(new Date().toISOString().replace(/\.\d{3}Z$/, "Z"))
3129
+ });
3130
+ updated = upsertTomlTableKeys(updated, `plugins."${CODEX_PLUGIN_NAME}@${CODEX_MARKETPLACE_NAME}"`, {
3131
+ enabled: "true"
3132
+ });
3133
+ if (updated !== original) {
3134
+ await writeFile(configPath, updated, "utf8");
3135
+ }
3136
+ return undefined;
3137
+ }
3138
+ catch (error) {
3139
+ return `${configPath} could not be updated with the Embed Labs plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`;
3140
+ }
3141
+ }
3142
+ async function resolveEmbedCliMcpLauncher(embedCliBin) {
3143
+ const explicit = embedCliBin?.trim();
3144
+ if (explicit) {
3145
+ return { command: explicit, args: ["mcp", "start"] };
3146
+ }
3147
+ const pathBin = await resolveExecutableOnPath("embedlabs") || await resolveExecutableOnPath("embed");
3148
+ if (pathBin) {
3149
+ return { command: pathBin, args: ["mcp", "start"] };
3150
+ }
3151
+ return { command: process.execPath, args: [join(CLI_MODULE_DIR, "index.js"), "mcp", "start"] };
3152
+ }
3153
+ function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embedCliBin, mcpCommand, mcpArgs) {
1876
3154
  try {
1877
3155
  const parsed = JSON.parse(stdout);
1878
3156
  const transport = parsed.transport;
1879
- if (transport?.type !== "stdio" || transport.command !== process.execPath)
1880
- return false;
1881
- if (!transport.args?.includes(bridgePath))
3157
+ if (transport?.type !== "stdio")
1882
3158
  return false;
3159
+ const args = transport.args || [];
1883
3160
  const env = transport.env || {};
1884
3161
  if (env.EMBED_CLOUD_API_URL !== cloudUrl)
1885
3162
  return false;
@@ -1887,13 +3164,16 @@ function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embed
1887
3164
  return false;
1888
3165
  if (embedCliBin && env.EMBED_CLI_BIN !== embedCliBin)
1889
3166
  return false;
1890
- return true;
3167
+ const directBridge = transport.command === process.execPath && args.includes(bridgePath);
3168
+ const cliMcp = transport.command === mcpCommand
3169
+ && mcpArgs.every((arg, index) => args[index] === arg);
3170
+ return directBridge || cliMcp;
1891
3171
  }
1892
3172
  catch {
1893
3173
  return false;
1894
3174
  }
1895
3175
  }
1896
- async function upsertCodexMcpRuntimeConfig(bridgePath) {
3176
+ async function upsertCodexMcpRuntimeConfig(command, args) {
1897
3177
  const configPath = join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "config.toml");
1898
3178
  try {
1899
3179
  await mkdir(dirname(configPath), { recursive: true });
@@ -1905,8 +3185,8 @@ async function upsertCodexMcpRuntimeConfig(bridgePath) {
1905
3185
  text = "";
1906
3186
  }
1907
3187
  const updated = upsertTomlTableKeys(text, "mcp_servers.embed-labs", {
1908
- command: tomlString(process.execPath),
1909
- args: `[${tomlString(bridgePath)}]`,
3188
+ command: tomlString(command),
3189
+ args: `[${args.map(tomlString).join(", ")}]`,
1910
3190
  startup_timeout_sec: "120"
1911
3191
  });
1912
3192
  if (updated !== text) {
@@ -1983,11 +3263,330 @@ async function resolveExecutableOnPath(name) {
1983
3263
  }
1984
3264
  return undefined;
1985
3265
  }
3266
+ async function runBridgeStart(parsed) {
3267
+ const host = stringFlag(parsed, "host");
3268
+ const port = numberFlag(parsed, "port");
3269
+ const bridge = await resolveBridgeLauncher();
3270
+ const args = [...bridge.args];
3271
+ if (host) {
3272
+ args.push("--host", host);
3273
+ }
3274
+ if (port !== undefined) {
3275
+ args.push("--port", String(port));
3276
+ }
3277
+ const env = {
3278
+ ...process.env,
3279
+ ...(host ? { EMBED_BRIDGE_HOST: host } : {}),
3280
+ ...(port !== undefined ? { EMBED_BRIDGE_PORT: String(port) } : {})
3281
+ };
3282
+ const child = spawn(bridge.command, args, {
3283
+ stdio: "inherit",
3284
+ env
3285
+ });
3286
+ const forwardSignal = (signal) => {
3287
+ if (!child.killed) {
3288
+ child.kill(signal);
3289
+ }
3290
+ };
3291
+ process.once("SIGINT", forwardSignal);
3292
+ process.once("SIGTERM", forwardSignal);
3293
+ return await new Promise((resolveCode) => {
3294
+ child.on("error", (error) => {
3295
+ process.off("SIGINT", forwardSignal);
3296
+ process.off("SIGTERM", forwardSignal);
3297
+ console.error(error instanceof Error ? error.message : String(error));
3298
+ resolveCode(1);
3299
+ });
3300
+ child.on("close", (code, signal) => {
3301
+ process.off("SIGINT", forwardSignal);
3302
+ process.off("SIGTERM", forwardSignal);
3303
+ if (signal === "SIGINT" || signal === "SIGTERM") {
3304
+ resolveCode(0);
3305
+ }
3306
+ else {
3307
+ resolveCode(code ?? 0);
3308
+ }
3309
+ });
3310
+ });
3311
+ }
3312
+ async function runMcpStart(parsed) {
3313
+ const unknownFlag = firstUnknownFlag(parsed, ["bridge-path"]);
3314
+ const unexpected = parsed.command.slice(2);
3315
+ if (unknownFlag || unexpected.length > 0) {
3316
+ console.error(unknownFlag ? `Unknown flag --${unknownFlag}. ${MCP_START_USAGE}` : MCP_START_USAGE);
3317
+ return 2;
3318
+ }
3319
+ const bridge = await resolveMcpBridgeLauncher(stringFlag(parsed, "bridge-path"));
3320
+ if (!bridge) {
3321
+ console.error([
3322
+ "Embed Labs MCP bridge was not found.",
3323
+ "Run npm run build in the source checkout, reinstall the embedlabs npm package, or set EMBED_MCP_BRIDGE_PATH to the bridge script.",
3324
+ "Expected command shape for MCP clients: embedlabs mcp start"
3325
+ ].join("\n"));
3326
+ return 1;
3327
+ }
3328
+ const child = spawn(bridge.command, bridge.args, {
3329
+ stdio: "inherit",
3330
+ env: {
3331
+ ...process.env,
3332
+ EMBED_CLIENT_KIND: process.env.EMBED_CLIENT_KIND || "mcp_client",
3333
+ EMBED_MCP_BRIDGE_PATH: bridge.bridgePath
3334
+ }
3335
+ });
3336
+ const forwardSignal = (signal) => {
3337
+ if (!child.killed) {
3338
+ child.kill(signal);
3339
+ }
3340
+ };
3341
+ process.once("SIGINT", forwardSignal);
3342
+ process.once("SIGTERM", forwardSignal);
3343
+ return await new Promise((resolveCode) => {
3344
+ child.on("error", (error) => {
3345
+ process.off("SIGINT", forwardSignal);
3346
+ process.off("SIGTERM", forwardSignal);
3347
+ console.error(error instanceof Error ? error.message : String(error));
3348
+ resolveCode(1);
3349
+ });
3350
+ child.on("close", (code, signal) => {
3351
+ process.off("SIGINT", forwardSignal);
3352
+ process.off("SIGTERM", forwardSignal);
3353
+ if (signal === "SIGINT" || signal === "SIGTERM") {
3354
+ resolveCode(0);
3355
+ }
3356
+ else {
3357
+ resolveCode(code ?? 0);
3358
+ }
3359
+ });
3360
+ });
3361
+ }
3362
+ async function mcpConfig(parsed) {
3363
+ const unknownFlag = firstUnknownFlag(parsed, ["client", "absolute-command", "cloud-url", "json"]);
3364
+ if (unknownFlag) {
3365
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${MCP_CONFIG_USAGE}`);
3366
+ }
3367
+ const unexpected = parsed.command.slice(2);
3368
+ if (unexpected.length > 0) {
3369
+ return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${MCP_CONFIG_USAGE}`);
3370
+ }
3371
+ const client = (stringFlag(parsed, "client") || "generic").toLowerCase();
3372
+ const supported = new Set(["generic", "trae", "cursor", "claude", "claude-desktop", "windsurf"]);
3373
+ if (!supported.has(client)) {
3374
+ return fail("invalid_args", `Unsupported MCP client: ${client}. ${MCP_CONFIG_USAGE}`);
3375
+ }
3376
+ const absolute = booleanFlag(parsed, "absolute-command") || client === "trae";
3377
+ const cloudUrl = stringFlag(parsed, "cloud-url") || DEFAULT_CLOUD_API_URL;
3378
+ const command = absolute
3379
+ ? (process.env.EMBED_CLI_BIN?.trim() || await resolveExecutableOnPath("embedlabs") || await resolveExecutableOnPath("embed") || process.argv[1] || "embedlabs")
3380
+ : "embedlabs";
3381
+ const server = {
3382
+ command,
3383
+ args: ["mcp", "start"],
3384
+ env: {
3385
+ EMBED_CLOUD_API_URL: cloudUrl
3386
+ }
3387
+ };
3388
+ if (client === "trae") {
3389
+ server.fromGalleryId = "local.embed-labs.mcp_server";
3390
+ }
3391
+ const normalizedClient = client === "claude-desktop" ? "claude" : client;
3392
+ const notes = [
3393
+ "Run `npm install -g embedlabs` first if the command is not installed.",
3394
+ "Run `embedlabs auth login --token <user-api-key>` before using protected Embed Labs tools.",
3395
+ "Restart or reload the MCP client after adding this configuration."
3396
+ ];
3397
+ if (absolute) {
3398
+ notes.push("This output uses an absolute command path because some desktop IDEs do not inherit shell PATH.");
3399
+ }
3400
+ if (normalizedClient === "trae") {
3401
+ notes.push("For Trae, prefer `embedlabs plugin install trae`; it writes this MCP config and the local Gallery icon metadata automatically.");
3402
+ notes.push("Manual Trae configs may not show an icon unless the local Gallery cache entry exists.");
3403
+ }
3404
+ else {
3405
+ notes.push("If the IDE UI requires a different wrapper key, paste the inner `embed-labs` server object into that UI's MCP server form.");
3406
+ }
3407
+ return ok({
3408
+ client: normalizedClient,
3409
+ config: {
3410
+ mcpServers: {
3411
+ "embed-labs": server
3412
+ }
3413
+ },
3414
+ install_command: normalizedClient === "trae" ? "embedlabs plugin install trae" : undefined,
3415
+ config_path_hint: mcpConfigPathHint(normalizedClient),
3416
+ notes
3417
+ });
3418
+ }
3419
+ function mcpConfigPathHint(client) {
3420
+ if (client === "trae") {
3421
+ return process.platform === "darwin"
3422
+ ? "~/Library/Application Support/Trae CN/User/mcp.json"
3423
+ : "Trae User mcp.json";
3424
+ }
3425
+ if (client === "cursor") {
3426
+ return "~/.cursor/mcp.json or Cursor MCP settings";
3427
+ }
3428
+ if (client === "claude") {
3429
+ return "Claude Desktop MCP settings JSON";
3430
+ }
3431
+ if (client === "windsurf") {
3432
+ return "Windsurf MCP settings JSON";
3433
+ }
3434
+ return undefined;
3435
+ }
3436
+ async function resolveMcpBridgeLauncher(overridePath) {
3437
+ const explicitPath = overridePath?.trim() || process.env.EMBED_MCP_BRIDGE_PATH?.trim();
3438
+ const candidates = [
3439
+ explicitPath ? resolve(explicitPath) : "",
3440
+ join(CLI_MODULE_DIR, "embed-labs-mcp-bridge.mjs"),
3441
+ sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", "scripts", "embed-labs-mcp-bridge.mjs"),
3442
+ join(defaultCodexPluginRoot(), CODEX_PLUGIN_NAME, "scripts", "embed-labs-mcp-bridge.mjs")
3443
+ ].filter(Boolean);
3444
+ for (const candidate of candidates) {
3445
+ try {
3446
+ await access(candidate, constants.R_OK);
3447
+ return { command: process.execPath, args: [candidate], bridgePath: candidate };
3448
+ }
3449
+ catch {
3450
+ // Keep looking.
3451
+ }
3452
+ }
3453
+ return undefined;
3454
+ }
3455
+ async function resolveBridgeLauncher() {
3456
+ const explicitBinary = process.env.EMBED_LOCAL_BRIDGE_BINARY?.trim();
3457
+ if (explicitBinary) {
3458
+ try {
3459
+ await access(explicitBinary, constants.X_OK);
3460
+ return { command: explicitBinary, args: [] };
3461
+ }
3462
+ catch {
3463
+ // Fall through so the package launcher can print its clearer repair message.
3464
+ }
3465
+ }
3466
+ const pathBinary = await resolveExecutableOnPath(process.platform === "win32" ? "embed-local-bridge.cmd" : "embed-local-bridge");
3467
+ if (pathBinary) {
3468
+ return { command: pathBinary, args: [] };
3469
+ }
3470
+ const packageLauncher = await resolveBridgePackageLauncher();
3471
+ if (packageLauncher) {
3472
+ return { command: process.execPath, args: [packageLauncher] };
3473
+ }
3474
+ return {
3475
+ command: process.execPath,
3476
+ args: [resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js")]
3477
+ };
3478
+ }
3479
+ async function resolveBridgePackageLauncher() {
3480
+ const candidates = [];
3481
+ try {
3482
+ const packageJson = require.resolve("@embed-labs/local-bridge/package.json");
3483
+ candidates.push(join(dirname(packageJson), "dist", "index.js"));
3484
+ }
3485
+ catch {
3486
+ // Source checkout fallback below.
3487
+ }
3488
+ candidates.push(resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js"));
3489
+ for (const candidate of candidates) {
3490
+ try {
3491
+ await access(candidate, constants.R_OK);
3492
+ return candidate;
3493
+ }
3494
+ catch {
3495
+ // Keep looking.
3496
+ }
3497
+ }
3498
+ return undefined;
3499
+ }
1986
3500
  function defaultOpenCodeRoot() {
1987
- return join(process.cwd(), ".opencode");
3501
+ return globalOpenCodeRoot();
3502
+ }
3503
+ function defaultTraeUserRoot() {
3504
+ const explicit = process.env.EMBED_TRAE_USER_DIR?.trim();
3505
+ if (explicit) {
3506
+ return resolve(explicit);
3507
+ }
3508
+ if (process.platform === "darwin") {
3509
+ const candidates = [
3510
+ join(homedir(), "Library", "Application Support", "Trae CN", "User"),
3511
+ join(homedir(), "Library", "Application Support", "Trae", "User")
3512
+ ];
3513
+ return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
3514
+ }
3515
+ if (process.platform === "win32") {
3516
+ const appData = process.env.APPDATA?.trim() || join(homedir(), "AppData", "Roaming");
3517
+ const candidates = [
3518
+ join(appData, "Trae CN", "User"),
3519
+ join(appData, "Trae", "User")
3520
+ ];
3521
+ return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
3522
+ }
3523
+ const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config");
3524
+ const candidates = [
3525
+ join(xdgConfigHome, "Trae CN", "User"),
3526
+ join(xdgConfigHome, "Trae", "User")
3527
+ ];
3528
+ return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
3529
+ }
3530
+ function globalOpenCodeRoot() {
3531
+ return join(homedir(), ".config", "opencode");
3532
+ }
3533
+ function isGlobalOpenCodeRoot(targetRoot) {
3534
+ return resolve(targetRoot) === resolve(globalOpenCodeRoot());
3535
+ }
3536
+ async function ensureOpenCodeGlobalPluginConfig() {
3537
+ const configPath = join(globalOpenCodeRoot(), "opencode.json");
3538
+ let existing = {};
3539
+ try {
3540
+ const parsed = JSON.parse(await readFile(configPath, "utf8"));
3541
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
3542
+ existing = parsed;
3543
+ }
3544
+ }
3545
+ catch {
3546
+ existing = {};
3547
+ }
3548
+ const configured = Array.isArray(existing.plugin)
3549
+ ? existing.plugin.filter((item) => typeof item === "string")
3550
+ : Array.isArray(existing.plugins)
3551
+ ? existing.plugins.filter((item) => typeof item === "string")
3552
+ : [];
3553
+ const removed = configured.filter(isLegacyOpenCodePluginConfigEntry);
3554
+ const cleaned = configured.filter((item) => !isLegacyOpenCodePluginConfigEntry(item));
3555
+ if (!cleaned.includes("embed-labs")) {
3556
+ cleaned.push("embed-labs");
3557
+ }
3558
+ await writeFile(configPath, `${JSON.stringify({
3559
+ ...existing,
3560
+ plugin: cleaned,
3561
+ plugins: undefined
3562
+ }, null, 2)}\n`, "utf8");
3563
+ return removed;
3564
+ }
3565
+ function isLegacyOpenCodePluginConfigEntry(item) {
3566
+ const normalized = item.trim().replace(/\\/g, "/");
3567
+ const pathOnly = normalized.split(/[?#]/, 1)[0] || normalized;
3568
+ return normalized === "dbt-agent"
3569
+ || normalized === "Dbt Agent"
3570
+ || normalized === "deve"
3571
+ || normalized === "development-board-toolchain"
3572
+ || normalized === "development-board-toolchain-dev"
3573
+ || normalized === "./plugins/deve"
3574
+ || normalized === "./plugins/deve.js"
3575
+ || /(?:^|\/)plugins\/deve(?:\.js)?$/.test(pathOnly)
3576
+ || /(?:^|\/)plugins\/dbt-agent(?:\.js)?$/.test(pathOnly)
3577
+ || normalized === "./plugins/development-board-toolchain"
3578
+ || normalized === "./plugins/development-board-toolchain.js"
3579
+ || normalized === "./plugins/development-board-toolchain-dev"
3580
+ || normalized === "./plugins/development-board-toolchain-dev.js"
3581
+ || pathOnly.endsWith("/plugins/development-board-toolchain")
3582
+ || pathOnly.endsWith("/plugins/development-board-toolchain.js")
3583
+ || pathOnly.endsWith("/plugins/development-board-toolchain-dev")
3584
+ || pathOnly.endsWith("/plugins/development-board-toolchain-dev.js")
3585
+ || normalized.includes("dbt-agent")
3586
+ || normalized.includes("development-board-toolchain");
1988
3587
  }
1989
3588
  async function openCodeDuplicatePluginWarning(targetRoot) {
1990
- const globalRoot = join(homedir(), ".config", "opencode");
3589
+ const globalRoot = globalOpenCodeRoot();
1991
3590
  if (resolve(targetRoot) === resolve(globalRoot))
1992
3591
  return undefined;
1993
3592
  const configPath = join(globalRoot, "opencode.json");
@@ -2019,6 +3618,21 @@ async function localPluginVersion(kind) {
2019
3618
  return undefined;
2020
3619
  }
2021
3620
  }
3621
+ async function installedCodexPluginVersion(pluginPath) {
3622
+ return await readPackageVersion(join(pluginPath, ".codex-plugin", "plugin.json"));
3623
+ }
3624
+ async function installedOpenCodePluginVersion(targetRoot) {
3625
+ return await readPackageVersion(join(targetRoot, "node_modules", "embed-labs", "package.json"));
3626
+ }
3627
+ async function readPackageVersion(filePath) {
3628
+ try {
3629
+ const parsed = JSON.parse(await readFile(filePath, "utf8"));
3630
+ return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : undefined;
3631
+ }
3632
+ catch {
3633
+ return undefined;
3634
+ }
3635
+ }
2022
3636
  async function localPluginSourcesAvailable() {
2023
3637
  return await pathExists(sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", ".codex-plugin", "plugin.json"))
2024
3638
  && await pathExists(sourceCheckoutPath("platform_plugins", "opencode_plugin", "package.json"));
@@ -2037,7 +3651,8 @@ async function pathExists(pathValue) {
2037
3651
  }
2038
3652
  async function runLocalProcess(command, args) {
2039
3653
  return await new Promise((resolveProcess) => {
2040
- const child = spawn(command, args, {
3654
+ const launcher = localProcessLauncher(command, args);
3655
+ const child = spawn(launcher.command, launcher.args, {
2041
3656
  cwd: process.cwd(),
2042
3657
  env: process.env,
2043
3658
  stdio: ["ignore", "pipe", "pipe"]
@@ -2060,6 +3675,50 @@ async function runLocalProcess(command, args) {
2060
3675
  });
2061
3676
  });
2062
3677
  }
3678
+ function npmCommand() {
3679
+ return platform() === "win32" ? "npm.cmd" : "npm";
3680
+ }
3681
+ function localProcessLauncher(command, args) {
3682
+ if (platform() !== "win32" || !/\.(?:cmd|bat)$/i.test(command)) {
3683
+ return { command, args };
3684
+ }
3685
+ return {
3686
+ command: process.env.ComSpec || "cmd.exe",
3687
+ args: ["/d", "/s", "/c", windowsCommandLine([command, ...args])]
3688
+ };
3689
+ }
3690
+ function windowsCommandLine(args) {
3691
+ return args.map(windowsQuoteArg).join(" ");
3692
+ }
3693
+ function windowsQuoteArg(arg) {
3694
+ if (arg.length > 0 && !/[\s"]/u.test(arg)) {
3695
+ return arg;
3696
+ }
3697
+ let quoted = "\"";
3698
+ let backslashes = 0;
3699
+ for (const char of arg) {
3700
+ if (char === "\\") {
3701
+ backslashes += 1;
3702
+ continue;
3703
+ }
3704
+ if (char === "\"") {
3705
+ quoted += "\\".repeat((backslashes * 2) + 1);
3706
+ quoted += "\"";
3707
+ backslashes = 0;
3708
+ continue;
3709
+ }
3710
+ if (backslashes > 0) {
3711
+ quoted += "\\".repeat(backslashes);
3712
+ backslashes = 0;
3713
+ }
3714
+ quoted += char;
3715
+ }
3716
+ if (backslashes > 0) {
3717
+ quoted += "\\".repeat(backslashes * 2);
3718
+ }
3719
+ quoted += "\"";
3720
+ return quoted;
3721
+ }
2063
3722
  async function parseErrorResponse(response) {
2064
3723
  const text = await response.text();
2065
3724
  if (!text.trim()) {
@@ -2077,19 +3736,65 @@ async function parseErrorResponse(response) {
2077
3736
  return undefined;
2078
3737
  }
2079
3738
  async function cloudAuthToken() {
3739
+ return (await cloudAuthConfig()).token;
3740
+ }
3741
+ async function cloudAuthConfig() {
2080
3742
  const envToken = process.env.EMBED_API_TOKEN?.trim();
2081
3743
  if (envToken) {
2082
- return envToken;
3744
+ const fileConfig = await readLocalAuthFile();
3745
+ return {
3746
+ ...fileConfig,
3747
+ token: envToken,
3748
+ profile: process.env.EMBED_AUTH_PROFILE ?? fileConfig.profile ?? "default",
3749
+ source: "env"
3750
+ };
2083
3751
  }
3752
+ const fileConfig = await readLocalAuthFile();
3753
+ return {
3754
+ ...fileConfig,
3755
+ token: fileConfig.token?.trim() || undefined,
3756
+ profile: fileConfig.profile ?? "default",
3757
+ source: fileConfig.token ? "file" : undefined
3758
+ };
3759
+ }
3760
+ async function readLocalAuthFile() {
2084
3761
  try {
2085
3762
  const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
2086
- const fileToken = typeof parsed.token === "string" ? parsed.token.trim() : "";
2087
- return fileToken || undefined;
3763
+ return normalizeLocalAuthFile(parsed);
2088
3764
  }
2089
3765
  catch {
2090
- return undefined;
3766
+ return {};
2091
3767
  }
2092
3768
  }
3769
+ function normalizeLocalAuthFile(parsed) {
3770
+ const device = isJsonObject(parsed.device) ? parsed.device : undefined;
3771
+ const normalizedDevice = device && typeof device.device_id === "string" && typeof device.fingerprint_hash === "string" && typeof device.private_key_pem === "string"
3772
+ ? {
3773
+ device_id: device.device_id,
3774
+ fingerprint_hash: device.fingerprint_hash,
3775
+ private_key_pem: device.private_key_pem,
3776
+ public_key_pem: typeof device.public_key_pem === "string" ? device.public_key_pem : undefined,
3777
+ label: typeof device.label === "string" ? device.label : undefined,
3778
+ platform: typeof device.platform === "string" ? device.platform : undefined,
3779
+ arch: typeof device.arch === "string" ? device.arch : undefined,
3780
+ hostname_hash: typeof device.hostname_hash === "string" ? device.hostname_hash : undefined,
3781
+ registered_at: typeof device.registered_at === "string" ? device.registered_at : undefined
3782
+ }
3783
+ : undefined;
3784
+ return {
3785
+ profile: typeof parsed.profile === "string" ? parsed.profile : undefined,
3786
+ token: typeof parsed.token === "string" ? parsed.token.trim() : undefined,
3787
+ updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined,
3788
+ account_id: typeof parsed.account_id === "string" ? parsed.account_id : undefined,
3789
+ api_key_id: typeof parsed.api_key_id === "string" ? parsed.api_key_id : undefined,
3790
+ device: normalizedDevice
3791
+ };
3792
+ }
3793
+ async function writeLocalAuthFile(config) {
3794
+ await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
3795
+ await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify(config, null, 2)}\n`, "utf8");
3796
+ await chmod(DEFAULT_AUTH_FILE, 0o600).catch(() => undefined);
3797
+ }
2093
3798
  function serviceBaseUrl(url) {
2094
3799
  return url.replace(/\/+$/, "");
2095
3800
  }
@@ -2112,9 +3817,16 @@ async function naturalLanguageQuery(parsed) {
2112
3817
  }
2113
3818
  const text = parsed.command.slice(1).join(" ").trim();
2114
3819
  if (!text) {
2115
- return { response: fail("invalid_args", QUERY_USAGE, { remediation: "Try: embed query \"查一下我的额度\"." }) };
3820
+ return { response: fail("invalid_args", QUERY_USAGE, { remediation: "Try: embed query \"当前支持哪些开发板\"." }) };
2116
3821
  }
2117
3822
  const normalized = text.toLowerCase();
3823
+ if (isQuotaOrBillingIntent(normalized)) {
3824
+ return {
3825
+ response: fail("quota_not_supported", "当前 Embed Labs 服务器只提供 MCP 服务,不提供额度、充值、余额、账本或存储配额功能。", {
3826
+ remediation: "请使用 embedlabs auth status 查看登录和设备绑定状态,或在 MCP 客户端中直接调用开发板、工具链、知识库和本地硬件相关工具。"
3827
+ })
3828
+ };
3829
+ }
2118
3830
  const accountId = await queryAccountId(parsed);
2119
3831
  const needsAccount = queryNeedsAccount(normalized);
2120
3832
  if (needsAccount && !accountId.ok) {
@@ -2133,7 +3845,7 @@ async function naturalLanguageQuery(parsed) {
2133
3845
  if (!amountUsd) {
2134
3846
  return {
2135
3847
  response: fail("amount_required", "Recharge requests need an amount.", {
2136
- remediation: "Try: embed query \"用 USDC 链上充值 100 美元\"."
3848
+ remediation: "当前 MCP 服务不提供充值流程。"
2137
3849
  })
2138
3850
  };
2139
3851
  }
@@ -2226,13 +3938,8 @@ async function naturalLanguageQuery(parsed) {
2226
3938
  response: fail("query_intent_unknown", "I could not map that request to a stable CLI action yet.", {
2227
3939
  remediation: [
2228
3940
  "Supported examples:",
2229
- "embed query \"查一下我的额度\"",
2230
- "embed query \"用 USDC 链上充值 100 美元\"",
2231
- "embed query \"看一下 token 用量\"",
2232
- "embed query \"看一下磁盘空间\"",
2233
3941
  "embed query \"列出我的 API key\"",
2234
3942
  "embed query \"有哪些开发板模板\"",
2235
- "embed query \"当前可以用哪些模型\"",
2236
3943
  "embed query \"本地有哪些工具能力\""
2237
3944
  ].join("\n")
2238
3945
  })
@@ -2445,6 +4152,10 @@ function agentRunToolInputs(parsed) {
2445
4152
  if (remotePath.error) {
2446
4153
  return remotePath.error;
2447
4154
  }
4155
+ const runCommand = optionalTrimmedStringFlag(parsed, "run-command");
4156
+ if (runCommand.error) {
4157
+ return runCommand.error;
4158
+ }
2448
4159
  const user = optionalTrimmedStringFlag(parsed, "user");
2449
4160
  if (user.error) {
2450
4161
  return user.error;
@@ -2498,6 +4209,7 @@ function agentRunToolInputs(parsed) {
2498
4209
  user: user.value,
2499
4210
  artifact_path: artifact.value,
2500
4211
  remote_path: remotePath.value,
4212
+ run_command: runCommand.value,
2501
4213
  run: booleanFlag(parsed, "run") || undefined,
2502
4214
  timeout_seconds: timeout.value
2503
4215
  });
@@ -2578,13 +4290,15 @@ async function queryAccountId(parsed) {
2578
4290
  return { ok: true, value: auth.data.account_id };
2579
4291
  }
2580
4292
  function queryNeedsAccount(normalized) {
4293
+ return isApiKeyIntent(normalized);
4294
+ }
4295
+ function isQuotaOrBillingIntent(normalized) {
2581
4296
  return isRechargeIntent(normalized)
2582
4297
  || isBalanceIntent(normalized)
2583
4298
  || isUsageIntent(normalized)
2584
4299
  || isBillingStatementIntent(normalized)
2585
4300
  || isStorageSettlementIntent(normalized)
2586
- || isStorageIntent(normalized)
2587
- || isApiKeyIntent(normalized);
4301
+ || isStorageIntent(normalized);
2588
4302
  }
2589
4303
  function isRechargeIntent(normalized) {
2590
4304
  return /(充值|充钱|续费|购买额度|买额度|top\s*up|recharge|add credits|buy credits)/i.test(normalized);
@@ -3412,25 +5126,66 @@ function boardKnowledgeFileRequest(parsed) {
3412
5126
  if (source.error) {
3413
5127
  return source.error;
3414
5128
  }
3415
- if (!source.value || !["board_pack", "build_template", "registry"].includes(source.value)) {
3416
- return BOARD_KNOWLEDGE_FILE_USAGE;
3417
- }
3418
- const knowledgePath = optionalTrimmedStringFlag(parsed, "path");
3419
- if (knowledgePath.error) {
3420
- return knowledgePath.error;
5129
+ if (!source.value || !["board_pack", "build_template", "registry"].includes(source.value)) {
5130
+ return BOARD_KNOWLEDGE_FILE_USAGE;
5131
+ }
5132
+ const knowledgePath = optionalTrimmedStringFlag(parsed, "path");
5133
+ if (knowledgePath.error) {
5134
+ return knowledgePath.error;
5135
+ }
5136
+ if (!knowledgePath.value) {
5137
+ return BOARD_KNOWLEDGE_FILE_USAGE;
5138
+ }
5139
+ const outputPath = optionalTrimmedStringFlag(parsed, "output");
5140
+ if (outputPath.error) {
5141
+ return outputPath.error;
5142
+ }
5143
+ return {
5144
+ templateId,
5145
+ source: source.value,
5146
+ path: knowledgePath.value,
5147
+ outputPath: outputPath.value
5148
+ };
5149
+ }
5150
+ function boardKnowledgeSearchRequest(parsed) {
5151
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "query", "source", "limit"]);
5152
+ if (unknownFlag) {
5153
+ return `Unknown flag --${unknownFlag}. ${BOARD_KNOWLEDGE_SEARCH_USAGE}`;
5154
+ }
5155
+ const templateId = parsed.command[3]?.trim();
5156
+ if (!templateId) {
5157
+ return BOARD_KNOWLEDGE_SEARCH_USAGE;
5158
+ }
5159
+ const extra = parsed.command.slice(4);
5160
+ if (extra.length > 0) {
5161
+ return `Unexpected argument: ${extra[0]}. ${BOARD_KNOWLEDGE_SEARCH_USAGE}`;
5162
+ }
5163
+ const query = optionalTrimmedStringFlag(parsed, "query");
5164
+ if (query.error) {
5165
+ return query.error;
5166
+ }
5167
+ if (!query.value) {
5168
+ return BOARD_KNOWLEDGE_SEARCH_USAGE;
5169
+ }
5170
+ const source = optionalTrimmedStringFlag(parsed, "source");
5171
+ if (source.error) {
5172
+ return source.error;
5173
+ }
5174
+ if (source.value && !["board_pack", "build_template", "registry"].includes(source.value)) {
5175
+ return BOARD_KNOWLEDGE_SEARCH_USAGE;
3421
5176
  }
3422
- if (!knowledgePath.value) {
3423
- return BOARD_KNOWLEDGE_FILE_USAGE;
5177
+ const limit = optionalPositiveIntegerFlag(parsed, "limit");
5178
+ if (limit.error) {
5179
+ return limit.error;
3424
5180
  }
3425
- const outputPath = optionalTrimmedStringFlag(parsed, "output");
3426
- if (outputPath.error) {
3427
- return outputPath.error;
5181
+ if (limit.value !== undefined && limit.value > 10) {
5182
+ return "--limit must be between 1 and 10.";
3428
5183
  }
3429
5184
  return {
3430
5185
  templateId,
5186
+ query: query.value,
3431
5187
  source: source.value,
3432
- path: knowledgePath.value,
3433
- outputPath: outputPath.value
5188
+ limit: limit.value ?? 5
3434
5189
  };
3435
5190
  }
3436
5191
  function toolCallRequest(parsed) {
@@ -3480,7 +5235,7 @@ function isTaishanPiDeployCommand(parsed) {
3480
5235
  || area === "deploy";
3481
5236
  }
3482
5237
  function boardDeployTaishanPiRequest(parsed) {
3483
- const unknownFlag = firstUnknownFlag(parsed, ["json", "host", "user", "artifact", "artifact-path", "remote-path", "run", "timeout", "approve", "approved"]);
5238
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "host", "user", "artifact", "artifact-path", "remote-path", "run-command", "run", "timeout", "approve", "approved"]);
3484
5239
  if (unknownFlag) {
3485
5240
  return `Unknown flag --${unknownFlag}. ${BOARD_DEPLOY_TAISHANPI_USAGE}`;
3486
5241
  }
@@ -3522,6 +5277,10 @@ function boardDeployTaishanPiRequest(parsed) {
3522
5277
  if (remotePath.error) {
3523
5278
  return remotePath.error;
3524
5279
  }
5280
+ const runCommand = optionalTrimmedStringFlag(parsed, "run-command");
5281
+ if (runCommand.error) {
5282
+ return runCommand.error;
5283
+ }
3525
5284
  const timeout = optionalNonNegativeIntegerFlag(parsed, "timeout");
3526
5285
  if (timeout.error) {
3527
5286
  return timeout.error;
@@ -3536,6 +5295,7 @@ function boardDeployTaishanPiRequest(parsed) {
3536
5295
  user: user.value,
3537
5296
  artifact_path: artifact.value,
3538
5297
  remote_path: remotePath.value,
5298
+ run_command: runCommand.value,
3539
5299
  run: booleanFlag(parsed, "run") || undefined,
3540
5300
  timeout_seconds: timeout.value,
3541
5301
  approved: approval.value || undefined
@@ -3548,30 +5308,272 @@ async function authLogin(parsed) {
3548
5308
  return fail("invalid_args", "Usage: embed auth login --token <token> [--profile default] [--json]");
3549
5309
  }
3550
5310
  const updatedAt = new Date().toISOString();
3551
- await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
3552
- await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify({ profile, token, updated_at: updatedAt }, null, 2)}\n`, "utf8");
3553
- return ok({ authenticated: true, profile, source: "file", updated_at: updatedAt });
5311
+ const current = await readLocalAuthFile();
5312
+ const localDevice = await buildLocalDeviceAuth(parsed, current.device);
5313
+ const registration = await registerLocalDevice(token.trim(), localDevice.registration);
5314
+ if (!registration.ok) {
5315
+ return fail(registration.error.code, registration.error.message, {
5316
+ remediation: registration.error.remediation,
5317
+ details: registration.error.details
5318
+ });
5319
+ }
5320
+ const device = {
5321
+ device_id: registration.data.device.device_id,
5322
+ fingerprint_hash: localDevice.device.fingerprint_hash,
5323
+ private_key_pem: localDevice.device.private_key_pem,
5324
+ public_key_pem: localDevice.device.public_key_pem,
5325
+ label: registration.data.device.label ?? localDevice.device.label,
5326
+ platform: registration.data.device.platform ?? localDevice.device.platform,
5327
+ arch: registration.data.device.arch ?? localDevice.device.arch,
5328
+ hostname_hash: registration.data.device.hostname_hash ?? localDevice.device.hostname_hash,
5329
+ registered_at: registration.data.device.first_seen_at
5330
+ };
5331
+ await writeLocalAuthFile({
5332
+ profile,
5333
+ token: token.trim(),
5334
+ updated_at: updatedAt,
5335
+ account_id: registration.data.device.account_id,
5336
+ api_key_id: registration.data.device.api_key_id,
5337
+ device
5338
+ });
5339
+ return ok({
5340
+ authenticated: true,
5341
+ profile,
5342
+ source: "file",
5343
+ updated_at: updatedAt,
5344
+ account_id: registration.data.device.account_id,
5345
+ api_key_id: registration.data.device.api_key_id,
5346
+ device_id: device.device_id,
5347
+ device_fingerprint_hash: device.fingerprint_hash,
5348
+ device_label: device.label,
5349
+ device_registered_at: device.registered_at,
5350
+ device_private_key_configured: true
5351
+ });
3554
5352
  }
3555
5353
  async function authStatus() {
3556
- if (process.env.EMBED_API_TOKEN?.trim()) {
5354
+ const envToken = process.env.EMBED_API_TOKEN?.trim();
5355
+ const file = await readLocalAuthFile();
5356
+ const deviceIntegrity = file.device
5357
+ ? (await validateLocalDeviceIntegrity(file.device)).ok ? "ok" : "failed"
5358
+ : "unbound";
5359
+ if (envToken) {
3557
5360
  return {
3558
5361
  authenticated: true,
3559
5362
  profile: process.env.EMBED_AUTH_PROFILE ?? "default",
3560
- source: "env"
5363
+ source: "env",
5364
+ account_id: file.account_id,
5365
+ api_key_id: file.api_key_id,
5366
+ device_id: file.device?.device_id,
5367
+ device_fingerprint_hash: file.device?.fingerprint_hash,
5368
+ device_label: file.device?.label,
5369
+ device_registered_at: file.device?.registered_at,
5370
+ device_private_key_configured: Boolean(file.device?.private_key_pem),
5371
+ device_integrity: deviceIntegrity
3561
5372
  };
3562
5373
  }
5374
+ return {
5375
+ authenticated: Boolean(file.token?.trim()),
5376
+ profile: file.profile ?? "default",
5377
+ source: file.token ? "file" : undefined,
5378
+ updated_at: file.updated_at,
5379
+ account_id: file.account_id,
5380
+ api_key_id: file.api_key_id,
5381
+ device_id: file.device?.device_id,
5382
+ device_fingerprint_hash: file.device?.fingerprint_hash,
5383
+ device_label: file.device?.label,
5384
+ device_registered_at: file.device?.registered_at,
5385
+ device_private_key_configured: Boolean(file.device?.private_key_pem),
5386
+ device_integrity: deviceIntegrity
5387
+ };
5388
+ }
5389
+ async function authDeviceStatus(parsed) {
5390
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
5391
+ const unexpected = parsed.command.slice(3);
5392
+ if (unknownFlag || unexpected.length > 0) {
5393
+ return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_STATUS_USAGE}` : AUTH_DEVICE_STATUS_USAGE);
5394
+ }
5395
+ const local = await authStatus();
5396
+ const remote = local.authenticated ? await cloudGet("/v1/me/devices") : undefined;
5397
+ if (remote && !remote.ok) {
5398
+ return ok({ local });
5399
+ }
5400
+ return ok({ local, remote: remote?.data });
5401
+ }
5402
+ async function authDeviceList(parsed) {
5403
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
5404
+ const unexpected = parsed.command.slice(3);
5405
+ if (unknownFlag || unexpected.length > 0) {
5406
+ return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_LIST_USAGE}` : AUTH_DEVICE_LIST_USAGE);
5407
+ }
5408
+ return await cloudGet("/v1/me/devices");
5409
+ }
5410
+ async function authDeviceRevoke(parsed) {
5411
+ const unknownFlag = firstUnknownFlag(parsed, ["json"]);
5412
+ if (unknownFlag) {
5413
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_REVOKE_USAGE}`);
5414
+ }
5415
+ const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_REVOKE_USAGE);
5416
+ if (!id.ok) {
5417
+ return fail("invalid_args", id.error);
5418
+ }
5419
+ return await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}/revoke`, {});
5420
+ }
5421
+ async function authDeviceRename(parsed) {
5422
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "label"]);
5423
+ if (unknownFlag) {
5424
+ return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_RENAME_USAGE}`);
5425
+ }
5426
+ const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_RENAME_USAGE);
5427
+ if (!id.ok) {
5428
+ return fail("invalid_args", id.error);
5429
+ }
5430
+ const label = stringFlag(parsed, "label");
5431
+ if (!label?.trim()) {
5432
+ return fail("invalid_args", AUTH_DEVICE_RENAME_USAGE);
5433
+ }
5434
+ const updated = await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}`, { label: label.trim() });
5435
+ if (updated.ok) {
5436
+ const auth = await readLocalAuthFile();
5437
+ if (auth.device?.device_id === updated.data.device_id) {
5438
+ await writeLocalAuthFile({ ...auth, device: { ...auth.device, label: updated.data.label } });
5439
+ }
5440
+ }
5441
+ return updated;
5442
+ }
5443
+ async function buildLocalDeviceAuth(parsed, existing) {
5444
+ const fingerprint = await localHardwareFingerprint();
5445
+ const keyPair = existing?.fingerprint_hash === fingerprint.fingerprint_hash && existing.private_key_pem && existing.public_key_pem
5446
+ ? { privateKeyPem: existing.private_key_pem, publicKeyPem: existing.public_key_pem }
5447
+ : generateLocalDeviceKeyPair();
5448
+ const label = stringFlag(parsed, "label")?.trim()
5449
+ || existing?.label
5450
+ || `${fingerprint.platform} ${fingerprint.arch}`;
5451
+ const device = {
5452
+ device_id: existing?.fingerprint_hash === fingerprint.fingerprint_hash ? existing.device_id : "",
5453
+ fingerprint_hash: fingerprint.fingerprint_hash,
5454
+ private_key_pem: keyPair.privateKeyPem,
5455
+ public_key_pem: keyPair.publicKeyPem,
5456
+ label,
5457
+ platform: fingerprint.platform,
5458
+ arch: fingerprint.arch,
5459
+ hostname_hash: fingerprint.hostname_hash,
5460
+ registered_at: existing?.registered_at
5461
+ };
5462
+ return {
5463
+ device,
5464
+ registration: {
5465
+ fingerprint_hash: fingerprint.fingerprint_hash,
5466
+ public_key: keyPair.publicKeyPem,
5467
+ label,
5468
+ platform: fingerprint.platform,
5469
+ arch: fingerprint.arch,
5470
+ hostname_hash: fingerprint.hostname_hash,
5471
+ client_name: EMBED_CLIENT_NAME,
5472
+ client_version: EMBED_CLIENT_VERSION,
5473
+ metadata: {
5474
+ fingerprint_version: "v1",
5475
+ fingerprint_source: fingerprint.source
5476
+ }
5477
+ }
5478
+ };
5479
+ }
5480
+ function generateLocalDeviceKeyPair() {
5481
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
5482
+ return {
5483
+ privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
5484
+ publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString()
5485
+ };
5486
+ }
5487
+ async function registerLocalDevice(token, body) {
3563
5488
  try {
3564
- const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
3565
- return {
3566
- authenticated: typeof parsed.token === "string" && parsed.token.trim().length > 0,
3567
- profile: typeof parsed.profile === "string" ? parsed.profile : "default",
3568
- source: "file",
3569
- updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined
5489
+ const bodyText = JSON.stringify(body);
5490
+ const headers = {
5491
+ "content-type": "application/json",
5492
+ authorization: `Bearer ${token}`
3570
5493
  };
5494
+ addCloudRequestSignature(headers, "POST", "/v1/me/devices/register", bodyText, token);
5495
+ const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/me/devices/register`, {
5496
+ method: "POST",
5497
+ headers,
5498
+ body: bodyText
5499
+ });
5500
+ const parsed = await response.json();
5501
+ return enrichCloudAuthFailure(parsed, true);
5502
+ }
5503
+ catch (error) {
5504
+ return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
5505
+ remediation: `Check that embed cloud-api is running at ${DEFAULT_CLOUD_API_URL}. Start it with: npm run cloud-api`
5506
+ });
5507
+ }
5508
+ }
5509
+ async function localHardwareFingerprint() {
5510
+ cachedLocalHardwareFingerprint ??= localHardwareFingerprintUncached();
5511
+ return await cachedLocalHardwareFingerprint;
5512
+ }
5513
+ async function localHardwareFingerprintUncached() {
5514
+ const platformName = platform();
5515
+ const archName = arch();
5516
+ const raw = await localHardwareId(platformName);
5517
+ const fingerprintHash = createHash("sha256")
5518
+ .update(`embed-labs:device:v1:${platformName}:${raw.value}`)
5519
+ .digest("hex");
5520
+ const hostnameHash = createHash("sha256")
5521
+ .update(`embed-labs:hostname:v1:${hostname()}`)
5522
+ .digest("hex");
5523
+ return {
5524
+ fingerprint_hash: fingerprintHash,
5525
+ platform: platformName,
5526
+ arch: archName,
5527
+ hostname_hash: hostnameHash,
5528
+ source: raw.source
5529
+ };
5530
+ }
5531
+ async function localHardwareId(platformName) {
5532
+ if (platformName === "darwin") {
5533
+ const result = await runLocalProcess("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
5534
+ const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(result.stdout);
5535
+ if (match?.[1]) {
5536
+ return { value: match[1], source: "macos_ioplatformuuid" };
5537
+ }
5538
+ }
5539
+ if (platformName === "win32") {
5540
+ const result = await runLocalProcess("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
5541
+ const match = /MachineGuid\s+REG_\w+\s+([^\r\n]+)/.exec(result.stdout);
5542
+ if (match?.[1]?.trim()) {
5543
+ return { value: match[1].trim(), source: "windows_machineguid" };
5544
+ }
5545
+ }
5546
+ if (platformName === "linux") {
5547
+ for (const pathValue of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
5548
+ try {
5549
+ const value = (await readFile(pathValue, "utf8")).trim();
5550
+ if (value) {
5551
+ return { value, source: `linux:${pathValue}` };
5552
+ }
5553
+ }
5554
+ catch {
5555
+ // Try the next stable machine id location.
5556
+ }
5557
+ }
5558
+ }
5559
+ const generated = await localGeneratedInstallId();
5560
+ return { value: generated, source: "generated_install_id" };
5561
+ }
5562
+ async function localGeneratedInstallId() {
5563
+ try {
5564
+ const parsed = JSON.parse(await readFile(DEFAULT_DEVICE_FILE, "utf8"));
5565
+ if (typeof parsed.generated_install_id === "string" && parsed.generated_install_id.trim()) {
5566
+ return parsed.generated_install_id.trim();
5567
+ }
3571
5568
  }
3572
5569
  catch {
3573
- return { authenticated: false, profile: "default" };
5570
+ // Fall through and create a local-only fallback id.
3574
5571
  }
5572
+ const generated = `install_${randomBytes(24).toString("base64url")}`;
5573
+ await mkdir(dirname(DEFAULT_DEVICE_FILE), { recursive: true });
5574
+ await writeFile(DEFAULT_DEVICE_FILE, `${JSON.stringify({ generated_install_id: generated, created_at: new Date().toISOString() }, null, 2)}\n`, "utf8");
5575
+ await chmod(DEFAULT_DEVICE_FILE, 0o600).catch(() => undefined);
5576
+ return generated;
3575
5577
  }
3576
5578
  function accountCreateBody(parsed) {
3577
5579
  const unknownFlag = firstUnknownFlag(parsed, ["json", "email", "display-name"]);
@@ -3737,6 +5739,8 @@ function mcpToolEventBody(parsed) {
3737
5739
  "mode",
3738
5740
  "server-model-used",
3739
5741
  "success",
5742
+ "local-device-id",
5743
+ "local_device_id",
3740
5744
  "request-id",
3741
5745
  "duration-ms",
3742
5746
  "input-summary",
@@ -3763,6 +5767,9 @@ function mcpToolEventBody(parsed) {
3763
5767
  const modeResult = optionalTrimmedStringFlag(parsed, "mode");
3764
5768
  if (modeResult.error)
3765
5769
  return modeResult.error;
5770
+ const localDeviceResult = optionalTrimmedStringAliasFlag(parsed, ["local-device-id", "local_device_id"], "local-device-id");
5771
+ if (localDeviceResult.error)
5772
+ return localDeviceResult.error;
3766
5773
  const requestIdResult = optionalTrimmedStringFlag(parsed, "request-id");
3767
5774
  if (requestIdResult.error)
3768
5775
  return requestIdResult.error;
@@ -3786,12 +5793,14 @@ function mcpToolEventBody(parsed) {
3786
5793
  tool_name: toolResult.value,
3787
5794
  client: clientResult.value,
3788
5795
  mode: modeResult.value,
5796
+ local_device_id: localDeviceResult.value,
3789
5797
  server_model_used: serverModelUsed,
3790
5798
  success,
3791
5799
  request_id: requestIdResult.value,
3792
5800
  duration_ms: durationResult.value,
3793
5801
  input_summary: inputSummaryResult.value,
3794
- output_summary: outputSummaryResult.value
5802
+ output_summary: outputSummaryResult.value,
5803
+ metadata: localDeviceResult.value ? { local_device_id: localDeviceResult.value } : undefined
3795
5804
  });
3796
5805
  }
3797
5806
  function usageSummaryRequest(parsed) {
@@ -4129,6 +6138,29 @@ function billingSnapshotListRequest(parsed) {
4129
6138
  }
4130
6139
  return { path: `/v1/accounts/${encodeURIComponent(accountResult.value)}/billing/snapshots` };
4131
6140
  }
6141
+ function localToolchainListRequest(parsed, usage = LOCAL_TOOLCHAIN_LIST_USAGE) {
6142
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "install-root"]);
6143
+ if (unknownFlag) {
6144
+ return `Unknown flag --${unknownFlag}. ${usage}`;
6145
+ }
6146
+ const extra = parsed.command.slice(3);
6147
+ if (extra.length > 0) {
6148
+ return `Unexpected argument: ${extra[0]}. ${usage}`;
6149
+ }
6150
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
6151
+ if (board.error)
6152
+ return board.error;
6153
+ const channel = optionalTrimmedStringFlag(parsed, "channel");
6154
+ if (channel.error)
6155
+ return channel.error;
6156
+ const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
6157
+ if (metadataRoot.error)
6158
+ return metadataRoot.error;
6159
+ const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
6160
+ if (installRoot.error)
6161
+ return installRoot.error;
6162
+ return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value, installRoot: installRoot.value };
6163
+ }
4132
6164
  function localToolchainLatestRequest(parsed) {
4133
6165
  const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root"]);
4134
6166
  if (unknownFlag) {
@@ -4149,6 +6181,32 @@ function localToolchainLatestRequest(parsed) {
4149
6181
  return metadataRoot.error;
4150
6182
  return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value };
4151
6183
  }
6184
+ function localWslInstallRequest(parsed) {
6185
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "distribution", "distro", "no-launch", "web-download", "timeout-ms"]);
6186
+ if (unknownFlag) {
6187
+ return `Unknown flag --${unknownFlag}. ${LOCAL_WSL_INSTALL_USAGE}`;
6188
+ }
6189
+ const extra = parsed.command.slice(3);
6190
+ if (extra.length > 0) {
6191
+ return `Unexpected argument: ${extra[0]}. ${LOCAL_WSL_INSTALL_USAGE}`;
6192
+ }
6193
+ const distribution = stringFlag(parsed, "distribution") ?? stringFlag(parsed, "distro");
6194
+ const noLaunch = optionalBooleanFlag(parsed, "no-launch");
6195
+ if (typeof noLaunch === "string")
6196
+ return noLaunch;
6197
+ const webDownload = optionalBooleanFlag(parsed, "web-download");
6198
+ if (typeof webDownload === "string")
6199
+ return webDownload;
6200
+ const timeoutMs = optionalIntegerFlag(parsed, "timeout-ms", 1_000, 3_600_000);
6201
+ if (timeoutMs.error)
6202
+ return timeoutMs.error;
6203
+ return {
6204
+ distribution,
6205
+ noLaunch,
6206
+ webDownload,
6207
+ timeoutMs: timeoutMs.value
6208
+ };
6209
+ }
4152
6210
  function localToolchainCurrentRequest(parsed) {
4153
6211
  const unknownFlag = firstUnknownFlag(parsed, ["json", "install-root"]);
4154
6212
  if (unknownFlag) {
@@ -4164,7 +6222,7 @@ function localToolchainCurrentRequest(parsed) {
4164
6222
  return { installRoot: installRoot.value };
4165
6223
  }
4166
6224
  function localToolchainInstallRequest(parsed) {
4167
- const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "force"]);
6225
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "mode", "force"]);
4168
6226
  if (unknownFlag) {
4169
6227
  return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_INSTALL_USAGE}`;
4170
6228
  }
@@ -4193,6 +6251,9 @@ function localToolchainInstallRequest(parsed) {
4193
6251
  const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
4194
6252
  if (installRoot.error)
4195
6253
  return installRoot.error;
6254
+ const mode = optionalTrimmedStringFlag(parsed, "mode");
6255
+ if (mode.error)
6256
+ return mode.error;
4196
6257
  return {
4197
6258
  boardId: board.value,
4198
6259
  channel: channel.value,
@@ -4200,11 +6261,32 @@ function localToolchainInstallRequest(parsed) {
4200
6261
  sourceUrl: sourceUrl.value,
4201
6262
  sourceReleaseRoot: sourceReleaseRoot.value,
4202
6263
  installRoot: installRoot.value,
6264
+ mode: mode.value,
4203
6265
  force: booleanFlag(parsed, "force")
4204
6266
  };
4205
6267
  }
6268
+ function localToolchainUninstallRequest(parsed) {
6269
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "install-root", "yes", "force"]);
6270
+ if (unknownFlag) {
6271
+ return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_UNINSTALL_USAGE}`;
6272
+ }
6273
+ const extra = parsed.command.slice(3);
6274
+ if (extra.length > 0) {
6275
+ return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_UNINSTALL_USAGE}`;
6276
+ }
6277
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
6278
+ if (board.error)
6279
+ return board.error;
6280
+ if (!board.value) {
6281
+ return LOCAL_TOOLCHAIN_UNINSTALL_USAGE;
6282
+ }
6283
+ const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
6284
+ if (installRoot.error)
6285
+ return installRoot.error;
6286
+ return { boardId: board.value, installRoot: installRoot.value };
6287
+ }
4206
6288
  function localToolchainValidateRequest(parsed) {
4207
- const unknownFlag = firstUnknownFlag(parsed, ["json", "release-root"]);
6289
+ const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "release-root", "mode"]);
4208
6290
  if (unknownFlag) {
4209
6291
  return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_VALIDATE_USAGE}`;
4210
6292
  }
@@ -4216,7 +6298,15 @@ function localToolchainValidateRequest(parsed) {
4216
6298
  if (releaseRoot.error) {
4217
6299
  return releaseRoot.error;
4218
6300
  }
4219
- return { releaseRoot: releaseRoot.value };
6301
+ const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
6302
+ if (board.error) {
6303
+ return board.error;
6304
+ }
6305
+ const mode = optionalTrimmedStringFlag(parsed, "mode");
6306
+ if (mode.error) {
6307
+ return mode.error;
6308
+ }
6309
+ return { releaseRoot: releaseRoot.value, mode: mode.value, boardId: board.value };
4220
6310
  }
4221
6311
  function localCompileTaishanPiRequest(parsed, auth) {
4222
6312
  const unknownFlag = firstUnknownFlag(parsed, ["json", "source", "output", "release-root", "account", "account-id"]);
@@ -4301,7 +6391,9 @@ function localToolchainAuthContext(auth, accountId) {
4301
6391
  authenticated: auth.authenticated,
4302
6392
  profile: auth.profile,
4303
6393
  source: auth.source,
4304
- account_id: accountId
6394
+ account_id: accountId,
6395
+ api_key_id: auth.api_key_id,
6396
+ device_id: auth.device_id
4305
6397
  };
4306
6398
  }
4307
6399
  function usageEventsRequest(parsed) {
@@ -4642,6 +6734,25 @@ function renderCloudStatus(data) {
4642
6734
  const item = data;
4643
6735
  return `${item.service ?? "embed-cloud-api"} ${item.version ?? ""} is ${item.status ?? "unknown"}${item.time ? ` at ${item.time}` : ""}.`;
4644
6736
  }
6737
+ function renderMcpConfig(result) {
6738
+ const lines = [
6739
+ `MCP configuration for ${result.client}:`,
6740
+ JSON.stringify(result.config, null, 2)
6741
+ ];
6742
+ if (result.install_command) {
6743
+ lines.push("", `Recommended installer: ${result.install_command}`);
6744
+ }
6745
+ if (result.config_path_hint) {
6746
+ lines.push(`Config path hint: ${result.config_path_hint}`);
6747
+ }
6748
+ if (result.notes.length > 0) {
6749
+ lines.push("", "Notes:");
6750
+ for (const note of result.notes) {
6751
+ lines.push(` - ${note}`);
6752
+ }
6753
+ }
6754
+ return lines.join("\n");
6755
+ }
4645
6756
  function renderModelCatalog(catalog) {
4646
6757
  const lines = [
4647
6758
  `default=${catalog.default_provider}/${catalog.default_model}`,
@@ -4711,6 +6822,35 @@ function renderPluginList(result) {
4711
6822
  `install="${plugin.install_command}"`
4712
6823
  ].filter(Boolean).join(" ")).join("\n");
4713
6824
  }
6825
+ function renderPluginUpdateCheck(result) {
6826
+ const lines = [
6827
+ `release_url=${result.release_url}`,
6828
+ result.latest_version ? `latest_version=${result.latest_version}` : ""
6829
+ ].filter(Boolean);
6830
+ if (result.release_notes.length > 0) {
6831
+ lines.push("release_notes:");
6832
+ for (const note of result.release_notes) {
6833
+ lines.push(` - ${note}`);
6834
+ }
6835
+ }
6836
+ for (const plugin of result.plugins) {
6837
+ lines.push("");
6838
+ lines.push(`${plugin.display_name} (${plugin.id})`);
6839
+ lines.push(` installed=${plugin.installed}`);
6840
+ lines.push(` installed_version=${plugin.installed_version ?? "none"}`);
6841
+ lines.push(` latest_version=${plugin.latest_version ?? "unknown"}`);
6842
+ lines.push(` update_available=${plugin.update_available}`);
6843
+ lines.push(` target=${plugin.target_path}`);
6844
+ lines.push(` update_command=${plugin.update_command}`);
6845
+ if (plugin.release_file) {
6846
+ lines.push(` release_file=${plugin.release_file}`);
6847
+ }
6848
+ for (const note of plugin.notes) {
6849
+ lines.push(` note=${note}`);
6850
+ }
6851
+ }
6852
+ return lines.join("\n");
6853
+ }
4714
6854
  function renderPluginInstall(result) {
4715
6855
  const lines = ["Installed plugins:"];
4716
6856
  for (const item of result.installed) {
@@ -4732,6 +6872,15 @@ function renderPluginInstall(result) {
4732
6872
  if (item.mcp_warning) {
4733
6873
  lines.push(` warning=${item.mcp_warning}`);
4734
6874
  }
6875
+ if (item.marketplace_registered !== undefined) {
6876
+ lines.push(` codex_marketplace_registered=${item.marketplace_registered}`);
6877
+ }
6878
+ if (item.marketplace_path) {
6879
+ lines.push(` codex_marketplace=${item.marketplace_path}`);
6880
+ }
6881
+ if (item.marketplace_warning) {
6882
+ lines.push(` warning=${item.marketplace_warning}`);
6883
+ }
4735
6884
  }
4736
6885
  return lines.join("\n");
4737
6886
  }
@@ -4763,9 +6912,53 @@ function renderAgentRunResult(result) {
4763
6912
  return lines.join("\n");
4764
6913
  }
4765
6914
  function renderAuthStatus(status) {
4766
- return status.authenticated
4767
- ? `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`
4768
- : `Not authenticated profile=${status.profile}`;
6915
+ if (!status.authenticated) {
6916
+ return `Not authenticated profile=${status.profile}`;
6917
+ }
6918
+ return [
6919
+ `Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`,
6920
+ status.account_id ? `account=${status.account_id}` : "",
6921
+ status.api_key_id ? `api_key=${status.api_key_id}` : "",
6922
+ status.device_id ? `device=${status.device_id}` : "device=not_registered",
6923
+ status.device_label ? `device_label=${status.device_label}` : "",
6924
+ status.device_integrity ? `device_integrity=${status.device_integrity}` : "",
6925
+ status.device_private_key_configured === false ? "device_private_key=missing" : ""
6926
+ ].filter(Boolean).join("\n");
6927
+ }
6928
+ function renderAuthDeviceStatus(status) {
6929
+ const lines = [renderAuthStatus(status.local)];
6930
+ if (status.remote) {
6931
+ const activeCount = status.remote.devices.filter((device) => device.status === "active").length;
6932
+ lines.push(`remote_devices=${activeCount}/${status.remote.device_limit}`);
6933
+ const localDevice = status.local.device_id
6934
+ ? status.remote.devices.find((device) => device.device_id === status.local.device_id)
6935
+ : undefined;
6936
+ if (localDevice) {
6937
+ lines.push(`remote_current=${renderAuthDevice(localDevice)}`);
6938
+ }
6939
+ }
6940
+ return lines.join("\n");
6941
+ }
6942
+ function renderAuthDeviceList(result) {
6943
+ if (result.devices.length === 0) {
6944
+ return `No registered devices. device_limit=${result.device_limit}`;
6945
+ }
6946
+ return [
6947
+ `device_limit=${result.device_limit}`,
6948
+ ...result.devices.map(renderAuthDevice)
6949
+ ].join("\n");
6950
+ }
6951
+ function renderAuthDevice(device) {
6952
+ return [
6953
+ `${device.device_id} account=${device.account_id}`,
6954
+ device.api_key_id ? `api_key=${device.api_key_id}` : "",
6955
+ `status=${device.status}`,
6956
+ device.label ? `label=${device.label}` : "",
6957
+ device.platform ? `platform=${device.platform}` : "",
6958
+ device.arch ? `arch=${device.arch}` : "",
6959
+ `last_seen_at=${device.last_seen_at}`,
6960
+ device.revoked_at ? `revoked_at=${device.revoked_at}` : ""
6961
+ ].filter(Boolean).join(" ");
4769
6962
  }
4770
6963
  function renderAccount(account) {
4771
6964
  return [
@@ -5140,6 +7333,20 @@ function renderBoardKnowledge(data) {
5140
7333
  `title=${file.title}`
5141
7334
  ].join(" ")).join("\n");
5142
7335
  }
7336
+ function renderBoardKnowledgeSearch(data) {
7337
+ const result = data;
7338
+ const matches = Array.isArray(result.matches) ? result.matches : [];
7339
+ if (matches.length === 0) {
7340
+ return "No matching board knowledge snippets.";
7341
+ }
7342
+ return matches.map((match, index) => [
7343
+ `#${index + 1}`,
7344
+ `${match.source}:${match.path}`,
7345
+ `score=${match.score}`,
7346
+ `title=${match.title}`,
7347
+ `excerpt=${match.excerpt.replace(/\s+/g, " ").trim()}`
7348
+ ].join(" ")).join("\n");
7349
+ }
5143
7350
  function renderBoardKnowledgeFile(file) {
5144
7351
  return [
5145
7352
  `template=${file.template_id}`,
@@ -5352,6 +7559,194 @@ function renderBuildWorkspaceSourcePatch(result) {
5352
7559
  }
5353
7560
  return lines.join("\n");
5354
7561
  }
7562
+ function renderLocalToolchainList(result) {
7563
+ const installedCount = result.environments.filter((environment) => !!environment.installed).length;
7564
+ const availableCount = result.environments.filter((environment) => environment.status === "available").length;
7565
+ const updateCount = result.environments.filter((environment) => environment.status === "update_available").length;
7566
+ const lines = [
7567
+ `Local development environments: ${result.environments.length}`,
7568
+ `installed=${installedCount} available=${availableCount} updates=${updateCount}`,
7569
+ `host=${result.host}`,
7570
+ `channel=${result.channel}`,
7571
+ result.metadata_source === "local_override" ? `metadata_override=${result.metadata_root}` : "metadata=production/built-in",
7572
+ `install_root=${result.install_root}`,
7573
+ `registry=${result.registry_path}`
7574
+ ];
7575
+ for (const environment of result.environments) {
7576
+ lines.push("");
7577
+ lines.push(`${environment.display_name} (${environment.board_id})`);
7578
+ lines.push(` status=${localToolchainStatusLabel(environment.status)}`);
7579
+ lines.push(` latest=${environment.latest.version}`);
7580
+ if (environment.installed) {
7581
+ lines.push(` installed=${environment.installed.version ?? "unknown"} mode=${environment.installed.mode ?? "unknown"}`);
7582
+ if (environment.installed.release_root) {
7583
+ lines.push(` release_root=${environment.installed.release_root}`);
7584
+ }
7585
+ }
7586
+ lines.push(` install_modes=${environment.install_modes.join(",")}`);
7587
+ if (environment.execution) {
7588
+ lines.push(` execution=${environment.execution.kind} supported=${environment.execution.supported}`);
7589
+ if (environment.execution.required_host) {
7590
+ lines.push(` execution_required_host=${environment.execution.required_host}`);
7591
+ }
7592
+ if (environment.execution.actual_host) {
7593
+ lines.push(` execution_actual_host=${environment.execution.actual_host}`);
7594
+ }
7595
+ if (environment.execution.reason) {
7596
+ lines.push(` execution_reason=${environment.execution.reason}`);
7597
+ }
7598
+ }
7599
+ lines.push(` install_command=${environment.install_command}`);
7600
+ if (environment.status === "update_available") {
7601
+ lines.push(` update_command=${environment.update_command}`);
7602
+ }
7603
+ if (environment.components?.length) {
7604
+ lines.push(` component_catalog=${localToolchainComponentSummary(environment.components)}`);
7605
+ if (environment.latest.default_mode) {
7606
+ lines.push(` default_mode_download=${environment.latest.default_mode}: ${localToolchainComponentSummaryForMode(environment.components, environment.latest.default_mode)}`);
7607
+ }
7608
+ if (environment.installed?.mode && environment.installed.mode !== environment.latest.default_mode) {
7609
+ lines.push(` installed_mode_components=${environment.installed.mode}: ${localToolchainComponentSummaryForMode(environment.components, environment.installed.mode)}`);
7610
+ }
7611
+ lines.push(` mode_downloads=${localToolchainModeSummaries(environment.install_modes, environment.components).join("; ")}`);
7612
+ lines.push(` package_groups=${localToolchainComponentGroups(environment.components).join(", ")}`);
7613
+ lines.push(` detail_command=embedlabs local toolchain latest --board ${environment.board_id}${result.channel === "stable" ? "" : ` --channel ${result.channel}`}`);
7614
+ }
7615
+ if (environment.notes.length > 0) {
7616
+ for (const note of environment.notes) {
7617
+ lines.push(` note=${note}`);
7618
+ }
7619
+ }
7620
+ }
7621
+ return lines.join("\n");
7622
+ }
7623
+ function localToolchainStatusLabel(status) {
7624
+ if (status === "installed")
7625
+ return "installed/已安装";
7626
+ if (status === "available")
7627
+ return "available/可安装";
7628
+ if (status === "update_available")
7629
+ return "update_available/可更新";
7630
+ if (status === "unsupported_host")
7631
+ return "unsupported_host/当前系统暂不支持";
7632
+ return status;
7633
+ }
7634
+ function renderWindowsWslStatus(result) {
7635
+ const lines = [
7636
+ "Windows WSL2 status",
7637
+ `host=${result.host}`,
7638
+ `applicable=${result.applicable}`,
7639
+ `wsl_available=${result.wsl_available}`,
7640
+ `usable=${result.usable}`,
7641
+ `taishanpi_execution=${result.taishanpi_execution.status} supported=${result.taishanpi_execution.supported}`,
7642
+ `taishanpi_required_host=${result.taishanpi_execution.required_host}`,
7643
+ result.taishanpi_execution.actual_host ? `taishanpi_actual_host=${result.taishanpi_execution.actual_host}` : "",
7644
+ `taishanpi_reason=${result.taishanpi_execution.reason}`,
7645
+ `checked_at=${result.checked_at}`,
7646
+ `status_command=${result.commands.status}`,
7647
+ `list_command=${result.commands.list}`,
7648
+ `list_online_command=${result.commands.list_online}`,
7649
+ `install_command=${result.commands.install_ubuntu}`
7650
+ ].filter(Boolean);
7651
+ if (result.distributions.length > 0) {
7652
+ lines.push("distributions:");
7653
+ for (const distro of result.distributions) {
7654
+ lines.push(` ${distro.default ? "*" : "-"} ${distro.name} state=${distro.state ?? "unknown"} version=${distro.version ?? "unknown"}`);
7655
+ }
7656
+ }
7657
+ if (result.online_distributions.length > 0) {
7658
+ lines.push("online_distributions:");
7659
+ for (const distro of result.online_distributions) {
7660
+ lines.push(` ${distro.default ? "*" : "-"} ${distro.name}${distro.friendly_name ? ` (${distro.friendly_name})` : ""}`);
7661
+ }
7662
+ }
7663
+ for (const note of result.notes) {
7664
+ lines.push(`note=${note}`);
7665
+ }
7666
+ return lines.join("\n");
7667
+ }
7668
+ function renderWindowsWslInstall(result) {
7669
+ const lines = [
7670
+ "Windows WSL2 install",
7671
+ `host=${result.host}`,
7672
+ `command=${result.command.join(" ")}`,
7673
+ `exit_code=${result.exit_code}`,
7674
+ `usable_after=${result.status_after.usable}`
7675
+ ];
7676
+ if (result.stdout_tail.length > 0) {
7677
+ lines.push("stdout_tail:");
7678
+ for (const line of result.stdout_tail) {
7679
+ lines.push(` ${line}`);
7680
+ }
7681
+ }
7682
+ if (result.stderr_tail.length > 0) {
7683
+ lines.push("stderr_tail:");
7684
+ for (const line of result.stderr_tail) {
7685
+ lines.push(` ${line}`);
7686
+ }
7687
+ }
7688
+ for (const note of result.notes) {
7689
+ lines.push(`note=${note}`);
7690
+ }
7691
+ return lines.join("\n");
7692
+ }
7693
+ function localToolchainComponentSummary(components) {
7694
+ const totalBytes = components.reduce((total, component) => total + component.size_bytes, 0);
7695
+ return `${components.length} components, ${formatByteSize(totalBytes)}`;
7696
+ }
7697
+ function localToolchainComponentSummaryForMode(components, mode) {
7698
+ return localToolchainComponentSummary(localToolchainComponentsForMode(components, mode));
7699
+ }
7700
+ function localToolchainModeSummaries(modes, components) {
7701
+ return modes.map((mode) => `${mode}=${localToolchainComponentSummaryForMode(components, mode)}`);
7702
+ }
7703
+ function localToolchainComponentsForMode(components, mode) {
7704
+ return components.filter((component) => {
7705
+ if (!component.install_modes?.length) {
7706
+ return true;
7707
+ }
7708
+ return component.install_modes.includes(mode);
7709
+ });
7710
+ }
7711
+ function localToolchainComponentGroups(components) {
7712
+ const groups = new Set();
7713
+ for (const component of components) {
7714
+ const text = `${component.id} ${component.role ?? ""}`.toLowerCase();
7715
+ if (text.includes("arm-none-eabi") || text.includes("bare-metal") || text.includes("compiler"))
7716
+ groups.add("compiler/ARM 裸机编译器");
7717
+ if (text.includes("pico-sdk") || text.includes("sdk-core"))
7718
+ groups.add("pico-sdk/C/C++ SDK");
7719
+ if (text.includes("sysroot") || text.includes("cross"))
7720
+ groups.add("sysroot/交叉运行库");
7721
+ if (text.includes("qt"))
7722
+ groups.add("qt/Qt 应用支持");
7723
+ if (text.includes("rockchip") || text.includes("boot") || text.includes("resource"))
7724
+ groups.add("boot-flash/启动与烧写工具");
7725
+ if (text.includes("image") || text.includes("rootfs"))
7726
+ groups.add("images/镜像资源");
7727
+ if (text.includes("initial-firmware"))
7728
+ groups.add("initial-firmware/初始化镜像");
7729
+ if (text.includes("rp2350-monitor"))
7730
+ groups.add("rp2350-monitor/可选硬件监控镜像");
7731
+ if (text.includes("meta"))
7732
+ groups.add("metadata/知识与脚本元数据");
7733
+ }
7734
+ return groups.size > 0 ? [...groups] : ["runtime/运行时工具"];
7735
+ }
7736
+ function formatByteSize(bytes) {
7737
+ if (!Number.isFinite(bytes) || bytes < 0) {
7738
+ return "unknown size";
7739
+ }
7740
+ const units = ["B", "KB", "MB", "GB", "TB"];
7741
+ let value = bytes;
7742
+ let unit = 0;
7743
+ while (value >= 1024 && unit < units.length - 1) {
7744
+ value /= 1024;
7745
+ unit += 1;
7746
+ }
7747
+ const fixed = unit === 0 || value >= 10 ? value.toFixed(0) : value.toFixed(1);
7748
+ return `${fixed} ${units[unit]}`;
7749
+ }
5355
7750
  function renderLocalToolchainLatest(result) {
5356
7751
  const lines = [
5357
7752
  `board=${result.board_id}`,
@@ -5359,11 +7754,26 @@ function renderLocalToolchainLatest(result) {
5359
7754
  `version=${result.version}`,
5360
7755
  `host=${result.host}`,
5361
7756
  result.metadata_root ? `metadata_root=${result.metadata_root}` : "metadata=built-in",
5362
- result.download ? `download=${result.download.mirror_kind}:${result.download.source_url}` : "",
5363
- result.download ? `archive_sha256=${result.download.archive.sha256}` : "",
5364
- result.download ? `archive_size_bytes=${result.download.archive.size_bytes}` : "",
7757
+ result.download?.source_url ? `download=${result.download.mirror_kind}:${result.download.source_url}` : "",
7758
+ result.download?.archive ? `archive_sha256=${result.download.archive.sha256}` : "",
7759
+ result.download?.archive ? `archive_size_bytes=${result.download.archive.size_bytes}` : "",
7760
+ result.download?.components?.length ? `components=${result.download.components.length}` : "",
7761
+ result.download?.default_mode ? `default_mode=${result.download.default_mode}` : "",
5365
7762
  result.download_error ? `download_error=${result.download_error}` : ""
5366
7763
  ].filter(Boolean);
7764
+ if (result.download?.components?.length) {
7765
+ const modes = [...new Set(result.download.components.flatMap((component) => component.install_modes ?? ["all"]))].filter((mode) => mode !== "all");
7766
+ if (result.download.default_mode) {
7767
+ lines.push(`default_mode_download=${result.download.default_mode}: ${localToolchainDownloadComponentSummaryForMode(result.download.components, result.download.default_mode)}`);
7768
+ }
7769
+ if (modes.length > 0) {
7770
+ lines.push(`mode_downloads=${modes.map((mode) => `${mode}=${localToolchainDownloadComponentSummaryForMode(result.download?.components ?? [], mode)}`).join("; ")}`);
7771
+ }
7772
+ lines.push("download_components:");
7773
+ for (const component of result.download.components) {
7774
+ lines.push(` ${component.id}@${component.version} modes=${component.install_modes?.join(",") || "all"} size=${formatByteSize(component.archive.size_bytes)} file=${component.archive.file}`);
7775
+ }
7776
+ }
5367
7777
  if (result.packages.length > 0) {
5368
7778
  lines.push("packages:");
5369
7779
  for (const pkg of result.packages) {
@@ -5372,6 +7782,11 @@ function renderLocalToolchainLatest(result) {
5372
7782
  }
5373
7783
  return lines.join("\n");
5374
7784
  }
7785
+ function localToolchainDownloadComponentSummaryForMode(components, mode) {
7786
+ const selected = localToolchainComponentsForMode(components ?? [], mode);
7787
+ const totalBytes = selected.reduce((total, component) => total + component.archive.size_bytes, 0);
7788
+ return `${selected.length} components, ${formatByteSize(totalBytes)}`;
7789
+ }
5375
7790
  function renderLocalToolchainCurrent(result) {
5376
7791
  if (!result.installed) {
5377
7792
  return [
@@ -5386,6 +7801,7 @@ function renderLocalToolchainCurrent(result) {
5386
7801
  `board=${result.board_id}`,
5387
7802
  result.version ? `version=${result.version}` : "",
5388
7803
  result.channel ? `channel=${result.channel}` : "",
7804
+ result.mode ? `mode=${result.mode}` : "",
5389
7805
  result.release_root ? `release_root=${result.release_root}` : "",
5390
7806
  `install_root=${result.install_root}`,
5391
7807
  `registry=${result.registry_path}`
@@ -5398,19 +7814,33 @@ function renderLocalToolchainInstall(result) {
5398
7814
  `version=${result.version}`,
5399
7815
  `channel=${result.channel}`,
5400
7816
  `host=${result.host}`,
7817
+ `mode=${result.mode}`,
5401
7818
  `install_root=${result.install_root}`,
5402
7819
  `release_root=${result.release_root}`,
5403
7820
  `registry=${result.registry_path}`,
5404
7821
  `source=${result.source.kind}:${result.source.value}`,
5405
7822
  result.source.downloaded_path ? `downloaded=${result.source.downloaded_path}` : "",
7823
+ result.source.components?.length ? `components=${result.source.components.length}` : "",
5406
7824
  `validation=${result.validation.ok ? "ok" : "failed"}`
5407
7825
  ].filter(Boolean);
7826
+ if (result.source.components?.length) {
7827
+ lines.push("installed_components:");
7828
+ for (const component of result.source.components) {
7829
+ lines.push(` ${component.id}@${component.version} ${component.mirror_kind || ""} bytes=${component.size_bytes}`);
7830
+ }
7831
+ }
5408
7832
  if (result.installed_paths.length > 0) {
5409
7833
  lines.push("installed_paths:");
5410
7834
  for (const installedPath of result.installed_paths) {
5411
7835
  lines.push(` ${installedPath}`);
5412
7836
  }
5413
7837
  }
7838
+ if (result.removed_old_versions.length > 0) {
7839
+ lines.push("removed_old_versions:");
7840
+ for (const removedPath of result.removed_old_versions) {
7841
+ lines.push(` ${removedPath}`);
7842
+ }
7843
+ }
5414
7844
  if (result.packages.length > 0) {
5415
7845
  lines.push("packages:");
5416
7846
  for (const pkg of result.packages) {
@@ -5419,16 +7849,50 @@ function renderLocalToolchainInstall(result) {
5419
7849
  }
5420
7850
  return lines.join("\n");
5421
7851
  }
7852
+ function renderLocalToolchainUninstall(result) {
7853
+ const lines = [
7854
+ result.removed ? "Local toolchain uninstalled." : "Local toolchain was not installed.",
7855
+ `board=${result.board_id}`,
7856
+ `install_root=${result.install_root}`,
7857
+ `registry=${result.registry_path}`,
7858
+ `removed_registry_entry=${result.removed_registry_entry}`,
7859
+ `observed_at=${result.observed_at}`
7860
+ ];
7861
+ if (result.removed_paths.length > 0) {
7862
+ lines.push("removed_paths:");
7863
+ for (const removedPath of result.removed_paths) {
7864
+ lines.push(` ${removedPath}`);
7865
+ }
7866
+ }
7867
+ if (result.remaining_installed_boards.length > 0) {
7868
+ lines.push(`remaining_installed_boards=${result.remaining_installed_boards.join(",")}`);
7869
+ }
7870
+ return lines.join("\n");
7871
+ }
5422
7872
  function renderLocalToolchainValidation(result) {
5423
7873
  const lines = [
5424
7874
  result.ok ? "Local toolchain ready." : "Local toolchain not ready.",
5425
7875
  `board=${result.board_id}`,
7876
+ `mode=${result.mode}`,
5426
7877
  `host=${result.host.platform}/${result.host.arch}`,
5427
- `release_root=${result.release_root}`
7878
+ `release_root=${result.release_root}`,
7879
+ `summary=${result.summary_for_user}`
5428
7880
  ];
7881
+ if (!result.ok && result.missing_groups.length > 0) {
7882
+ lines.push(`missing_groups=${result.missing_groups.join(", ")}`);
7883
+ }
7884
+ if (result.repair_command) {
7885
+ lines.push(`repair_command=${result.repair_command}`);
7886
+ }
5429
7887
  for (const check of result.checked_paths) {
5430
7888
  lines.push(`${check.exists ? "ok" : "missing"} ${check.label}: ${check.path}`);
5431
7889
  }
7890
+ if (result.path_leaks.length > 0) {
7891
+ lines.push("path_leaks:");
7892
+ for (const leak of result.path_leaks) {
7893
+ lines.push(` ${leak.label}: ${leak.path} contains ${leak.forbidden}`);
7894
+ }
7895
+ }
5432
7896
  if (result.notes.length > 0) {
5433
7897
  lines.push("notes:");
5434
7898
  for (const note of result.notes) {
@@ -6056,6 +8520,24 @@ function optionalPositiveIntegerFlag(parsed, name) {
6056
8520
  }
6057
8521
  return { value };
6058
8522
  }
8523
+ function optionalIntegerFlag(parsed, name, min, max) {
8524
+ const values = flagValues(parsed, name);
8525
+ if (values.length === 0) {
8526
+ return {};
8527
+ }
8528
+ if (values.some((value) => typeof value !== "string")) {
8529
+ return { error: `--${name} requires a value.` };
8530
+ }
8531
+ const raw = values[values.length - 1].trim();
8532
+ if (!/^-?\d+$/.test(raw)) {
8533
+ return { error: `--${name} must be an integer.` };
8534
+ }
8535
+ const value = Number(raw);
8536
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
8537
+ return { error: `--${name} must be an integer from ${min} through ${max}.` };
8538
+ }
8539
+ return { value };
8540
+ }
6059
8541
  function jsonObjectListFlag(parsed, name) {
6060
8542
  const objects = [];
6061
8543
  for (const raw of flagValues(parsed, name)) {
@@ -6173,12 +8655,17 @@ async function waitForever() {
6173
8655
  });
6174
8656
  return 0;
6175
8657
  }
8658
+ function quotaAndBillingDisabled() {
8659
+ return fail("quota_not_supported", "当前 Embed Labs 服务器只提供 MCP 服务,不提供额度、充值、余额、账本、存储配额或用量计费功能。", {
8660
+ remediation: "请使用 auth、plugin、board、local toolchain、mcp start、device 和 Local Bridge 相关命令。"
8661
+ });
8662
+ }
6176
8663
  function printHelp() {
6177
8664
  printCliHelp(`embed CLI
6178
8665
 
6179
8666
  Usage:
6180
8667
  embed <command> [options]
6181
- embed query "查一下我的额度"
8668
+ embed query "当前支持哪些开发板"
6182
8669
  embed help getting-started
6183
8670
  embed help commands
6184
8671
 
@@ -6188,68 +8675,58 @@ Main workflow:
6188
8675
  2. Sign in with a token:
6189
8676
  embed auth login --token <token>
6190
8677
  # or set EMBED_API_TOKEN / create an account API key for automation.
6191
- 3. Inspect server model routing:
8678
+ 3. Install or update local AI client plugins:
6192
8679
  embed plugin install codex
6193
8680
  embed plugin install opencode
6194
- embed service modes
6195
- embed model list
6196
- embed model default
8681
+ embed plugin install trae
8682
+ embed plugin update check
6197
8683
  4. Run a natural-language local tool loop:
6198
8684
  embed agent run --prompt "验证开发板状态"
6199
8685
  5. Validate or use the local TaishanPi toolchain:
8686
+ embed local toolchain list
8687
+ embed local toolchain installed
6200
8688
  embed local toolchain latest
6201
8689
  embed local toolchain install
8690
+ embed local toolchain uninstall --board pico2w-rp2350-monitor
6202
8691
  embed local toolchain validate
6203
8692
  embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
6204
8693
  embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
6205
- 6. Pick a cloud build template:
8694
+ embed local build qt-smoke --source ./taishanpi-app --target-name app --build-dir ./.embed-labs/build/app
8695
+ 6. Query board knowledge and method metadata:
6206
8696
  embed board registry list
6207
8697
  embed board methods taishanpi-1m-rk3566
6208
8698
  embed board knowledge taishanpi-1m-rk3566
8699
+ embed board knowledge search taishanpi-1m-rk3566 --query "UART pinout"
6209
8700
  embed build template list
6210
8701
  embed build template show <template_id>
6211
- 7. Provision and populate a build workspace:
6212
- embed build workspace provision --account <account_id> --project <project_id> --template <template_id>
6213
- embed build resource lease create --workspace <workspace_id> --execution-mode cloud_worker
6214
- embed build workspace source put <workspace_id> --file ./main.c:src/main.c
6215
- embed build workspace source list <workspace_id>
6216
- embed build workspace source get <workspace_id> --path src/main.c --output ./main.c
6217
- embed build workspace source search <workspace_id> --query init --glob "**/*.c"
6218
- embed build workspace source patch <workspace_id> --patch ./fix.patch
6219
- embed build workspace release <workspace_id> --dry-run
6220
- 8. Generate application source on the server and follow artifacts:
6221
- embed build application generate --workspace <workspace_id> --prompt "Create a minimal Linux app" --provider bai --model gpt-5.2
6222
- embed build application compile --workspace <workspace_id> --source app/generated.c --execution-mode docker_worker
6223
- embed build image generate --workspace <workspace_id> --prompt "Generate a minimal TaishanPi image"
8702
+ 7. Generate small image packages and compose locally:
6224
8703
  embed build image boot-logo --logo ./logo.png --board taishanpi --variant 1M-RK3566 --output ./boot-logo-package.json
6225
8704
  embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
6226
8705
  embed cloud task artifacts <task_id>
6227
8706
  embed artifact download <artifact_id> --output ./artifact.bin
6228
- 9. Check credits or create a recharge QR:
6229
- embed billing balance --account <account_id>
6230
- embed billing tokens --account <account_id>
6231
- embed billing ledger --account <account_id>
6232
- embed billing storage --account <account_id>
6233
- embed billing storage settle --account <account_id> --dry-run
6234
- embed billing recharge create --account <account_id> --amount-usd 10 --provider onchain --qr
6235
-
6236
8707
  Local hardware:
6237
8708
  embed bridge start
8709
+ embed mcp start
6238
8710
  embed agent run --prompt "验证开发板状态"
6239
8711
  embed agent run --prompt "部署泰山派应用" --host 198.19.77.2 --artifact ./artifact.bin --remote-path /userdata/embed-labs/apps/app --approve --run
6240
8712
  embed agent run --prompt "部署生成的泰山派应用" --host 198.19.77.2 --artifact-task <task_id> --remote-path /userdata/embed-labs/apps/app --approve --run
6241
8713
  embed tool list
6242
- embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22,15301]}'
8714
+ embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22]}'
6243
8715
  embed tool call wifi.scan --input-json '{"host":"198.19.77.2","user":"root"}'
8716
+ embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve
6244
8717
  embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}'
6245
8718
  embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}'
6246
8719
  embed device list
8720
+ embed local toolchain list
8721
+ embed local toolchain installed
6247
8722
  embed local toolchain latest
6248
8723
  embed local toolchain current
6249
8724
  embed local toolchain install
8725
+ embed local toolchain uninstall --board pico2w-rp2350-monitor
6250
8726
  embed local toolchain validate
6251
8727
  embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
6252
8728
  embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
8729
+ embed local build qt-smoke --source ./taishanpi-app --target-name app --build-dir ./.embed-labs/build/app
6253
8730
  embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
6254
8731
  embed deploy taishanpi --host 198.19.77.2 --artifact ./artifact.bin --approve --run
6255
8732
  embed flash plan --board <rp2350|taishanpi>
@@ -6260,16 +8737,14 @@ Help:
6260
8737
 
6261
8738
  Environment:
6262
8739
  EMBED_BRIDGE_URL=http://127.0.0.1:18083
6263
- EMBED_CLOUD_API_URL=http://127.0.0.1:18100
8740
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
6264
8741
  EMBED_API_TOKEN=<token>
6265
8742
  CODEX_HOME=~/.codex
6266
8743
 
6267
8744
  Natural language:
6268
- embed query "查一下我的额度"
6269
- embed query "用 USDC 链上充值 100 美元"
6270
- embed query "看一下 token 用量"
6271
- embed query "看一下磁盘空间"
6272
- embed query "结算磁盘空间扣费"
8745
+ embed query "当前支持哪些开发板"
8746
+ embed query "验证开发板状态"
8747
+ embed query "安装泰山派本地开发环境"
6273
8748
  `);
6274
8749
  }
6275
8750
  function printHelpTopic(topic) {
@@ -6295,14 +8770,14 @@ Available topics:
6295
8770
  function printGettingStartedHelp() {
6296
8771
  printCliHelp(`embed getting started
6297
8772
 
6298
- After npm install, use the installed embed binary directly:
8773
+ After npm install, use the installed embedlabs binary directly:
6299
8774
 
6300
- embed doctor
8775
+ embedlabs doctor
6301
8776
 
6302
8777
  If doctor reports missing authentication, sign in with a user token:
6303
8778
 
6304
- embed auth login --token <token>
6305
- embed auth status
8779
+ embedlabs auth login --token <token>
8780
+ embedlabs auth status
6306
8781
 
6307
8782
  For service or CI flows, use an account API key instead of an interactive
6308
8783
  profile token:
@@ -6312,23 +8787,29 @@ profile token:
6312
8787
 
6313
8788
  Install local AI client plugins explicitly:
6314
8789
 
6315
- embed plugin list
6316
- embed plugin install codex
6317
- embed plugin install opencode
8790
+ embedlabs plugin list
8791
+ embedlabs plugin install codex
8792
+ embedlabs plugin install opencode
8793
+ embedlabs plugin install trae
8794
+ embedlabs plugin update check
8795
+ embedlabs plugin update all
8796
+
8797
+ For Trae, restart Trae after install. The installer writes mcp.json and a local
8798
+ Gallery cache entry so the Embed Labs MCP icon can appear in Trae builds that
8799
+ only render Gallery icons.
6318
8800
 
6319
- Cloud build path:
8801
+ Local MCP service path:
6320
8802
 
6321
- embed service modes
6322
- embed model list
6323
- embed model default
6324
8803
  embed agent run --prompt "验证开发板状态"
6325
8804
  embed local toolchain validate
6326
8805
  embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
6327
8806
  embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
8807
+ embed local build qt-smoke --source ./taishanpi-app --target-name app --build-dir ./.embed-labs/build/app
6328
8808
  embed board registry list
6329
8809
  embed board registry show taishanpi-1m-rk3566
6330
8810
  embed board methods taishanpi-1m-rk3566
6331
8811
  embed board knowledge taishanpi-1m-rk3566
8812
+ embed board knowledge search taishanpi-1m-rk3566 --query "UART pinout"
6332
8813
  embed build template list
6333
8814
  embed build template show <template_id>
6334
8815
  embed build workspace provision --account <account_id> --project <project_id> --template <template_id>
@@ -6339,9 +8820,6 @@ Cloud build path:
6339
8820
  embed build workspace source search <workspace_id> --query init --glob "**/*.c"
6340
8821
  embed build workspace source patch <workspace_id> --patch ./fix.patch
6341
8822
  embed build workspace release <workspace_id> --dry-run
6342
- embed build application generate --workspace <workspace_id> --prompt "Create a minimal Linux app" --provider bai --model gpt-5.2
6343
- embed build application compile --workspace <workspace_id> --source app/generated.c --execution-mode docker_worker
6344
- embed build image generate --workspace <workspace_id> --prompt "Generate a minimal TaishanPi image"
6345
8823
  embed build image boot-logo --logo ./logo.png --board taishanpi --variant 1M-RK3566 --output ./boot-logo-package.json
6346
8824
  embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
6347
8825
  embed cloud task status <task_id>
@@ -6350,29 +8828,18 @@ Cloud build path:
6350
8828
 
6351
8829
  Natural language shortcuts:
6352
8830
 
6353
- embed query "查一下我的额度"
6354
- embed query "用 USDC 链上充值 100 美元"
6355
- embed query "看一下 token 用量"
6356
- embed query "看一下磁盘空间"
6357
-
6358
- Billing path:
6359
-
6360
- embed billing balance --account <account_id>
6361
- embed billing tokens --account <account_id>
6362
- embed billing ledger --account <account_id>
6363
- embed billing storage --account <account_id>
6364
- embed billing storage settle --account <account_id> --dry-run
6365
- embed billing recharge create --account <account_id> --amount-usd 10 --provider onchain --qr
6366
- embed billing recharge submit-tx <recharge_session_id> --tx-hash <hash>
6367
- embed billing recharge list --account <account_id>
8831
+ embed query "当前支持哪些开发板"
8832
+ embed query "验证开发板状态"
8833
+ embed query "安装泰山派本地开发环境"
6368
8834
 
6369
8835
  Local hardware path:
6370
8836
 
6371
8837
  embed bridge start
8838
+ embed mcp start
6372
8839
  embed run "验证开发板状态"
6373
8840
  embed tool list
6374
8841
  embed tool call debug.tools.scan
6375
- embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22,15301]}'
8842
+ embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22]}'
6376
8843
  embed device list
6377
8844
  embed deploy taishanpi --host 198.19.77.2 --artifact ./artifact.bin --approve --run
6378
8845
  embed flash plan --board <rp2350|taishanpi> --artifact ./artifact.bin
@@ -6388,11 +8855,15 @@ Usage:
6388
8855
  embed query <natural language request> [--account <account_id>] [--qr] [--json]
6389
8856
  embed bridge start [--host 127.0.0.1] [--port 18083]
6390
8857
  embed bridge status [--json]
8858
+ embed mcp start [--bridge-path <path>]
8859
+ embed mcp config [--client generic|trae|cursor|claude|windsurf] [--absolute-command] [--cloud-url <url>] [--json]
6391
8860
  embed auth login --token <token> [--profile default] [--json]
6392
8861
  embed auth status [--json]
6393
8862
  embed auth logout [--json]
6394
8863
  embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]
6395
- embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]
8864
+ embed plugin install <codex|opencode|trae|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--trae-target <dir>] [--force] [--json]
8865
+ embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--trae-target <dir>] [--json]
8866
+ embed plugin update <codex|opencode|trae|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--trae-target <dir>] [--json]
6396
8867
  embed service modes [--json]
6397
8868
  embed model list [--json]
6398
8869
  embed model default [--json]
@@ -6400,6 +8871,7 @@ Usage:
6400
8871
  embed board registry show <template_id> [--json]
6401
8872
  embed board methods <template_id> [--json]
6402
8873
  embed board knowledge <template_id> [--json]
8874
+ embed board knowledge search <template_id> --query <text> [--source board_pack|build_template|registry] [--limit 5] [--json]
6403
8875
  embed board knowledge file <template_id> --source board_pack|build_template|registry --path <relative_path> [--output <local_path>] [--json]
6404
8876
  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]
6405
8877
  embed run <natural language request> [--provider stub|openai|bai|cc|claude-code] [--approve] [--json]
@@ -6410,23 +8882,6 @@ Usage:
6410
8882
  embed account keys create --account <account_id> [--name <name>] [--json]
6411
8883
  embed account keys list --account <account_id> [--json]
6412
8884
  embed account keys revoke <api_key_id> [--json]
6413
- 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]
6414
- embed usage summary --account <account_id>|--api-key-id <api_key_id> [--from <iso>] [--to <iso>] [--json]
6415
- embed usage events --account <account_id> [--api-key-id <api_key_id>] [--from <iso>] [--to <iso>] [--limit 100] [--json]
6416
- embed billing statement --account <account_id> [--from <iso>] [--to <iso>] [--json]
6417
- embed billing balance --account <account_id> [--json]
6418
- embed billing tokens --account <account_id> [--json]
6419
- embed billing ledger --account <account_id> [--json]
6420
- embed billing storage --account <account_id> [--json]
6421
- embed billing storage settle --account <account_id> [--dry-run] [--json]
6422
- 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]
6423
- embed billing recharge list --account <account_id> [--json]
6424
- embed billing recharge show <recharge_session_id> [--json]
6425
- embed billing recharge submit-tx <recharge_session_id> --tx-hash <hash> [--json]
6426
- embed billing recharge confirm <recharge_session_id> [--json]
6427
- embed billing snapshot create --account <account_id> [--from <iso>] [--to <iso>] [--json]
6428
- embed billing snapshot list --account <account_id> [--json]
6429
- embed billing snapshot show <billing_snapshot_id> [--json]
6430
8885
  embed service modes [--json]
6431
8886
  embed build template list [--json]
6432
8887
  embed build template show <template_id> [--json]
@@ -6448,13 +8903,16 @@ Usage:
6448
8903
  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]
6449
8904
  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]
6450
8905
  embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]
6451
- embed local toolchain latest [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--json]
8906
+ embed local toolchain list [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
8907
+ embed local toolchain installed [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
8908
+ embed local toolchain latest [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--json]
6452
8909
  embed local toolchain current [--install-root <path>] [--json]
6453
- 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]
8910
+ 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]
6454
8911
  Defaults to the production download channel at download.embedboard.com.
6455
- embed local toolchain validate [--release-root <path>] [--json]
8912
+ embed local toolchain uninstall --board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor [--install-root <path>] [--yes|--force] [--json]
8913
+ 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]
6456
8914
  embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]
6457
- embed local build qt-smoke --build-dir <dir> [--source <qt-smoke-dir>] [--release-root <path>] [--account <account_id>] [--json]
8915
+ embed local build qt-smoke --build-dir <dir> [--source <qt-cmake-dir>] [--target-name <executable>] [--release-root <path>] [--account <account_id>] [--json]
6458
8916
  embed debug tools [--json]
6459
8917
  embed tool list [--json]
6460
8918
  embed tool call wifi.scan --input-json '{"host":"198.19.77.2","user":"root"}' [--json]
@@ -6463,6 +8921,18 @@ Usage:
6463
8921
  embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}' [--json]
6464
8922
  embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
6465
8923
  embed tool call qml.runtime.start --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
8924
+ embed tool call rp2350.monitor.capabilities [--json]
8925
+ embed tool call rp2350.monitor.status [--json]
8926
+ embed tool call rp2350.monitor.gpio.read --input-json '{"pins":[16,17]}' --approve [--json]
8927
+ embed tool call rp2350.monitor.gpio.write --input-json '{"pin":16,"level":true}' --approve [--json]
8928
+ embed tool call rp2350.monitor.uart.write --input-json '{"baud":115200,"text":"hello","line_ending":"lf"}' --approve [--json]
8929
+ embed tool call rp2350.monitor.i2c.transfer --input-json '{"address":"0x50","write":"00","read_len":4}' --approve [--json]
8930
+ embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve [--json]
8931
+ embed tool call rp2350.monitor.logic.capture --input-json '{"pin_base":16,"pin_count":4,"sample_rate":1000000,"samples":4096}' --approve [--json]
8932
+ embed tool call rp2350.monitor.logic.decode --input-json '{"input_path":".embed-labs/rp2350-monitor/captures/logic.jsonl","decoder":"summary"}' [--json]
8933
+ embed tool call rp2350.monitor.wifi.manage --input-json '{"action":"scan"}' --approve [--json]
8934
+ embed tool call rp2350.monitor.probe.debug --input-json '{"action":"status"}' [--json]
8935
+ embed tool call rp2350.monitor.operation --input-json '{"action":"logic.stop","params":{}}' --approve [--json]
6466
8936
  embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
6467
8937
  embed board deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
6468
8938
  embed device list [--json]
@@ -6496,7 +8966,7 @@ Usage:
6496
8966
 
6497
8967
  Environment:
6498
8968
  EMBED_BRIDGE_URL=http://127.0.0.1:18083
6499
- EMBED_CLOUD_API_URL=http://127.0.0.1:18100
8969
+ EMBED_CLOUD_API_URL=https://api.embedboard.com
6500
8970
  EMBED_API_TOKEN=<token>
6501
8971
  CODEX_HOME=~/.codex
6502
8972
  `);