@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/README.md +107 -23
- package/dist/assets/embed-labs-icon-dark.png +0 -0
- package/dist/assets/embed-labs-icon-light.png +0 -0
- package/dist/embed-labs-mcp-bridge.mjs +3564 -0
- package/dist/index.js +2722 -252
- package/dist/index.js.map +1 -1
- package/dist/install-progress.d.ts +2 -0
- package/dist/install-progress.js +75 -0
- package/dist/install-progress.js.map +1 -0
- package/dist/local-toolchain.d.ts +191 -5
- package/dist/local-toolchain.js +2000 -135
- package/dist/local-toolchain.js.map +1 -1
- package/package.json +3 -3
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 ?? "
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
999
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
1355
|
-
return await response.json();
|
|
1506
|
+
return await bridgeRequest("GET", path);
|
|
1356
1507
|
}
|
|
1357
1508
|
async function bridgePost(path, body) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
-
|
|
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
|
|
1375
|
-
if (token) {
|
|
1376
|
-
|
|
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
|
|
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
|
-
|
|
1697
|
+
const bodyText = body === undefined ? "" : JSON.stringify(body);
|
|
1698
|
+
if (bodyText) {
|
|
1413
1699
|
headers["content-type"] = "application/json";
|
|
1414
1700
|
}
|
|
1415
|
-
const
|
|
1416
|
-
if (token) {
|
|
1417
|
-
|
|
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 :
|
|
1715
|
+
body: body === undefined ? undefined : bodyText
|
|
1423
1716
|
});
|
|
1424
|
-
|
|
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,
|
|
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
|
-
?
|
|
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
|
|
1566
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
1600
|
-
|
|
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(
|
|
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(
|
|
2518
|
+
return join(defaultCodexHome(), "plugins");
|
|
1815
2519
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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
|
-
|
|
1833
|
-
if (
|
|
1834
|
-
|
|
1835
|
-
|
|
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
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
3030
|
+
const warning = await upsertCodexMcpRuntimeConfig(mcpLauncher.command, mcpLauncher.args);
|
|
1873
3031
|
return warning ? { registered: true, warning } : { registered: true };
|
|
1874
3032
|
}
|
|
1875
|
-
function
|
|
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"
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1909
|
-
args: `[${tomlString(
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2087
|
-
return fileToken || undefined;
|
|
3763
|
+
return normalizeLocalAuthFile(parsed);
|
|
2088
3764
|
}
|
|
2089
3765
|
catch {
|
|
2090
|
-
return
|
|
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: "
|
|
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
|
-
|
|
3423
|
-
|
|
5177
|
+
const limit = optionalPositiveIntegerFlag(parsed, "limit");
|
|
5178
|
+
if (limit.error) {
|
|
5179
|
+
return limit.error;
|
|
3424
5180
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3552
|
-
await
|
|
3553
|
-
|
|
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
|
-
|
|
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
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
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.
|
|
8678
|
+
3. Install or update local AI client plugins:
|
|
6192
8679
|
embed plugin install codex
|
|
6193
8680
|
embed plugin install opencode
|
|
6194
|
-
embed
|
|
6195
|
-
embed
|
|
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
|
-
|
|
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.
|
|
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
|
|
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=
|
|
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 "
|
|
6270
|
-
embed query "
|
|
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
|
|
8773
|
+
After npm install, use the installed embedlabs binary directly:
|
|
6299
8774
|
|
|
6300
|
-
|
|
8775
|
+
embedlabs doctor
|
|
6301
8776
|
|
|
6302
8777
|
If doctor reports missing authentication, sign in with a user token:
|
|
6303
8778
|
|
|
6304
|
-
|
|
6305
|
-
|
|
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
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
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
|
-
|
|
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 "
|
|
6355
|
-
embed query "
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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=
|
|
8969
|
+
EMBED_CLOUD_API_URL=https://api.embedboard.com
|
|
6500
8970
|
EMBED_API_TOKEN=<token>
|
|
6501
8971
|
CODEX_HOME=~/.codex
|
|
6502
8972
|
`);
|