@kvell007/embed-labs-cli 0.1.0-alpha.6 → 0.1.0-alpha.61
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 +87 -23
- package/dist/embed-labs-mcp-bridge.mjs +3104 -0
- package/dist/index.js +2395 -184
- 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 +190 -5
- package/dist/local-toolchain.js +1480 -87
- 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";
|
|
2
|
+
import { createHash, createHmac, generateKeyPairSync, randomBytes, sign as signCrypto } from "node:crypto";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { access, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
-
import { homedir, tmpdir } from "node:os";
|
|
7
|
+
import { arch, hostname, homedir, platform, tmpdir } from "node:os";
|
|
8
8
|
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { startServer } from "@embed-labs/local-bridge";
|
|
11
10
|
import { composeBootLogoPackage } from "./image-compose.js";
|
|
12
|
-
import { buildTaishanPiQtSmoke, compileTaishanPiSingleFile, currentLocalToolchain, installLocalToolchain, latestLocalToolchain, validateLocalToolchain } from "./local-toolchain.js";
|
|
11
|
+
import { buildTaishanPiQtSmoke, compileTaishanPiSingleFile, currentLocalToolchain, installLocalToolchain, latestLocalToolchain, listLocalToolchainEnvironments, uninstallLocalToolchain, validateLocalToolchain, windowsWslInstall, windowsWslStatus } from "./local-toolchain.js";
|
|
13
12
|
import { fail, ok } from "@embed-labs/protocol";
|
|
14
13
|
const require = createRequire(import.meta.url);
|
|
15
14
|
const CLI_MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
16
15
|
const SOURCE_CHECKOUT_ROOT = resolve(CLI_MODULE_DIR, "..", "..", "..");
|
|
17
16
|
const qrcodeTerminal = require("qrcode-terminal");
|
|
18
17
|
const DEFAULT_BRIDGE_URL = process.env.EMBED_BRIDGE_URL ?? "http://127.0.0.1:18083";
|
|
19
|
-
const DEFAULT_CLOUD_API_URL = process.env.EMBED_CLOUD_API_URL ?? "
|
|
18
|
+
const DEFAULT_CLOUD_API_URL = process.env.EMBED_CLOUD_API_URL ?? "https://api.embedboard.com";
|
|
20
19
|
const DEFAULT_AUTH_FILE = process.env.EMBED_AUTH_FILE ?? ".embed-labs/auth.json";
|
|
20
|
+
const DEFAULT_DEVICE_FILE = process.env.EMBED_DEVICE_FILE ?? join(dirname(DEFAULT_AUTH_FILE), "device.json");
|
|
21
|
+
const DEFAULT_DASHBOARD_URL = process.env.EMBED_DASHBOARD_URL ?? "https://api.embedboard.com/dashboard";
|
|
22
|
+
const EMBED_CLIENT_NAME = "embedlabs-cli";
|
|
23
|
+
const EMBED_CLIENT_VERSION = process.env.EMBED_CLIENT_VERSION ?? "0.1.0";
|
|
21
24
|
const DEFAULT_AGENT_ARTIFACT_DIR = process.env.EMBED_AGENT_ARTIFACT_DIR ?? ".embed-labs/artifacts";
|
|
25
|
+
const TOOL_INTEGRITY_RELOGIN_MESSAGE = "工具完整性校验失败,请在当前电脑重新执行 embedlabs auth login --token <key>";
|
|
22
26
|
const DOCTOR_USAGE = "Usage: embed doctor [--json]";
|
|
23
27
|
const DOCTOR_HTTP_TIMEOUT_MS = 8000;
|
|
24
28
|
const MIN_NODE_MAJOR = 20;
|
|
@@ -27,7 +31,20 @@ const DEVICE_PROBE_USAGE = "Usage: embed device probe --host <host> --ports 22,1
|
|
|
27
31
|
const QUERY_USAGE = "Usage: embed query <natural language request> [--account <account_id>] [--qr] [--json]";
|
|
28
32
|
const DEFAULT_PLUGIN_RELEASE_URL = process.env.EMBED_PLUGIN_RELEASE_URL?.trim() || "https://api.embedboard.com/plugin-releases/agent-plugins/latest";
|
|
29
33
|
const PLUGIN_LIST_USAGE = "Usage: embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]";
|
|
34
|
+
const CODEX_PLUGIN_NAME = "embed-labs";
|
|
35
|
+
const CODEX_MARKETPLACE_NAME = "embed-labs";
|
|
36
|
+
const MCP_START_USAGE = "Usage: embed mcp start [--bridge-path <path>]";
|
|
37
|
+
const LEGACY_CODEX_PLUGIN_NAMES = [
|
|
38
|
+
"dbt-agent",
|
|
39
|
+
"Dbt Agent",
|
|
40
|
+
"development-board-toolchain",
|
|
41
|
+
"development-board-toolchain-dev",
|
|
42
|
+
"deve"
|
|
43
|
+
];
|
|
44
|
+
const LEGACY_CODEX_MARKETPLACE_NAMES = new Set(["embed-labs-plugins", "plugins", "Plugins", "deve"]);
|
|
30
45
|
const PLUGIN_INSTALL_USAGE = "Usage: embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]";
|
|
46
|
+
const PLUGIN_UPDATE_CHECK_USAGE = "Usage: embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
|
|
47
|
+
const PLUGIN_UPDATE_USAGE = "Usage: embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
|
|
31
48
|
const CLOUD_TASK_ARTIFACTS_USAGE = "Usage: embed cloud task artifacts <task_id> [--json]";
|
|
32
49
|
const CLOUD_TASK_EVIDENCE_USAGE = "Usage: embed cloud task evidence <task_id> [--json]";
|
|
33
50
|
const ARTIFACT_STATUS_USAGE = "Usage: embed artifact status <artifact_id> [--json]";
|
|
@@ -81,24 +98,36 @@ const BUILD_IMAGE_BOOT_LOGO_USAGE = "Usage: embed build image boot-logo --logo <
|
|
|
81
98
|
const IMAGE_BOOT_LOGO_COMPOSE_USAGE = "Usage: embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]";
|
|
82
99
|
const BUILD_IMAGE_DTB_USAGE = "Usage: embed build image dtb --dtb <local.dtb|local.dts> [--input-format auto|dtb|dts] [--account <account_id>] [--project <project_id>] [--board taishanpi] [--variant 1M-RK3566] [--output <package.json>] [--json]";
|
|
83
100
|
const IMAGE_DTB_COMPOSE_USAGE = "Usage: embed image dtb compose --package <dtb-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]";
|
|
84
|
-
const
|
|
101
|
+
const LOCAL_TOOLCHAIN_LIST_USAGE = "Usage: embed local toolchain list [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]";
|
|
102
|
+
const LOCAL_TOOLCHAIN_INSTALLED_USAGE = "Usage: embed local toolchain installed [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]";
|
|
103
|
+
const LOCAL_TOOLCHAIN_LATEST_USAGE = "Usage: embed local toolchain latest [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--json]";
|
|
85
104
|
const LOCAL_TOOLCHAIN_CURRENT_USAGE = "Usage: embed local toolchain current [--install-root <path>] [--json]";
|
|
86
|
-
const LOCAL_TOOLCHAIN_INSTALL_USAGE = "Usage: embed local toolchain install [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--force] [--json]\nDefault source: the production download channel at download.embedboard.com.";
|
|
87
|
-
const
|
|
105
|
+
const LOCAL_TOOLCHAIN_INSTALL_USAGE = "Usage: embed local toolchain install [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--force] [--json]\nDefault source: the production download channel at download.embedboard.com.";
|
|
106
|
+
const LOCAL_TOOLCHAIN_UNINSTALL_USAGE = "Usage: embed local toolchain uninstall --board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor [--install-root <path>] [--json]";
|
|
107
|
+
const LOCAL_TOOLCHAIN_VALIDATE_USAGE = "Usage: embed local toolchain validate [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--release-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--json]";
|
|
108
|
+
const LOCAL_WSL_STATUS_USAGE = "Usage: embed local wsl status [--json]";
|
|
109
|
+
const LOCAL_WSL_INSTALL_USAGE = "Usage: embed local wsl install [--distribution Ubuntu] [--no-launch true|false] [--web-download true|false] [--timeout-ms 600000] [--json]";
|
|
88
110
|
const LOCAL_COMPILE_TAISHANPI_USAGE = "Usage: embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]";
|
|
89
|
-
const LOCAL_BUILD_QT_SMOKE_USAGE = "Usage: embed local build qt-smoke --build-dir <dir> [--source <qt-
|
|
111
|
+
const LOCAL_BUILD_QT_SMOKE_USAGE = "Usage: embed local build qt-smoke --build-dir <dir> [--source <qt-cmake-dir>] [--target-name <executable>] [--release-root <path>] [--account <account_id>] [--json]";
|
|
112
|
+
const AUTH_DEVICE_STATUS_USAGE = "Usage: embed auth device status [--json]";
|
|
113
|
+
const AUTH_DEVICE_LIST_USAGE = "Usage: embed auth device list [--json]";
|
|
114
|
+
const AUTH_DEVICE_REVOKE_USAGE = "Usage: embed auth device revoke <device_id> [--json]";
|
|
115
|
+
const AUTH_DEVICE_RENAME_USAGE = "Usage: embed auth device rename <device_id> --label <name> [--json]";
|
|
90
116
|
const BOARD_REGISTRY_LIST_USAGE = "Usage: embed board registry list [--json]";
|
|
91
117
|
const BOARD_REGISTRY_SHOW_USAGE = "Usage: embed board registry show <template_id> [--json]";
|
|
92
118
|
const BOARD_METHODS_USAGE = "Usage: embed board methods <template_id> [--json]";
|
|
93
119
|
const BOARD_KNOWLEDGE_USAGE = "Usage: embed board knowledge <template_id> [--json]";
|
|
120
|
+
const BOARD_KNOWLEDGE_SEARCH_USAGE = "Usage: embed board knowledge search <template_id> --query <text> [--source board_pack|build_template|registry] [--limit 5] [--json]";
|
|
94
121
|
const BOARD_KNOWLEDGE_FILE_USAGE = "Usage: embed board knowledge file <template_id> --source board_pack|build_template|registry --path <relative_path> [--output <local_path>] [--json]";
|
|
95
122
|
const MODEL_LIST_USAGE = "Usage: embed model list [--json]";
|
|
96
123
|
const MODEL_DEFAULT_USAGE = "Usage: embed model default [--json]";
|
|
97
124
|
const SERVICE_MODES_USAGE = "Usage: embed service modes [--json]";
|
|
98
|
-
const AGENT_RUN_USAGE = "Usage: embed agent run --prompt <request> [--account <account_id>] [--workspace <workspace_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--max-tool-calls 6] [--host <ip>] [--ports 22,15301] [--artifact <local_file>|--artifact-id <artifact_id>|--artifact-task <task_id>] [--artifact-output <path>] [--remote-path <path>] [--run] [--approve] [--json]";
|
|
125
|
+
const AGENT_RUN_USAGE = "Usage: embed agent run --prompt <request> [--account <account_id>] [--workspace <workspace_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--max-tool-calls 6] [--host <ip>] [--ports 22,15301] [--artifact <local_file>|--artifact-id <artifact_id>|--artifact-task <task_id>] [--artifact-output <path>] [--remote-path <path>] [--run-command <cmd>] [--run] [--approve] [--json]";
|
|
126
|
+
let cachedLocalHardwareFingerprint;
|
|
99
127
|
const TOOL_LIST_USAGE = "Usage: embed tool list [--json]";
|
|
100
128
|
const TOOL_CALL_USAGE = "Usage: embed tool call <capability_id> [--input-json '<json>'] [--approve] [--json]";
|
|
101
|
-
const
|
|
129
|
+
const MCP_TOOL_EVENT_USAGE = "Usage: embed mcp log --tool <tool_name> [--client codex|opencode] [--mode local_ai|server_ai] [--local-device-id <id>] [--server-model-used true|false] [--success true|false] [--request-id <id>] [--duration-ms <ms>] [--input-summary <text>] [--output-summary <text>] [--json]";
|
|
130
|
+
const BOARD_DEPLOY_TAISHANPI_USAGE = "Usage: embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--user root] [--remote-path /userdata/embed-labs/apps/app] [--run-command <cmd>] [--run] [--timeout 30] [--json]";
|
|
102
131
|
const CLOUD_TASK_EVENT_APPEND_USAGE = "Usage: embed cloud task event append <task_id> [--state <state>] [--progress-stage <stage>|--stage <stage>] [--progress-text <text>|--message <text>] [--progress-percent 0-100] [--severity info|warning|error] [--type <event_type>] [--artifact-json '<json>'] [--evidence-json '<json>'] [--json]";
|
|
103
132
|
const TASK_STATES = new Set([
|
|
104
133
|
"created",
|
|
@@ -147,15 +176,17 @@ async function main(argv) {
|
|
|
147
176
|
return output(parsed, await bridgePost("/v1/board/taishanpi/deploy", request), renderBoardDeployResult);
|
|
148
177
|
}
|
|
149
178
|
if (area === "bridge" && action === "start") {
|
|
150
|
-
|
|
151
|
-
host: stringFlag(parsed, "host"),
|
|
152
|
-
port: numberFlag(parsed, "port")
|
|
153
|
-
});
|
|
154
|
-
return await waitForever();
|
|
179
|
+
return await runBridgeStart(parsed);
|
|
155
180
|
}
|
|
156
181
|
if (area === "bridge" && action === "status") {
|
|
157
182
|
return output(parsed, await bridgeGet("/healthz"), renderBridgeStatus);
|
|
158
183
|
}
|
|
184
|
+
if (area === "mcp" && action === "start") {
|
|
185
|
+
return await runMcpStart(parsed);
|
|
186
|
+
}
|
|
187
|
+
if (area === "mcp") {
|
|
188
|
+
return output(parsed, fail("invalid_args", MCP_START_USAGE), undefined, 2);
|
|
189
|
+
}
|
|
159
190
|
if (area === "cloud" && action === "status") {
|
|
160
191
|
return output(parsed, await cloudGet("/healthz"), renderCloudStatus);
|
|
161
192
|
}
|
|
@@ -216,6 +247,17 @@ async function main(argv) {
|
|
|
216
247
|
return output(parsed, await cloudGet(`/v1/board-registry/templates/${encodeURIComponent(idResult.value)}/methods`), renderBoardMethods);
|
|
217
248
|
}
|
|
218
249
|
if (action === "knowledge") {
|
|
250
|
+
if (parsed.command[2] === "search") {
|
|
251
|
+
const request = boardKnowledgeSearchRequest(parsed);
|
|
252
|
+
if (typeof request === "string") {
|
|
253
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
254
|
+
}
|
|
255
|
+
const params = new URLSearchParams({ q: request.query, limit: String(request.limit) });
|
|
256
|
+
if (request.source) {
|
|
257
|
+
params.set("source", request.source);
|
|
258
|
+
}
|
|
259
|
+
return output(parsed, await cloudGet(`/v1/board-registry/templates/${encodeURIComponent(request.templateId)}/knowledge-search?${params.toString()}`), renderBoardKnowledgeSearch);
|
|
260
|
+
}
|
|
219
261
|
if (parsed.command[2] === "file") {
|
|
220
262
|
const request = boardKnowledgeFileRequest(parsed);
|
|
221
263
|
if (typeof request === "string") {
|
|
@@ -241,6 +283,7 @@ async function main(argv) {
|
|
|
241
283
|
BOARD_REGISTRY_SHOW_USAGE,
|
|
242
284
|
BOARD_METHODS_USAGE,
|
|
243
285
|
BOARD_KNOWLEDGE_USAGE,
|
|
286
|
+
BOARD_KNOWLEDGE_SEARCH_USAGE,
|
|
244
287
|
BOARD_KNOWLEDGE_FILE_USAGE
|
|
245
288
|
].join("\n")), undefined, 2);
|
|
246
289
|
}
|
|
@@ -279,7 +322,18 @@ async function main(argv) {
|
|
|
279
322
|
const result = await pluginInstall(parsed);
|
|
280
323
|
return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
|
|
281
324
|
}
|
|
282
|
-
|
|
325
|
+
if (action === "update") {
|
|
326
|
+
if (parsed.command[2] === "check") {
|
|
327
|
+
const result = await pluginUpdateCheck(parsed);
|
|
328
|
+
return output(parsed, result, renderPluginUpdateCheck, result.ok ? 0 : 2);
|
|
329
|
+
}
|
|
330
|
+
if (["codex", "opencode", "all"].includes(parsed.command[2] ?? "")) {
|
|
331
|
+
const result = await pluginUpdate(parsed);
|
|
332
|
+
return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
|
|
333
|
+
}
|
|
334
|
+
return output(parsed, fail("invalid_args", [PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
|
|
335
|
+
}
|
|
336
|
+
return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE, PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
|
|
283
337
|
}
|
|
284
338
|
if (area === "auth" && action === "login") {
|
|
285
339
|
const result = await authLogin(parsed);
|
|
@@ -288,6 +342,31 @@ async function main(argv) {
|
|
|
288
342
|
if (area === "auth" && action === "status") {
|
|
289
343
|
return output(parsed, ok(await authStatus()), renderAuthStatus);
|
|
290
344
|
}
|
|
345
|
+
if (area === "auth" && action === "device") {
|
|
346
|
+
const deviceAction = parsed.command[2] ?? "status";
|
|
347
|
+
if (deviceAction === "status") {
|
|
348
|
+
const result = await authDeviceStatus(parsed);
|
|
349
|
+
return output(parsed, result, renderAuthDeviceStatus, result.ok ? 0 : 2);
|
|
350
|
+
}
|
|
351
|
+
if (deviceAction === "list") {
|
|
352
|
+
const result = await authDeviceList(parsed);
|
|
353
|
+
return output(parsed, result, renderAuthDeviceList, result.ok ? 0 : 2);
|
|
354
|
+
}
|
|
355
|
+
if (deviceAction === "revoke") {
|
|
356
|
+
const result = await authDeviceRevoke(parsed);
|
|
357
|
+
return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
|
|
358
|
+
}
|
|
359
|
+
if (deviceAction === "rename") {
|
|
360
|
+
const result = await authDeviceRename(parsed);
|
|
361
|
+
return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
|
|
362
|
+
}
|
|
363
|
+
return output(parsed, fail("invalid_args", [
|
|
364
|
+
AUTH_DEVICE_STATUS_USAGE,
|
|
365
|
+
AUTH_DEVICE_LIST_USAGE,
|
|
366
|
+
AUTH_DEVICE_REVOKE_USAGE,
|
|
367
|
+
AUTH_DEVICE_RENAME_USAGE
|
|
368
|
+
].join("\n")), undefined, 2);
|
|
369
|
+
}
|
|
291
370
|
if (area === "auth" && action === "logout") {
|
|
292
371
|
await rm(DEFAULT_AUTH_FILE, { force: true });
|
|
293
372
|
return output(parsed, ok(await authStatus()), renderAuthStatus);
|
|
@@ -339,6 +418,9 @@ async function main(argv) {
|
|
|
339
418
|
ACCOUNT_KEY_REVOKE_USAGE
|
|
340
419
|
].join("\n")), undefined, 2);
|
|
341
420
|
}
|
|
421
|
+
if (area === "usage" || area === "billing") {
|
|
422
|
+
return output(parsed, quotaAndBillingDisabled(), undefined, 2);
|
|
423
|
+
}
|
|
342
424
|
if (area === "usage") {
|
|
343
425
|
if (action === "record") {
|
|
344
426
|
const body = usageRecordBody(parsed);
|
|
@@ -367,6 +449,16 @@ async function main(argv) {
|
|
|
367
449
|
USAGE_EVENTS_USAGE
|
|
368
450
|
].join("\n")), undefined, 2);
|
|
369
451
|
}
|
|
452
|
+
if (area === "mcp") {
|
|
453
|
+
if (action === "log" || action === "tool-event") {
|
|
454
|
+
const body = mcpToolEventBody(parsed);
|
|
455
|
+
if (typeof body === "string") {
|
|
456
|
+
return output(parsed, fail("invalid_args", body), undefined, 2);
|
|
457
|
+
}
|
|
458
|
+
return output(parsed, await cloudPost("/v1/mcp/tool-events", body), renderMcpToolEvent);
|
|
459
|
+
}
|
|
460
|
+
return output(parsed, fail("invalid_args", MCP_TOOL_EVENT_USAGE), undefined, 2);
|
|
461
|
+
}
|
|
370
462
|
if (area === "billing") {
|
|
371
463
|
if (action === "statement") {
|
|
372
464
|
const request = billingStatementRequest(parsed);
|
|
@@ -506,6 +598,20 @@ async function main(argv) {
|
|
|
506
598
|
return output(parsed, fail("invalid_args", [IMAGE_BOOT_LOGO_COMPOSE_USAGE, IMAGE_DTB_COMPOSE_USAGE].join("\n")), undefined, 2);
|
|
507
599
|
}
|
|
508
600
|
if (area === "local") {
|
|
601
|
+
if (action === "toolchain" && parsed.command[2] === "list") {
|
|
602
|
+
const request = localToolchainListRequest(parsed);
|
|
603
|
+
if (typeof request === "string") {
|
|
604
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
605
|
+
}
|
|
606
|
+
return output(parsed, ok(await listLocalToolchainEnvironments(request)), renderLocalToolchainList);
|
|
607
|
+
}
|
|
608
|
+
if (action === "toolchain" && parsed.command[2] === "installed") {
|
|
609
|
+
const request = localToolchainListRequest(parsed, LOCAL_TOOLCHAIN_INSTALLED_USAGE);
|
|
610
|
+
if (typeof request === "string") {
|
|
611
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
612
|
+
}
|
|
613
|
+
return output(parsed, ok(await listLocalToolchainEnvironments({ ...request, installedOnly: true })), renderLocalToolchainList);
|
|
614
|
+
}
|
|
509
615
|
if (action === "toolchain" && parsed.command[2] === "latest") {
|
|
510
616
|
const request = localToolchainLatestRequest(parsed);
|
|
511
617
|
if (typeof request === "string") {
|
|
@@ -527,12 +633,34 @@ async function main(argv) {
|
|
|
527
633
|
}
|
|
528
634
|
return output(parsed, ok(await installLocalToolchain(request)), renderLocalToolchainInstall);
|
|
529
635
|
}
|
|
636
|
+
if (action === "toolchain" && (parsed.command[2] === "uninstall" || parsed.command[2] === "remove")) {
|
|
637
|
+
const request = localToolchainUninstallRequest(parsed);
|
|
638
|
+
if (typeof request === "string") {
|
|
639
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
640
|
+
}
|
|
641
|
+
return output(parsed, ok(await uninstallLocalToolchain(request)), renderLocalToolchainUninstall);
|
|
642
|
+
}
|
|
530
643
|
if (action === "toolchain" && parsed.command[2] === "validate") {
|
|
531
644
|
const request = localToolchainValidateRequest(parsed);
|
|
532
645
|
if (typeof request === "string") {
|
|
533
646
|
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
534
647
|
}
|
|
535
|
-
return output(parsed, ok(await validateLocalToolchain(request
|
|
648
|
+
return output(parsed, ok(await validateLocalToolchain(request)), renderLocalToolchainValidation);
|
|
649
|
+
}
|
|
650
|
+
if (action === "wsl" && parsed.command[2] === "status") {
|
|
651
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
652
|
+
const extra = parsed.command.slice(3);
|
|
653
|
+
if (unknownFlag || extra.length > 0) {
|
|
654
|
+
return output(parsed, fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${LOCAL_WSL_STATUS_USAGE}` : LOCAL_WSL_STATUS_USAGE), undefined, 2);
|
|
655
|
+
}
|
|
656
|
+
return output(parsed, ok(await windowsWslStatus()), renderWindowsWslStatus);
|
|
657
|
+
}
|
|
658
|
+
if (action === "wsl" && parsed.command[2] === "install") {
|
|
659
|
+
const request = localWslInstallRequest(parsed);
|
|
660
|
+
if (typeof request === "string") {
|
|
661
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
662
|
+
}
|
|
663
|
+
return output(parsed, ok(await windowsWslInstall(request)), renderWindowsWslInstall);
|
|
536
664
|
}
|
|
537
665
|
if (action === "compile" && parsed.command[2] === "taishanpi") {
|
|
538
666
|
const request = localCompileTaishanPiRequest(parsed, await authStatus());
|
|
@@ -549,10 +677,15 @@ async function main(argv) {
|
|
|
549
677
|
return output(parsed, ok(await buildTaishanPiQtSmoke(request)), renderLocalCompileResult);
|
|
550
678
|
}
|
|
551
679
|
return output(parsed, fail("invalid_args", [
|
|
680
|
+
LOCAL_TOOLCHAIN_LIST_USAGE,
|
|
681
|
+
LOCAL_TOOLCHAIN_INSTALLED_USAGE,
|
|
552
682
|
LOCAL_TOOLCHAIN_LATEST_USAGE,
|
|
553
683
|
LOCAL_TOOLCHAIN_CURRENT_USAGE,
|
|
554
684
|
LOCAL_TOOLCHAIN_INSTALL_USAGE,
|
|
685
|
+
LOCAL_TOOLCHAIN_UNINSTALL_USAGE,
|
|
555
686
|
LOCAL_TOOLCHAIN_VALIDATE_USAGE,
|
|
687
|
+
LOCAL_WSL_STATUS_USAGE,
|
|
688
|
+
LOCAL_WSL_INSTALL_USAGE,
|
|
556
689
|
LOCAL_COMPILE_TAISHANPI_USAGE,
|
|
557
690
|
LOCAL_BUILD_QT_SMOKE_USAGE
|
|
558
691
|
].join("\n")), undefined, 2);
|
|
@@ -984,11 +1117,18 @@ async function main(argv) {
|
|
|
984
1117
|
return output(parsed, fail("unknown_command", `Unknown command: ${parsed.command.join(" ")}`), undefined, 2);
|
|
985
1118
|
}
|
|
986
1119
|
catch (error) {
|
|
987
|
-
|
|
988
|
-
|
|
1120
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1121
|
+
return output(parsed, fail("command_failed", message, {
|
|
1122
|
+
remediation: commandFailureRemediation(message)
|
|
989
1123
|
}), undefined, 1);
|
|
990
1124
|
}
|
|
991
1125
|
}
|
|
1126
|
+
function commandFailureRemediation(message) {
|
|
1127
|
+
if (message.includes("TaishanPi") && message.includes("WSL2")) {
|
|
1128
|
+
return "Run: embedlabs local wsl status";
|
|
1129
|
+
}
|
|
1130
|
+
return `Check that embed-local-bridge is running at ${DEFAULT_BRIDGE_URL}. Start it with: embed bridge start`;
|
|
1131
|
+
}
|
|
992
1132
|
function parseArgs(argv) {
|
|
993
1133
|
const command = [];
|
|
994
1134
|
const flags = {};
|
|
@@ -1098,6 +1238,7 @@ function flashBody(parsed, includeApproval) {
|
|
|
1098
1238
|
variant_id: stringFlag(parsed, "variant"),
|
|
1099
1239
|
hardware_profile_id: stringFlag(parsed, "hardware-profile"),
|
|
1100
1240
|
profile_id: stringFlag(parsed, "profile"),
|
|
1241
|
+
local_device_id: stringFlag(parsed, "local-device-id") ?? stringFlag(parsed, "device-id"),
|
|
1101
1242
|
image_dir: stringFlag(parsed, "image-dir"),
|
|
1102
1243
|
artifact_path: stringFlag(parsed, "artifact"),
|
|
1103
1244
|
target_volume_path: stringFlag(parsed, "target-volume")
|
|
@@ -1126,11 +1267,11 @@ async function doctor() {
|
|
|
1126
1267
|
const bridgeHealth = await apiDoctorCheck("bridge_health", "Local Bridge health", `${bridgeBaseUrl}/healthz`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderHealthSummary, healthStatus);
|
|
1127
1268
|
checks.push(bridgeHealth);
|
|
1128
1269
|
if (isUsableDoctorCheck(bridgeHealth)) {
|
|
1129
|
-
checks.push(await apiDoctorCheck("device_scan", "Device
|
|
1270
|
+
checks.push(await apiDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDeviceScanSummary, warningIfWarnings));
|
|
1130
1271
|
checks.push(await apiDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDebugToolScanSummary, warningIfWarnings));
|
|
1131
1272
|
}
|
|
1132
1273
|
else {
|
|
1133
|
-
checks.push(dependentDoctorCheck("device_scan", "Device
|
|
1274
|
+
checks.push(dependentDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "Device inventory requires a reachable Local Bridge."));
|
|
1134
1275
|
checks.push(dependentDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "Debug tool scan requires a reachable Local Bridge."));
|
|
1135
1276
|
}
|
|
1136
1277
|
checks.push(await apiDoctorCheck("cloud_api_health", "Cloud API health", `${cloudBaseUrl}/healthz`, "cloud_api_unreachable", `Cloud API is unreachable at ${cloudBaseUrl}.`, renderHealthSummary, healthStatus));
|
|
@@ -1189,7 +1330,8 @@ function authDoctorCheck(status) {
|
|
|
1189
1330
|
: {
|
|
1190
1331
|
code: "auth_not_ready",
|
|
1191
1332
|
message: "No CLI auth token is configured.",
|
|
1192
|
-
remediation:
|
|
1333
|
+
remediation: cloudAuthSetupRemediation(),
|
|
1334
|
+
details: cloudAuthSetupDetails()
|
|
1193
1335
|
}
|
|
1194
1336
|
};
|
|
1195
1337
|
}
|
|
@@ -1322,7 +1464,7 @@ function warningIfWarnings(data) {
|
|
|
1322
1464
|
}
|
|
1323
1465
|
function renderDeviceScanSummary(result) {
|
|
1324
1466
|
const warningText = result.warnings?.length ? ` ${result.warnings.length} warning(s).` : "";
|
|
1325
|
-
return `Device
|
|
1467
|
+
return `Device inventory snapshot: ${result.devices.length} device(s), ${result.usb.length} USB item(s), ${result.serial.length} serial port(s).${warningText}`;
|
|
1326
1468
|
}
|
|
1327
1469
|
function renderDebugToolScanSummary(result) {
|
|
1328
1470
|
const available = result.tools.filter((tool) => tool.available).length;
|
|
@@ -1340,16 +1482,142 @@ function isApiResponse(value) {
|
|
|
1340
1482
|
return isJsonObject(error) && typeof error.code === "string" && typeof error.message === "string";
|
|
1341
1483
|
}
|
|
1342
1484
|
async function bridgeGet(path) {
|
|
1343
|
-
|
|
1344
|
-
return await response.json();
|
|
1485
|
+
return await bridgeRequest("GET", path);
|
|
1345
1486
|
}
|
|
1346
1487
|
async function bridgePost(path, body) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1488
|
+
return await bridgeRequest("POST", path, body);
|
|
1489
|
+
}
|
|
1490
|
+
async function bridgeRequest(method, path, body) {
|
|
1491
|
+
const bodyText = body === undefined ? "" : JSON.stringify(body);
|
|
1492
|
+
const makeRequest = async () => {
|
|
1493
|
+
const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
|
|
1494
|
+
method,
|
|
1495
|
+
headers: bridgeHeaders(method, path, method === "POST" ? bodyText : "", method === "POST" ? { "content-type": "application/json" } : {}),
|
|
1496
|
+
body: method === "POST" ? bodyText : undefined
|
|
1497
|
+
});
|
|
1498
|
+
return await response.json();
|
|
1499
|
+
};
|
|
1500
|
+
try {
|
|
1501
|
+
return await makeRequest();
|
|
1502
|
+
}
|
|
1503
|
+
catch (error) {
|
|
1504
|
+
if (!isBridgeConnectionFailure(error)) {
|
|
1505
|
+
throw error;
|
|
1506
|
+
}
|
|
1507
|
+
const started = await ensureBridgeStartedForRequest();
|
|
1508
|
+
if (!started.ok) {
|
|
1509
|
+
return started;
|
|
1510
|
+
}
|
|
1511
|
+
return await makeRequest();
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
function bridgeHeaders(method, path, bodyText, base = {}) {
|
|
1515
|
+
const token = process.env.EMBED_BRIDGE_TOKEN?.trim();
|
|
1516
|
+
if (!token) {
|
|
1517
|
+
return base;
|
|
1518
|
+
}
|
|
1519
|
+
const headers = {
|
|
1520
|
+
...base,
|
|
1521
|
+
authorization: `Bearer ${token}`
|
|
1522
|
+
};
|
|
1523
|
+
addBridgeRequestSignature(headers, method, path, bodyText, token);
|
|
1524
|
+
return headers;
|
|
1525
|
+
}
|
|
1526
|
+
function addBridgeRequestSignature(headers, method, pathWithQuery, bodyText, token) {
|
|
1527
|
+
if (process.env.EMBED_BRIDGE_SIGNING === "0") {
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
1531
|
+
const nonce = randomBytes(16).toString("hex");
|
|
1532
|
+
const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
|
|
1533
|
+
const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
1534
|
+
const canonical = cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
|
|
1535
|
+
headers["x-embed-key-id"] = keyId;
|
|
1536
|
+
headers["x-embed-timestamp"] = timestamp;
|
|
1537
|
+
headers["x-embed-nonce"] = nonce;
|
|
1538
|
+
headers["x-embed-body-sha256"] = bodySha256;
|
|
1539
|
+
headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
|
|
1540
|
+
}
|
|
1541
|
+
function isBridgeConnectionFailure(error) {
|
|
1542
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1543
|
+
return message.includes("fetch failed") ||
|
|
1544
|
+
message.includes("ECONNREFUSED") ||
|
|
1545
|
+
message.includes("ECONNRESET") ||
|
|
1546
|
+
message.includes("UND_ERR_SOCKET");
|
|
1547
|
+
}
|
|
1548
|
+
async function ensureBridgeStartedForRequest() {
|
|
1549
|
+
if (process.env.EMBED_BRIDGE_AUTO_START === "0") {
|
|
1550
|
+
return fail("bridge_unavailable", `embed-local-bridge is not running at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1551
|
+
remediation: `Start it with: embed bridge start`
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
let bridgeURL;
|
|
1555
|
+
try {
|
|
1556
|
+
bridgeURL = new URL(DEFAULT_BRIDGE_URL);
|
|
1557
|
+
}
|
|
1558
|
+
catch {
|
|
1559
|
+
return fail("bridge_url_invalid", `EMBED_BRIDGE_URL is not a valid URL: ${DEFAULT_BRIDGE_URL}`);
|
|
1560
|
+
}
|
|
1561
|
+
if (!isLocalBridgeURL(bridgeURL)) {
|
|
1562
|
+
return fail("bridge_unavailable", `embed-local-bridge is not reachable at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1563
|
+
remediation: `Start the bridge for that host, or set EMBED_BRIDGE_URL to a local bridge URL.`
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
const launcher = await resolveBridgeLauncher();
|
|
1567
|
+
const host = bridgeURL.hostname === "::1" ? "::1" : bridgeURL.hostname || "127.0.0.1";
|
|
1568
|
+
const port = bridgeURL.port || "18083";
|
|
1569
|
+
const env = {
|
|
1570
|
+
...process.env,
|
|
1571
|
+
EMBED_BRIDGE_HOST: host,
|
|
1572
|
+
EMBED_BRIDGE_PORT: port
|
|
1573
|
+
};
|
|
1574
|
+
const child = spawn(launcher.command, [...launcher.args, "--host", host, "--port", port], {
|
|
1575
|
+
cwd: process.cwd(),
|
|
1576
|
+
detached: true,
|
|
1577
|
+
stdio: "ignore",
|
|
1578
|
+
env
|
|
1579
|
+
});
|
|
1580
|
+
child.unref();
|
|
1581
|
+
const ready = await waitForBridgeHealth(bridgeURL, 8000);
|
|
1582
|
+
if (!ready.ok) {
|
|
1583
|
+
return ready;
|
|
1584
|
+
}
|
|
1585
|
+
return ok({
|
|
1586
|
+
started: true,
|
|
1587
|
+
bridge_url: DEFAULT_BRIDGE_URL,
|
|
1588
|
+
command: launcher.command
|
|
1351
1589
|
});
|
|
1352
|
-
|
|
1590
|
+
}
|
|
1591
|
+
function isLocalBridgeURL(url) {
|
|
1592
|
+
const host = url.hostname.toLowerCase();
|
|
1593
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1594
|
+
}
|
|
1595
|
+
async function waitForBridgeHealth(bridgeURL, timeoutMs) {
|
|
1596
|
+
const deadline = Date.now() + timeoutMs;
|
|
1597
|
+
let lastError = "";
|
|
1598
|
+
while (Date.now() < deadline) {
|
|
1599
|
+
try {
|
|
1600
|
+
const response = await fetch(new URL("/healthz", bridgeURL), {
|
|
1601
|
+
headers: bridgeHeaders("GET", "/healthz", "")
|
|
1602
|
+
});
|
|
1603
|
+
const parsed = await response.json();
|
|
1604
|
+
if (parsed.ok) {
|
|
1605
|
+
return parsed;
|
|
1606
|
+
}
|
|
1607
|
+
lastError = parsed.error?.message ?? `HTTP ${response.status}`;
|
|
1608
|
+
}
|
|
1609
|
+
catch (error) {
|
|
1610
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
1611
|
+
}
|
|
1612
|
+
await delay(100);
|
|
1613
|
+
}
|
|
1614
|
+
return fail("bridge_start_failed", `embed-local-bridge did not become healthy at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1615
|
+
remediation: `Run embed bridge start in a separate terminal and retry.`,
|
|
1616
|
+
details: { last_error: lastError }
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
function delay(ms) {
|
|
1620
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
1353
1621
|
}
|
|
1354
1622
|
async function cloudGet(path) {
|
|
1355
1623
|
return await cloudRequest("GET", path);
|
|
@@ -1360,16 +1628,23 @@ async function cloudPost(path, body) {
|
|
|
1360
1628
|
async function cloudDownloadArtifact(artifactId, outputPath) {
|
|
1361
1629
|
try {
|
|
1362
1630
|
const headers = {};
|
|
1363
|
-
const
|
|
1364
|
-
if (token) {
|
|
1365
|
-
|
|
1631
|
+
const auth = await cloudAuthConfig();
|
|
1632
|
+
if (auth.token) {
|
|
1633
|
+
if (auth.device) {
|
|
1634
|
+
const integrity = await validateLocalDeviceIntegrity(auth.device);
|
|
1635
|
+
if (!integrity.ok) {
|
|
1636
|
+
return integrity;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
headers.authorization = `Bearer ${auth.token}`;
|
|
1640
|
+
addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", auth.token, auth.device);
|
|
1366
1641
|
}
|
|
1367
1642
|
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/artifacts/${encodeURIComponent(artifactId)}/download`, {
|
|
1368
1643
|
headers: Object.keys(headers).length > 0 ? headers : undefined
|
|
1369
1644
|
});
|
|
1370
1645
|
if (!response.ok) {
|
|
1371
1646
|
const parsed = await parseErrorResponse(response);
|
|
1372
|
-
return parsed
|
|
1647
|
+
return parsed ? enrichCloudAuthFailure(parsed, Boolean(auth.token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
|
|
1373
1648
|
}
|
|
1374
1649
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
1375
1650
|
const expectedSha256 = response.headers.get("x-embed-artifact-sha256")?.trim();
|
|
@@ -1398,19 +1673,28 @@ async function cloudDownloadArtifact(artifactId, outputPath) {
|
|
|
1398
1673
|
async function cloudRequest(method, path, body) {
|
|
1399
1674
|
try {
|
|
1400
1675
|
const headers = {};
|
|
1401
|
-
|
|
1676
|
+
const bodyText = body === undefined ? "" : JSON.stringify(body);
|
|
1677
|
+
if (bodyText) {
|
|
1402
1678
|
headers["content-type"] = "application/json";
|
|
1403
1679
|
}
|
|
1404
|
-
const
|
|
1405
|
-
if (token) {
|
|
1406
|
-
|
|
1680
|
+
const auth = await cloudAuthConfig();
|
|
1681
|
+
if (auth.token) {
|
|
1682
|
+
if (auth.device) {
|
|
1683
|
+
const integrity = await validateLocalDeviceIntegrity(auth.device);
|
|
1684
|
+
if (!integrity.ok) {
|
|
1685
|
+
return integrity;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
headers.authorization = `Bearer ${auth.token}`;
|
|
1689
|
+
addCloudRequestSignature(headers, method, path, bodyText, auth.token, auth.device);
|
|
1407
1690
|
}
|
|
1408
1691
|
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}${path}`, {
|
|
1409
1692
|
method,
|
|
1410
1693
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
1411
|
-
body: body === undefined ? undefined :
|
|
1694
|
+
body: body === undefined ? undefined : bodyText
|
|
1412
1695
|
});
|
|
1413
|
-
|
|
1696
|
+
const parsed = await response.json();
|
|
1697
|
+
return enrichCloudAuthFailure(parsed, Boolean(auth.token));
|
|
1414
1698
|
}
|
|
1415
1699
|
catch (error) {
|
|
1416
1700
|
return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
|
|
@@ -1418,6 +1702,129 @@ async function cloudRequest(method, path, body) {
|
|
|
1418
1702
|
});
|
|
1419
1703
|
}
|
|
1420
1704
|
}
|
|
1705
|
+
async function validateLocalDeviceIntegrity(device) {
|
|
1706
|
+
const current = await localHardwareFingerprint();
|
|
1707
|
+
if (current.fingerprint_hash === device.fingerprint_hash) {
|
|
1708
|
+
return ok(undefined);
|
|
1709
|
+
}
|
|
1710
|
+
return fail("tool_integrity_check_failed", TOOL_INTEGRITY_RELOGIN_MESSAGE, {
|
|
1711
|
+
remediation: [
|
|
1712
|
+
"当前 Embed Labs CLI/插件配置绑定的电脑与本机硬件唯一码不一致。",
|
|
1713
|
+
TOOL_INTEGRITY_RELOGIN_MESSAGE,
|
|
1714
|
+
"如果账号设备数量已达上限,请先在原电脑或用户后台撤销旧设备。"
|
|
1715
|
+
].join("\n"),
|
|
1716
|
+
details: {
|
|
1717
|
+
expected_fingerprint_hash: device.fingerprint_hash,
|
|
1718
|
+
current_fingerprint_hash: current.fingerprint_hash,
|
|
1719
|
+
platform: current.platform,
|
|
1720
|
+
arch: current.arch,
|
|
1721
|
+
fingerprint_source: current.source
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
function enrichCloudAuthFailure(response, hadToken) {
|
|
1726
|
+
if (response.ok) {
|
|
1727
|
+
return response;
|
|
1728
|
+
}
|
|
1729
|
+
if (response.error.code.startsWith("device_") || response.error.code.startsWith("request_signature_")) {
|
|
1730
|
+
return fail(response.error.code, response.error.message, {
|
|
1731
|
+
remediation: [
|
|
1732
|
+
"This computer is not fully registered for the configured Embed Labs API Token.",
|
|
1733
|
+
"Run: embedlabs auth login --token <your_token>",
|
|
1734
|
+
"Then verify with: embedlabs auth device status",
|
|
1735
|
+
"If the account already has too many devices, revoke one with: embedlabs auth device revoke <device_id>"
|
|
1736
|
+
].join("\n"),
|
|
1737
|
+
details: response.error.details
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
if (response.error.code !== "unauthorized") {
|
|
1741
|
+
return response;
|
|
1742
|
+
}
|
|
1743
|
+
if (!hadToken) {
|
|
1744
|
+
return fail("auth_token_missing", "Embed Labs API Token is not configured. Register or sign in, create an API Token, then configure it locally before using cloud and plugin services.", {
|
|
1745
|
+
remediation: cloudAuthSetupRemediation(),
|
|
1746
|
+
details: cloudAuthSetupDetails()
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
return fail("auth_token_rejected", "The configured Embed Labs API Token was rejected by the server. Recreate or copy a fresh token from the dashboard and sign in again.", {
|
|
1750
|
+
remediation: cloudAuthSetupRemediation(),
|
|
1751
|
+
details: cloudAuthSetupDetails()
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
function cloudAuthSetupRemediation() {
|
|
1755
|
+
return [
|
|
1756
|
+
`1. Open ${DEFAULT_DASHBOARD_URL} and register or sign in.`,
|
|
1757
|
+
"2. Create or copy your Embed Labs API Token from the user dashboard.",
|
|
1758
|
+
"3. Run: embedlabs auth login --token <your_token>",
|
|
1759
|
+
"4. For automation, set: EMBED_API_TOKEN=<your_token>",
|
|
1760
|
+
"5. Verify with: embedlabs auth status"
|
|
1761
|
+
].join("\n");
|
|
1762
|
+
}
|
|
1763
|
+
function cloudAuthSetupDetails() {
|
|
1764
|
+
return {
|
|
1765
|
+
dashboard_url: DEFAULT_DASHBOARD_URL,
|
|
1766
|
+
login_command: "embedlabs auth login --token <your_token>",
|
|
1767
|
+
env_var: "EMBED_API_TOKEN",
|
|
1768
|
+
auth_status_command: "embedlabs auth status",
|
|
1769
|
+
auth_file: DEFAULT_AUTH_FILE
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token, device) {
|
|
1773
|
+
if (process.env.EMBED_CLOUD_API_SIGNING === "0") {
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
1777
|
+
const nonce = randomBytes(16).toString("hex");
|
|
1778
|
+
const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
|
|
1779
|
+
const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
1780
|
+
const canonical = device
|
|
1781
|
+
? cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, EMBED_CLIENT_NAME, EMBED_CLIENT_VERSION)
|
|
1782
|
+
: cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
|
|
1783
|
+
headers["x-embed-key-id"] = keyId;
|
|
1784
|
+
headers["x-embed-timestamp"] = timestamp;
|
|
1785
|
+
headers["x-embed-nonce"] = nonce;
|
|
1786
|
+
headers["x-embed-body-sha256"] = bodySha256;
|
|
1787
|
+
if (device) {
|
|
1788
|
+
headers["x-embed-signature-version"] = "v2";
|
|
1789
|
+
headers["x-embed-device-id"] = device.device_id;
|
|
1790
|
+
headers["x-embed-device-fingerprint-sha256"] = device.fingerprint_hash;
|
|
1791
|
+
headers["x-embed-client-name"] = EMBED_CLIENT_NAME;
|
|
1792
|
+
headers["x-embed-client-version"] = EMBED_CLIENT_VERSION;
|
|
1793
|
+
headers["x-embed-device-signature"] = signCrypto(null, Buffer.from(canonical), device.private_key_pem).toString("base64url");
|
|
1794
|
+
}
|
|
1795
|
+
headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
|
|
1796
|
+
}
|
|
1797
|
+
function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256) {
|
|
1798
|
+
return [
|
|
1799
|
+
method.toUpperCase(),
|
|
1800
|
+
normalizeCloudPathForSignature(pathWithQuery),
|
|
1801
|
+
timestamp,
|
|
1802
|
+
nonce,
|
|
1803
|
+
bodySha256
|
|
1804
|
+
].join("\n");
|
|
1805
|
+
}
|
|
1806
|
+
function cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, clientName, clientVersion) {
|
|
1807
|
+
return [
|
|
1808
|
+
method.toUpperCase(),
|
|
1809
|
+
normalizeCloudPathForSignature(pathWithQuery),
|
|
1810
|
+
timestamp,
|
|
1811
|
+
nonce,
|
|
1812
|
+
bodySha256,
|
|
1813
|
+
device.device_id,
|
|
1814
|
+
device.fingerprint_hash,
|
|
1815
|
+
clientName,
|
|
1816
|
+
clientVersion
|
|
1817
|
+
].join("\n");
|
|
1818
|
+
}
|
|
1819
|
+
function normalizeCloudPathForSignature(pathWithQuery) {
|
|
1820
|
+
try {
|
|
1821
|
+
const parsed = new URL(pathWithQuery, "http://embed.local");
|
|
1822
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
1823
|
+
}
|
|
1824
|
+
catch {
|
|
1825
|
+
return pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1421
1828
|
async function pluginList(parsed) {
|
|
1422
1829
|
const releaseDir = stringFlag(parsed, "release-dir");
|
|
1423
1830
|
const manifest = releaseDir ? await readPluginReleaseManifest(releaseDir) : undefined;
|
|
@@ -1517,13 +1924,112 @@ async function pluginInstall(parsed) {
|
|
|
1517
1924
|
await rm(tempDir, { recursive: true, force: true });
|
|
1518
1925
|
}
|
|
1519
1926
|
}
|
|
1927
|
+
async function pluginUpdateCheck(parsed) {
|
|
1928
|
+
const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
|
|
1929
|
+
if (unknownFlag) {
|
|
1930
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
|
|
1931
|
+
}
|
|
1932
|
+
const unexpected = parsed.command.slice(3);
|
|
1933
|
+
if (unexpected.length > 0) {
|
|
1934
|
+
return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
|
|
1935
|
+
}
|
|
1936
|
+
const remoteManifest = await fetchRemotePluginManifest(parsed);
|
|
1937
|
+
if (!remoteManifest.ok) {
|
|
1938
|
+
return remoteManifest;
|
|
1939
|
+
}
|
|
1940
|
+
const manifest = remoteManifest.data;
|
|
1941
|
+
const codexPackage = manifest.packages?.find((item) => item.id === "codex-embed-labs");
|
|
1942
|
+
const opencodePackage = manifest.packages?.find((item) => item.id === "opencode-embed-labs");
|
|
1943
|
+
const codexTarget = join(codexPluginTargetRoot(parsed, true), CODEX_PLUGIN_NAME);
|
|
1944
|
+
const openCodeTarget = openCodePluginTargetRoot(parsed, true);
|
|
1945
|
+
return ok({
|
|
1946
|
+
release_url: pluginReleaseBaseUrl(parsed),
|
|
1947
|
+
latest_version: manifest.version,
|
|
1948
|
+
release_notes: normalizedReleaseNotes(manifest.release_notes),
|
|
1949
|
+
plugins: [
|
|
1950
|
+
await pluginUpdateItem({
|
|
1951
|
+
id: "codex",
|
|
1952
|
+
displayName: "Embed Labs Codex plugin",
|
|
1953
|
+
targetPath: codexTarget,
|
|
1954
|
+
installedVersion: await installedCodexPluginVersion(codexTarget),
|
|
1955
|
+
latestVersion: codexPackage?.version ?? manifest.version,
|
|
1956
|
+
releaseFile: codexPackage?.file,
|
|
1957
|
+
updateCommand: "embedlabs plugin update codex"
|
|
1958
|
+
}),
|
|
1959
|
+
await pluginUpdateItem({
|
|
1960
|
+
id: "opencode",
|
|
1961
|
+
displayName: "Embed Labs OpenCode plugin",
|
|
1962
|
+
targetPath: openCodeTarget,
|
|
1963
|
+
installedVersion: await installedOpenCodePluginVersion(openCodeTarget),
|
|
1964
|
+
latestVersion: opencodePackage?.version ?? manifest.version,
|
|
1965
|
+
releaseFile: opencodePackage?.file,
|
|
1966
|
+
updateCommand: "embedlabs plugin update opencode"
|
|
1967
|
+
})
|
|
1968
|
+
]
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
function normalizedReleaseNotes(notes) {
|
|
1972
|
+
if (!Array.isArray(notes)) {
|
|
1973
|
+
return [];
|
|
1974
|
+
}
|
|
1975
|
+
return notes
|
|
1976
|
+
.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
1977
|
+
.map((item) => item.trim());
|
|
1978
|
+
}
|
|
1979
|
+
async function pluginUpdate(parsed) {
|
|
1980
|
+
const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
|
|
1981
|
+
if (unknownFlag) {
|
|
1982
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_USAGE}`);
|
|
1983
|
+
}
|
|
1984
|
+
const target = parsed.command[2];
|
|
1985
|
+
if (!target || !["codex", "opencode", "all"].includes(target)) {
|
|
1986
|
+
return fail("invalid_args", PLUGIN_UPDATE_USAGE);
|
|
1987
|
+
}
|
|
1988
|
+
const unexpected = parsed.command.slice(3);
|
|
1989
|
+
if (unexpected.length > 0) {
|
|
1990
|
+
return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_USAGE}`);
|
|
1991
|
+
}
|
|
1992
|
+
const installParsed = {
|
|
1993
|
+
...parsed,
|
|
1994
|
+
command: ["plugin", "install", target],
|
|
1995
|
+
flags: { ...parsed.flags, force: true }
|
|
1996
|
+
};
|
|
1997
|
+
return await pluginInstall(installParsed);
|
|
1998
|
+
}
|
|
1999
|
+
async function pluginUpdateItem(input) {
|
|
2000
|
+
const installed = !!input.installedVersion;
|
|
2001
|
+
const updateAvailable = !!input.latestVersion && input.installedVersion !== input.latestVersion;
|
|
2002
|
+
const notes = [];
|
|
2003
|
+
if (!installed) {
|
|
2004
|
+
notes.push("Plugin is not installed in the selected target.");
|
|
2005
|
+
}
|
|
2006
|
+
else if (updateAvailable) {
|
|
2007
|
+
notes.push("A newer plugin release is available. Run the update command, then restart Codex/OpenCode.");
|
|
2008
|
+
}
|
|
2009
|
+
else {
|
|
2010
|
+
notes.push("Plugin is up to date for the selected release channel.");
|
|
2011
|
+
}
|
|
2012
|
+
return {
|
|
2013
|
+
id: input.id,
|
|
2014
|
+
display_name: input.displayName,
|
|
2015
|
+
installed,
|
|
2016
|
+
installed_version: input.installedVersion,
|
|
2017
|
+
latest_version: input.latestVersion,
|
|
2018
|
+
update_available: updateAvailable,
|
|
2019
|
+
target_path: input.targetPath,
|
|
2020
|
+
release_file: input.releaseFile,
|
|
2021
|
+
update_command: input.updateCommand,
|
|
2022
|
+
notes
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
1520
2025
|
async function installCodexPlugin(parsed, context) {
|
|
1521
2026
|
const source = await resolveCodexPluginSource(context);
|
|
1522
2027
|
if (!source.ok) {
|
|
1523
2028
|
return source;
|
|
1524
2029
|
}
|
|
1525
2030
|
const targetRoot = codexPluginTargetRoot(parsed, context.installingAll);
|
|
1526
|
-
const targetPath = join(targetRoot,
|
|
2031
|
+
const targetPath = join(targetRoot, CODEX_PLUGIN_NAME);
|
|
2032
|
+
const legacyCleanup = await cleanupLegacyCodexPluginRemnants(targetRoot);
|
|
1527
2033
|
if (await pathExists(targetPath) && !booleanFlag(parsed, "force")) {
|
|
1528
2034
|
return fail("plugin_already_installed", `Codex plugin already exists at ${targetPath}.`, {
|
|
1529
2035
|
remediation: "Pass --force to replace it, or pass --codex-target/--target to install into a different directory."
|
|
@@ -1533,16 +2039,24 @@ async function installCodexPlugin(parsed, context) {
|
|
|
1533
2039
|
await mkdir(targetRoot, { recursive: true });
|
|
1534
2040
|
await cp(source.data.sourcePath, targetPath, { recursive: true });
|
|
1535
2041
|
const mcpRegistration = await maybeRegisterCodexMcp(parsed, targetRoot, targetPath);
|
|
2042
|
+
const marketplaceRegistration = await maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath);
|
|
1536
2043
|
return ok({
|
|
1537
2044
|
id: "codex",
|
|
1538
2045
|
target_path: targetPath,
|
|
1539
2046
|
source: source.data.sourceLabel,
|
|
1540
2047
|
version: source.data.version,
|
|
1541
2048
|
command_hint: mcpRegistration.registered
|
|
1542
|
-
?
|
|
2049
|
+
? (marketplaceRegistration.registered
|
|
2050
|
+
? "Codex MCP and plugin marketplace entry were registered. Fully restart Codex to reload @Embed Labs."
|
|
2051
|
+
: "Codex MCP was registered. Start a new Codex session to reload tools.")
|
|
1543
2052
|
: mcpRegistration.hint,
|
|
2053
|
+
warning: legacyCodexCleanupWarning(legacyCleanup),
|
|
1544
2054
|
mcp_registered: mcpRegistration.registered,
|
|
1545
|
-
mcp_warning: mcpRegistration.warning
|
|
2055
|
+
mcp_warning: mcpRegistration.warning,
|
|
2056
|
+
marketplace_registered: marketplaceRegistration.registered,
|
|
2057
|
+
marketplace_path: marketplaceRegistration.marketplacePath,
|
|
2058
|
+
marketplace_warning: marketplaceRegistration.warning,
|
|
2059
|
+
cleanup: legacyCleanup
|
|
1546
2060
|
});
|
|
1547
2061
|
}
|
|
1548
2062
|
async function installOpenCodePlugin(parsed, context) {
|
|
@@ -1551,8 +2065,10 @@ async function installOpenCodePlugin(parsed, context) {
|
|
|
1551
2065
|
return source;
|
|
1552
2066
|
}
|
|
1553
2067
|
const targetRoot = openCodePluginTargetRoot(parsed, context.installingAll);
|
|
1554
|
-
const
|
|
1555
|
-
|
|
2068
|
+
const globalInstall = isGlobalOpenCodeRoot(targetRoot);
|
|
2069
|
+
const wrapperPath = join(targetRoot, "plugins", "embed-labs.js");
|
|
2070
|
+
const legacyCleanup = await cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall);
|
|
2071
|
+
if (!globalInstall && await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
|
|
1556
2072
|
return fail("plugin_already_installed", `OpenCode plugin wrapper already exists at ${wrapperPath}.`, {
|
|
1557
2073
|
remediation: "Pass --force to replace it, or pass --opencode-target/--target to install into a different directory."
|
|
1558
2074
|
});
|
|
@@ -1578,15 +2094,25 @@ async function installOpenCodePlugin(parsed, context) {
|
|
|
1578
2094
|
});
|
|
1579
2095
|
}
|
|
1580
2096
|
await ensureOpenCodeInstallPackageJson(targetRoot);
|
|
1581
|
-
|
|
2097
|
+
if (globalInstall) {
|
|
2098
|
+
await rm(wrapperPath, { force: true });
|
|
2099
|
+
legacyCleanup.legacy_removed_config_entries?.push(...await ensureOpenCodeGlobalPluginConfig());
|
|
2100
|
+
}
|
|
2101
|
+
else {
|
|
2102
|
+
await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
|
|
2103
|
+
}
|
|
1582
2104
|
const duplicateWarning = await openCodeDuplicatePluginWarning(targetRoot);
|
|
2105
|
+
const cleanupWarning = legacyOpenCodeCleanupWarning(legacyCleanup);
|
|
1583
2106
|
return ok({
|
|
1584
2107
|
id: "opencode",
|
|
1585
2108
|
target_path: targetRoot,
|
|
1586
2109
|
source: source.data.sourceLabel,
|
|
1587
2110
|
version: source.data.version,
|
|
1588
|
-
command_hint:
|
|
1589
|
-
|
|
2111
|
+
command_hint: globalInstall
|
|
2112
|
+
? "Restart OpenCode so the global embed-labs package plugin is reloaded."
|
|
2113
|
+
: "Start OpenCode from the project containing this .opencode directory.",
|
|
2114
|
+
warning: combineWarnings(cleanupWarning, duplicateWarning),
|
|
2115
|
+
cleanup: legacyCleanup
|
|
1590
2116
|
});
|
|
1591
2117
|
}
|
|
1592
2118
|
async function resolveCodexPluginSource(context) {
|
|
@@ -1800,14 +2326,465 @@ function openCodePluginTargetRoot(parsed, installingAll) {
|
|
|
1800
2326
|
return resolve(target ?? defaultOpenCodeRoot());
|
|
1801
2327
|
}
|
|
1802
2328
|
function defaultCodexPluginRoot() {
|
|
1803
|
-
return join(
|
|
2329
|
+
return join(defaultCodexHome(), "plugins");
|
|
2330
|
+
}
|
|
2331
|
+
function defaultCodexHome() {
|
|
2332
|
+
return resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
2333
|
+
}
|
|
2334
|
+
function codexConfigPath() {
|
|
2335
|
+
return join(defaultCodexHome(), "config.toml");
|
|
2336
|
+
}
|
|
2337
|
+
async function cleanupLegacyCodexPluginRemnants(targetRoot) {
|
|
2338
|
+
const removedPaths = [];
|
|
2339
|
+
const removedConfigTables = [];
|
|
2340
|
+
const warnings = [];
|
|
2341
|
+
const stoppedProcesses = await stopLegacyCodexPluginProcesses(warnings);
|
|
2342
|
+
const legacyPaths = [
|
|
2343
|
+
join(targetRoot, "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME)
|
|
2344
|
+
];
|
|
2345
|
+
for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
|
|
2346
|
+
legacyPaths.push(join(targetRoot, "cache", marketplaceName, CODEX_PLUGIN_NAME));
|
|
2347
|
+
}
|
|
2348
|
+
for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
|
|
2349
|
+
legacyPaths.push(join(targetRoot, pluginName));
|
|
2350
|
+
legacyPaths.push(join(targetRoot, "cache", pluginName));
|
|
2351
|
+
for (const marketplaceName of [CODEX_MARKETPLACE_NAME, ...LEGACY_CODEX_MARKETPLACE_NAMES]) {
|
|
2352
|
+
legacyPaths.push(join(targetRoot, "cache", marketplaceName, pluginName));
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
legacyPaths.push(...await discoverLegacyCodexCachePaths(targetRoot));
|
|
2356
|
+
if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
|
|
2357
|
+
legacyPaths.push(join(defaultCodexHome(), ".tmp", "plugins"), join(defaultCodexHome(), ".tmp", "plugins.sha"), join(defaultCodexHome(), "ambient-suggestions"), join(defaultCodexHome(), "skills", "dbt-agent"), join(defaultCodexHome(), "skills", "development-board-toolchain-dev"), join(defaultCodexHome(), "memories", "skills", "dbt-agent-live-board-ops"), join(defaultCodexHome(), "memories", "skills", "dbt-agent-platform-plugin-maintenance"));
|
|
2358
|
+
legacyPaths.push(...legacyCodexLocalMarketplacePaths(), ...await discoverLegacyHomeAgentsMarketplacePaths(warnings), ...await discoverLegacyCodexProjectMarketplacePaths(warnings), ...legacyDevelopmentBoardRuntimePluginPaths());
|
|
2359
|
+
}
|
|
2360
|
+
for (const candidate of legacyPaths) {
|
|
2361
|
+
try {
|
|
2362
|
+
if (await pathExists(candidate)) {
|
|
2363
|
+
await rm(candidate, { recursive: true, force: true });
|
|
2364
|
+
removedPaths.push(candidate);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
catch (error) {
|
|
2368
|
+
warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
|
|
2372
|
+
const configPath = codexConfigPath();
|
|
2373
|
+
try {
|
|
2374
|
+
if (await pathExists(configPath)) {
|
|
2375
|
+
const current = await readFile(configPath, "utf8");
|
|
2376
|
+
const updated = removeLegacyCodexConfigTables(current);
|
|
2377
|
+
if (updated.text !== current) {
|
|
2378
|
+
await writeFile(configPath, updated.text, "utf8");
|
|
2379
|
+
}
|
|
2380
|
+
removedConfigTables.push(...updated.removedTables);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
catch (error) {
|
|
2384
|
+
warnings.push(`Could not update ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
const removedHistoryEntries = await cleanupLegacyCodexTextState(warnings);
|
|
2388
|
+
return {
|
|
2389
|
+
legacy_removed_paths: Array.from(new Set(removedPaths)),
|
|
2390
|
+
legacy_removed_config_tables: removedConfigTables,
|
|
2391
|
+
legacy_removed_history_entries: removedHistoryEntries,
|
|
2392
|
+
legacy_stopped_processes: stoppedProcesses,
|
|
2393
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
async function stopLegacyCodexPluginProcesses(warnings) {
|
|
2397
|
+
if (process.platform === "win32")
|
|
2398
|
+
return 0;
|
|
2399
|
+
try {
|
|
2400
|
+
const ps = await runLocalProcess("ps", ["-axo", "pid=,command="]);
|
|
2401
|
+
if (ps.code !== 0)
|
|
2402
|
+
return 0;
|
|
2403
|
+
let stopped = 0;
|
|
2404
|
+
for (const line of ps.stdout.split("\n")) {
|
|
2405
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
2406
|
+
if (!match)
|
|
2407
|
+
continue;
|
|
2408
|
+
const pid = Number(match[1]);
|
|
2409
|
+
const command = match[2] || "";
|
|
2410
|
+
if (!isLegacyCodexPluginProcess(command))
|
|
2411
|
+
continue;
|
|
2412
|
+
try {
|
|
2413
|
+
process.kill(pid, "SIGTERM");
|
|
2414
|
+
stopped += 1;
|
|
2415
|
+
}
|
|
2416
|
+
catch (error) {
|
|
2417
|
+
warnings.push(`Could not stop legacy Codex plugin process ${pid}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
return stopped;
|
|
2421
|
+
}
|
|
2422
|
+
catch (error) {
|
|
2423
|
+
warnings.push(`Could not scan legacy Codex plugin processes: ${error instanceof Error ? error.message : String(error)}`);
|
|
2424
|
+
return 0;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
function isLegacyCodexPluginProcess(command) {
|
|
2428
|
+
const trimmed = command.trim();
|
|
2429
|
+
return /^\/.*\/dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
|
|
2430
|
+
|| /^dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
|
|
2431
|
+
|| legacyDevelopmentBoardRuntimeProcessPatterns().some((pattern) => pattern.test(trimmed));
|
|
2432
|
+
}
|
|
2433
|
+
function legacyDevelopmentBoardRuntimeProcessPatterns() {
|
|
2434
|
+
const home = escapeRegExp(homedir());
|
|
2435
|
+
return [
|
|
2436
|
+
new RegExp(`^${home}/Library/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
|
|
2437
|
+
new RegExp(`^${home}/Library/Application Support/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
|
|
2438
|
+
new RegExp(`^${home}/Library/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
|
|
2439
|
+
new RegExp(`^${home}/Library/Application Support/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
|
|
2440
|
+
new RegExp(`^${home}/.*?/DBT-Agent\\.app/Contents/MacOS/DBT-Agent(?:\\s|$)`)
|
|
2441
|
+
];
|
|
2442
|
+
}
|
|
2443
|
+
function legacyDevelopmentBoardRuntimePluginPaths() {
|
|
2444
|
+
return [
|
|
2445
|
+
join(homedir(), "Library", "development-board-toolchain", "runtime", "editor_plugins"),
|
|
2446
|
+
join(homedir(), "Library", "development-board-toolchain", "runtime", "opencode_plugin"),
|
|
2447
|
+
join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "editor_plugins"),
|
|
2448
|
+
join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "opencode_plugin")
|
|
2449
|
+
];
|
|
2450
|
+
}
|
|
2451
|
+
function legacyCodexLocalMarketplacePaths() {
|
|
2452
|
+
return Array.from(LEGACY_CODEX_MARKETPLACE_NAMES)
|
|
2453
|
+
.filter((name) => name !== CODEX_MARKETPLACE_NAME)
|
|
2454
|
+
.map((name) => join(defaultCodexHome(), "local-marketplaces", name));
|
|
2455
|
+
}
|
|
2456
|
+
async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
|
|
2457
|
+
const paths = [];
|
|
2458
|
+
const pluginRoot = join(homedir(), ".agents", "plugins");
|
|
2459
|
+
try {
|
|
2460
|
+
const entries = await readdir(pluginRoot, { withFileTypes: true });
|
|
2461
|
+
for (const entry of entries) {
|
|
2462
|
+
if (!entry.isFile() || !entry.name.startsWith("marketplace.json"))
|
|
2463
|
+
continue;
|
|
2464
|
+
const filePath = join(pluginRoot, entry.name);
|
|
2465
|
+
try {
|
|
2466
|
+
const current = await readFile(filePath, "utf8");
|
|
2467
|
+
if (isLegacyHomeAgentsMarketplace(current)) {
|
|
2468
|
+
paths.push(filePath);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
catch (error) {
|
|
2472
|
+
warnings.push(`Could not inspect legacy Codex home marketplace ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
catch {
|
|
2477
|
+
return paths;
|
|
2478
|
+
}
|
|
2479
|
+
return paths;
|
|
2480
|
+
}
|
|
2481
|
+
function isLegacyHomeAgentsMarketplace(text) {
|
|
2482
|
+
return marketplaceTextHasLegacyCodexPlugin(text);
|
|
2483
|
+
}
|
|
2484
|
+
async function discoverLegacyCodexProjectMarketplacePaths(warnings) {
|
|
2485
|
+
const configPath = codexConfigPath();
|
|
2486
|
+
let text = "";
|
|
2487
|
+
try {
|
|
2488
|
+
text = await readFile(configPath, "utf8");
|
|
2489
|
+
}
|
|
2490
|
+
catch {
|
|
2491
|
+
return [];
|
|
2492
|
+
}
|
|
2493
|
+
const paths = new Set();
|
|
2494
|
+
for (const projectPath of legacyCodexProjectPathsFromConfig(text)) {
|
|
2495
|
+
for (const candidate of [
|
|
2496
|
+
join(projectPath, ".agents", "plugins", "marketplace.json"),
|
|
2497
|
+
join(projectPath, "platform_plugin", ".agents", "plugins", "marketplace.json"),
|
|
2498
|
+
join(projectPath, "platform_plugins", "codex_plugin", ".agents", "plugins", "marketplace.json")
|
|
2499
|
+
]) {
|
|
2500
|
+
try {
|
|
2501
|
+
if (!await pathExists(candidate))
|
|
2502
|
+
continue;
|
|
2503
|
+
const current = await readFile(candidate, "utf8");
|
|
2504
|
+
if (marketplaceTextHasLegacyCodexPlugin(current)) {
|
|
2505
|
+
paths.add(candidate);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
catch (error) {
|
|
2509
|
+
warnings.push(`Could not inspect legacy Codex project marketplace ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
return Array.from(paths);
|
|
2514
|
+
}
|
|
2515
|
+
function legacyCodexProjectPathsFromConfig(text) {
|
|
2516
|
+
const paths = [];
|
|
2517
|
+
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
2518
|
+
for (const line of lines) {
|
|
2519
|
+
const table = parseTomlTableHeader(line);
|
|
2520
|
+
const match = table ? /^projects\."([^"]+)"$/.exec(table) : undefined;
|
|
2521
|
+
if (match?.[1] && /DBT-Agent-Project|development-board-toolchain|dbt-agent/i.test(match[1])) {
|
|
2522
|
+
paths.push(match[1].replace(/\\"/g, '"'));
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
return paths;
|
|
2526
|
+
}
|
|
2527
|
+
function marketplaceTextHasLegacyCodexPlugin(text) {
|
|
2528
|
+
try {
|
|
2529
|
+
const parsed = JSON.parse(text);
|
|
2530
|
+
const marketplaceName = typeof parsed.name === "string" ? parsed.name : "";
|
|
2531
|
+
const marketplaceDisplayName = typeof parsed.interface?.displayName === "string" ? parsed.interface.displayName : "";
|
|
2532
|
+
const marketplaceLooksLegacy = legacyTextHasCodexPluginResidue(marketplaceName)
|
|
2533
|
+
|| legacyTextHasCodexPluginResidue(marketplaceDisplayName)
|
|
2534
|
+
|| LEGACY_CODEX_MARKETPLACE_NAMES.has(marketplaceName);
|
|
2535
|
+
return (parsed.plugins ?? []).some((plugin) => {
|
|
2536
|
+
const values = [
|
|
2537
|
+
plugin.name,
|
|
2538
|
+
plugin.category,
|
|
2539
|
+
plugin.source?.path,
|
|
2540
|
+
plugin.interface?.displayName
|
|
2541
|
+
].filter((value) => typeof value === "string");
|
|
2542
|
+
return values.some(legacyTextHasCodexPluginResidue) || marketplaceLooksLegacy && values.some((value) => /embed-labs|dbt|development-board/i.test(value));
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
catch {
|
|
2546
|
+
return legacyTextHasCodexPluginResidue(text);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
async function discoverLegacyCodexCachePaths(targetRoot) {
|
|
2550
|
+
const paths = [];
|
|
2551
|
+
const cacheRoot = join(targetRoot, "cache");
|
|
2552
|
+
try {
|
|
2553
|
+
const marketplaces = await readdir(cacheRoot, { withFileTypes: true });
|
|
2554
|
+
for (const entry of marketplaces) {
|
|
2555
|
+
if (!entry.isDirectory())
|
|
2556
|
+
continue;
|
|
2557
|
+
if (LEGACY_CODEX_MARKETPLACE_NAMES.has(entry.name)) {
|
|
2558
|
+
paths.push(join(cacheRoot, entry.name, CODEX_PLUGIN_NAME));
|
|
2559
|
+
}
|
|
2560
|
+
for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
|
|
2561
|
+
paths.push(join(cacheRoot, entry.name, pluginName));
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
catch {
|
|
2566
|
+
return paths;
|
|
2567
|
+
}
|
|
2568
|
+
return paths;
|
|
2569
|
+
}
|
|
2570
|
+
async function cleanupLegacyCodexTextState(warnings) {
|
|
2571
|
+
let removed = 0;
|
|
2572
|
+
removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "history.jsonl"), warnings);
|
|
2573
|
+
removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "session_index.jsonl"), warnings);
|
|
2574
|
+
removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "rules", "default.rules"), warnings);
|
|
2575
|
+
return removed;
|
|
2576
|
+
}
|
|
2577
|
+
async function cleanupLegacyCodexTextFile(filePath, warnings) {
|
|
2578
|
+
try {
|
|
2579
|
+
if (!await pathExists(filePath))
|
|
2580
|
+
return 0;
|
|
2581
|
+
const current = await readFile(filePath, "utf8");
|
|
2582
|
+
const lines = current.split("\n");
|
|
2583
|
+
let removed = 0;
|
|
2584
|
+
const kept = lines.filter((line) => {
|
|
2585
|
+
if (!line)
|
|
2586
|
+
return true;
|
|
2587
|
+
if (isLegacyCodexHistoryMention(line)) {
|
|
2588
|
+
removed += 1;
|
|
2589
|
+
return false;
|
|
2590
|
+
}
|
|
2591
|
+
return true;
|
|
2592
|
+
});
|
|
2593
|
+
if (removed > 0) {
|
|
2594
|
+
await writeFile(filePath, kept.join("\n"), "utf8");
|
|
2595
|
+
}
|
|
2596
|
+
return removed;
|
|
2597
|
+
}
|
|
2598
|
+
catch (error) {
|
|
2599
|
+
warnings.push(`Could not clean Codex legacy text state ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2600
|
+
return 0;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function isLegacyCodexHistoryMention(line) {
|
|
2604
|
+
return line.includes("plugin://dbt-agent@plugins")
|
|
2605
|
+
|| line.includes("plugin://dbt-agent@embed-labs")
|
|
2606
|
+
|| line.includes("development-board-toolchain-dev")
|
|
2607
|
+
|| line.includes("development-board-toolchain")
|
|
2608
|
+
|| /plugin:\/\/Dbt Agent@/i.test(line)
|
|
2609
|
+
|| /plugin:\/\/deve@/i.test(line)
|
|
2610
|
+
|| /dbt-agent/i.test(line);
|
|
2611
|
+
}
|
|
2612
|
+
function removeLegacyCodexConfigTables(text) {
|
|
2613
|
+
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
2614
|
+
const output = [];
|
|
2615
|
+
const removedTables = [];
|
|
2616
|
+
let skipping = false;
|
|
2617
|
+
for (const line of lines) {
|
|
2618
|
+
const table = parseTomlTableHeader(line);
|
|
2619
|
+
if (table) {
|
|
2620
|
+
skipping = isLegacyCodexConfigTable(table);
|
|
2621
|
+
if (skipping) {
|
|
2622
|
+
removedTables.push(table);
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
if (!skipping) {
|
|
2627
|
+
output.push(line);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
return { text: output.join("").replace(/\n{3,}/g, "\n\n"), removedTables };
|
|
2631
|
+
}
|
|
2632
|
+
function parseTomlTableHeader(line) {
|
|
2633
|
+
const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line);
|
|
2634
|
+
return match?.[1]?.trim();
|
|
2635
|
+
}
|
|
2636
|
+
function isLegacyCodexConfigTable(table) {
|
|
2637
|
+
return /^plugins\."dbt-agent@[^"]+"$/.test(table)
|
|
2638
|
+
|| /^plugins\."Dbt Agent@[^"]+"$/i.test(table)
|
|
2639
|
+
|| /^plugins\."deve@[^"]+"$/i.test(table)
|
|
2640
|
+
|| isLegacyEmbedLabsCodexMarketplaceConfigTable(table)
|
|
2641
|
+
|| table === "mcp_servers.dbt-agent"
|
|
2642
|
+
|| table.startsWith("mcp_servers.dbt-agent.")
|
|
2643
|
+
|| table === 'mcp_servers."dbt-agent"'
|
|
2644
|
+
|| table.startsWith('mcp_servers."dbt-agent".')
|
|
2645
|
+
|| table === "mcp_servers.deve"
|
|
2646
|
+
|| table.startsWith("mcp_servers.deve.")
|
|
2647
|
+
|| /^projects\."[^"]*\/DBT-Agent-Project(?:\/[^"]*)?"$/.test(table);
|
|
2648
|
+
}
|
|
2649
|
+
function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
|
|
2650
|
+
for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
|
|
2651
|
+
if (table === `marketplaces.${marketplaceName}`) {
|
|
2652
|
+
return true;
|
|
2653
|
+
}
|
|
2654
|
+
if (table === `plugins."${CODEX_PLUGIN_NAME}@${marketplaceName}"`) {
|
|
2655
|
+
return true;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
return false;
|
|
2659
|
+
}
|
|
2660
|
+
function legacyTextHasCodexPluginResidue(value) {
|
|
2661
|
+
return /dbt-agent|Dbt Agent|development-board-toolchain|development-board-toolchain-dev/i.test(value)
|
|
2662
|
+
|| value === "deve"
|
|
2663
|
+
|| value.replace(/\\/g, "/").includes("/plugins/deve")
|
|
2664
|
+
|| value.includes("plugin://deve@");
|
|
2665
|
+
}
|
|
2666
|
+
function legacyCodexCleanupWarning(cleanup) {
|
|
2667
|
+
const parts = [];
|
|
2668
|
+
if (cleanup.legacy_removed_paths.length > 0) {
|
|
2669
|
+
parts.push(`removed ${cleanup.legacy_removed_paths.length} stale/legacy Codex plugin path(s)`);
|
|
2670
|
+
}
|
|
2671
|
+
if (cleanup.legacy_removed_config_tables?.length) {
|
|
2672
|
+
parts.push(`removed ${cleanup.legacy_removed_config_tables.length} legacy Codex config table(s)`);
|
|
2673
|
+
}
|
|
2674
|
+
if (cleanup.legacy_removed_history_entries) {
|
|
2675
|
+
parts.push(`removed ${cleanup.legacy_removed_history_entries} legacy Codex text-state mention(s)`);
|
|
2676
|
+
}
|
|
2677
|
+
if (cleanup.legacy_stopped_processes) {
|
|
2678
|
+
parts.push(`stopped ${cleanup.legacy_stopped_processes} legacy Codex plugin process(es)`);
|
|
2679
|
+
}
|
|
2680
|
+
if (cleanup.warnings?.length) {
|
|
2681
|
+
parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
|
|
2682
|
+
}
|
|
2683
|
+
return parts.length > 0 ? `Codex plugin cleanup: ${parts.join(", ")}.` : undefined;
|
|
2684
|
+
}
|
|
2685
|
+
async function cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall) {
|
|
2686
|
+
const removedPaths = [];
|
|
2687
|
+
const warnings = [];
|
|
2688
|
+
const legacyPaths = [
|
|
2689
|
+
join(targetRoot, "plugins", "development-board-toolchain.js"),
|
|
2690
|
+
join(targetRoot, "plugins", "development-board-toolchain-dev.js"),
|
|
2691
|
+
join(targetRoot, "plugins", "dbt-agent.js"),
|
|
2692
|
+
join(targetRoot, "plugins", "Dbt Agent.js"),
|
|
2693
|
+
join(targetRoot, "plugins", "deve.js"),
|
|
2694
|
+
join(targetRoot, "plugins", "deve"),
|
|
2695
|
+
join(targetRoot, "node_modules", "development-board-toolchain"),
|
|
2696
|
+
join(targetRoot, "node_modules", "development-board-toolchain-dev"),
|
|
2697
|
+
join(targetRoot, "node_modules", "dbt-agent")
|
|
2698
|
+
];
|
|
2699
|
+
if (globalInstall) {
|
|
2700
|
+
legacyPaths.push(join(targetRoot, "plugins", "embed-labs.js"));
|
|
2701
|
+
legacyPaths.push(...await discoverLegacyOpenCodeBackupPaths(targetRoot, warnings));
|
|
2702
|
+
legacyPaths.push(...await discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings));
|
|
2703
|
+
}
|
|
2704
|
+
for (const candidate of legacyPaths) {
|
|
2705
|
+
try {
|
|
2706
|
+
if (await pathExists(candidate)) {
|
|
2707
|
+
await rm(candidate, { recursive: true, force: true });
|
|
2708
|
+
removedPaths.push(candidate);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
catch (error) {
|
|
2712
|
+
warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
return {
|
|
2716
|
+
legacy_removed_paths: removedPaths,
|
|
2717
|
+
legacy_removed_config_entries: [],
|
|
2718
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
|
|
2722
|
+
const paths = [];
|
|
2723
|
+
try {
|
|
2724
|
+
const entries = await readdir(targetRoot, { withFileTypes: true });
|
|
2725
|
+
for (const entry of entries) {
|
|
2726
|
+
if (!entry.isFile() || !entry.name.includes(".bak"))
|
|
2727
|
+
continue;
|
|
2728
|
+
const filePath = join(targetRoot, entry.name);
|
|
2729
|
+
try {
|
|
2730
|
+
const current = await readFile(filePath, "utf8");
|
|
2731
|
+
if (legacyTextHasCodexPluginResidue(current)) {
|
|
2732
|
+
paths.push(filePath);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
catch (error) {
|
|
2736
|
+
warnings.push(`Could not inspect legacy OpenCode backup ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
catch {
|
|
2741
|
+
return paths;
|
|
2742
|
+
}
|
|
2743
|
+
return paths;
|
|
2744
|
+
}
|
|
2745
|
+
async function discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings) {
|
|
2746
|
+
const paths = [];
|
|
2747
|
+
const cacheRoot = join(targetRoot, ".embed-labs", "plugin-cache");
|
|
2748
|
+
try {
|
|
2749
|
+
const entries = await readdir(cacheRoot, { withFileTypes: true });
|
|
2750
|
+
for (const entry of entries) {
|
|
2751
|
+
if (!entry.isFile())
|
|
2752
|
+
continue;
|
|
2753
|
+
if (/^(embed-labs|embed-labs-opencode-plugin)-\d+\.\d+\.\d+\.tgz$/.test(entry.name)) {
|
|
2754
|
+
paths.push(join(cacheRoot, entry.name));
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
catch (error) {
|
|
2759
|
+
if (error.code !== "ENOENT") {
|
|
2760
|
+
warnings.push(`Could not inspect OpenCode plugin cache ${cacheRoot}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
return paths;
|
|
2764
|
+
}
|
|
2765
|
+
function legacyOpenCodeCleanupWarning(cleanup) {
|
|
2766
|
+
const parts = [];
|
|
2767
|
+
if (cleanup.legacy_removed_paths.length > 0) {
|
|
2768
|
+
parts.push(`removed ${cleanup.legacy_removed_paths.length} legacy OpenCode plugin path(s)`);
|
|
2769
|
+
}
|
|
2770
|
+
if (cleanup.legacy_removed_config_entries?.length) {
|
|
2771
|
+
parts.push(`removed ${cleanup.legacy_removed_config_entries.length} legacy OpenCode config entry(s)`);
|
|
2772
|
+
}
|
|
2773
|
+
if (cleanup.warnings?.length) {
|
|
2774
|
+
parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
|
|
2775
|
+
}
|
|
2776
|
+
return parts.length > 0 ? `Legacy OpenCode cleanup: ${parts.join(", ")}.` : undefined;
|
|
2777
|
+
}
|
|
2778
|
+
function combineWarnings(...warnings) {
|
|
2779
|
+
const actual = warnings.filter((warning) => Boolean(warning));
|
|
2780
|
+
return actual.length > 0 ? actual.join(" ") : undefined;
|
|
1804
2781
|
}
|
|
1805
2782
|
async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
|
|
1806
2783
|
const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
|
|
1807
2784
|
if (explicitTarget && process.env.EMBED_CODEX_MCP_REGISTER !== "1") {
|
|
1808
2785
|
return {
|
|
1809
2786
|
registered: false,
|
|
1810
|
-
hint:
|
|
2787
|
+
hint: "Installed into a custom target. Register manually with: codex mcp add embed-labs -- embedlabs mcp start"
|
|
1811
2788
|
};
|
|
1812
2789
|
}
|
|
1813
2790
|
const bridgePath = join(targetPath, "scripts", "embed-labs-mcp-bridge.mjs");
|
|
@@ -1826,11 +2803,12 @@ async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
|
|
|
1826
2803
|
};
|
|
1827
2804
|
}
|
|
1828
2805
|
const embedCliBin = process.env.EMBED_CLI_BIN?.trim() || await resolveExecutableOnPath("embedlabs") || await resolveExecutableOnPath("embed") || "";
|
|
2806
|
+
const mcpLauncher = await resolveEmbedCliMcpLauncher(embedCliBin);
|
|
1829
2807
|
const authFile = resolve(process.env.EMBED_AUTH_FILE?.trim() || DEFAULT_AUTH_FILE);
|
|
1830
2808
|
const cloudUrl = pluginMcpCloudApiUrl(parsed);
|
|
1831
2809
|
const existing = await runLocalProcess(codexBin, ["mcp", "get", "embed-labs", "--json"]);
|
|
1832
|
-
if (existing.code === 0 && codexMcpAlreadyRegistered(existing.stdout, bridgePath, cloudUrl, authFile, embedCliBin)) {
|
|
1833
|
-
const warning = await upsertCodexMcpRuntimeConfig(
|
|
2810
|
+
if (existing.code === 0 && codexMcpAlreadyRegistered(existing.stdout, bridgePath, cloudUrl, authFile, embedCliBin, mcpLauncher.command, mcpLauncher.args)) {
|
|
2811
|
+
const warning = await upsertCodexMcpRuntimeConfig(mcpLauncher.command, mcpLauncher.args);
|
|
1834
2812
|
if (warning) {
|
|
1835
2813
|
return { registered: true, warning };
|
|
1836
2814
|
}
|
|
@@ -1844,31 +2822,152 @@ async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
|
|
|
1844
2822
|
"--env",
|
|
1845
2823
|
`EMBED_CLOUD_API_URL=${cloudUrl}`,
|
|
1846
2824
|
"--env",
|
|
1847
|
-
`EMBED_AUTH_FILE=${authFile}
|
|
2825
|
+
`EMBED_AUTH_FILE=${authFile}`,
|
|
2826
|
+
"--env",
|
|
2827
|
+
`EMBED_MCP_BRIDGE_PATH=${bridgePath}`
|
|
1848
2828
|
];
|
|
1849
2829
|
if (embedCliBin) {
|
|
1850
2830
|
args.push("--env", `EMBED_CLI_BIN=${embedCliBin}`);
|
|
1851
2831
|
}
|
|
1852
|
-
args.push("--",
|
|
2832
|
+
args.push("--", mcpLauncher.command, ...mcpLauncher.args);
|
|
1853
2833
|
const addResult = await runLocalProcess(codexBin, args);
|
|
1854
2834
|
if (addResult.code !== 0) {
|
|
1855
2835
|
return {
|
|
1856
2836
|
registered: false,
|
|
1857
|
-
hint:
|
|
2837
|
+
hint: "Codex plugin installed. Register manually with: codex mcp add embed-labs -- embedlabs mcp start",
|
|
1858
2838
|
warning: `codex mcp add failed: ${addResult.stderr.trim() || addResult.stdout.trim() || `exit ${addResult.code}`}`
|
|
1859
2839
|
};
|
|
1860
2840
|
}
|
|
1861
|
-
const warning = await upsertCodexMcpRuntimeConfig(
|
|
2841
|
+
const warning = await upsertCodexMcpRuntimeConfig(mcpLauncher.command, mcpLauncher.args);
|
|
1862
2842
|
return warning ? { registered: true, warning } : { registered: true };
|
|
1863
2843
|
}
|
|
1864
|
-
function
|
|
2844
|
+
async function maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath) {
|
|
2845
|
+
const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
|
|
2846
|
+
if (explicitTarget && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
|
|
2847
|
+
return {
|
|
2848
|
+
registered: false,
|
|
2849
|
+
warning: "Codex plugin marketplace entry was not registered because a custom target was used. Set EMBED_CODEX_MARKETPLACE_REGISTER=1 to register it anyway."
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
if (resolve(targetRoot) !== resolve(defaultCodexPluginRoot()) && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
|
|
2853
|
+
return {
|
|
2854
|
+
registered: false,
|
|
2855
|
+
warning: "Codex plugin marketplace entry was not registered because the install target is not the default Codex plugin root."
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
const marketplacePath = defaultCodexLocalMarketplaceRoot();
|
|
2859
|
+
const marketplacePluginPath = join(marketplacePath, "plugins", CODEX_PLUGIN_NAME);
|
|
2860
|
+
try {
|
|
2861
|
+
if (!await pathExists(join(targetPath, ".codex-plugin", "plugin.json"))) {
|
|
2862
|
+
return {
|
|
2863
|
+
registered: false,
|
|
2864
|
+
warning: `Codex plugin manifest was not found at ${join(targetPath, ".codex-plugin", "plugin.json")}.`
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
await rm(marketplacePluginPath, { recursive: true, force: true });
|
|
2868
|
+
await mkdir(dirname(marketplacePluginPath), { recursive: true });
|
|
2869
|
+
await cp(targetPath, marketplacePluginPath, { recursive: true });
|
|
2870
|
+
await refreshCodexPluginCache(targetPath);
|
|
2871
|
+
await writeCodexLocalMarketplaceManifest(marketplacePath);
|
|
2872
|
+
const warning = await upsertCodexPluginMarketplaceConfig(marketplacePath);
|
|
2873
|
+
return warning ? { registered: true, marketplacePath, warning } : { registered: true, marketplacePath };
|
|
2874
|
+
}
|
|
2875
|
+
catch (error) {
|
|
2876
|
+
return {
|
|
2877
|
+
registered: false,
|
|
2878
|
+
marketplacePath,
|
|
2879
|
+
warning: `Could not register Codex plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
function defaultCodexLocalMarketplaceRoot() {
|
|
2884
|
+
return join(defaultCodexHome(), "local-marketplaces", CODEX_PLUGIN_NAME);
|
|
2885
|
+
}
|
|
2886
|
+
async function refreshCodexPluginCache(targetPath) {
|
|
2887
|
+
const manifestPath = join(targetPath, ".codex-plugin", "plugin.json");
|
|
2888
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
2889
|
+
const version = typeof manifest.version === "string" && manifest.version.trim()
|
|
2890
|
+
? manifest.version.trim()
|
|
2891
|
+
: "local";
|
|
2892
|
+
const cachePluginRoot = join(defaultCodexPluginRoot(), "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME);
|
|
2893
|
+
const cacheVersionPath = join(cachePluginRoot, version);
|
|
2894
|
+
await rm(cachePluginRoot, { recursive: true, force: true });
|
|
2895
|
+
await mkdir(dirname(cacheVersionPath), { recursive: true });
|
|
2896
|
+
await cp(targetPath, cacheVersionPath, { recursive: true });
|
|
2897
|
+
}
|
|
2898
|
+
async function writeCodexLocalMarketplaceManifest(marketplacePath) {
|
|
2899
|
+
const manifestPath = join(marketplacePath, ".agents", "plugins", "marketplace.json");
|
|
2900
|
+
const manifest = {
|
|
2901
|
+
name: CODEX_MARKETPLACE_NAME,
|
|
2902
|
+
interface: {
|
|
2903
|
+
displayName: "Embed Labs"
|
|
2904
|
+
},
|
|
2905
|
+
plugins: [
|
|
2906
|
+
{
|
|
2907
|
+
name: CODEX_PLUGIN_NAME,
|
|
2908
|
+
source: {
|
|
2909
|
+
source: "local",
|
|
2910
|
+
path: `./plugins/${CODEX_PLUGIN_NAME}`
|
|
2911
|
+
},
|
|
2912
|
+
policy: {
|
|
2913
|
+
installation: "AVAILABLE",
|
|
2914
|
+
authentication: "ON_USE"
|
|
2915
|
+
},
|
|
2916
|
+
category: "Developer Tools"
|
|
2917
|
+
}
|
|
2918
|
+
]
|
|
2919
|
+
};
|
|
2920
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
2921
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
2922
|
+
}
|
|
2923
|
+
async function upsertCodexPluginMarketplaceConfig(marketplacePath) {
|
|
2924
|
+
const configPath = codexConfigPath();
|
|
2925
|
+
try {
|
|
2926
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
2927
|
+
let text = "";
|
|
2928
|
+
try {
|
|
2929
|
+
text = await readFile(configPath, "utf8");
|
|
2930
|
+
}
|
|
2931
|
+
catch {
|
|
2932
|
+
text = "";
|
|
2933
|
+
}
|
|
2934
|
+
const original = text;
|
|
2935
|
+
const cleaned = removeLegacyCodexConfigTables(text).text;
|
|
2936
|
+
let updated = upsertTomlTableKeys(cleaned, `marketplaces.${CODEX_MARKETPLACE_NAME}`, {
|
|
2937
|
+
source_type: tomlString("local"),
|
|
2938
|
+
source: tomlString(marketplacePath),
|
|
2939
|
+
last_updated: tomlString(new Date().toISOString().replace(/\.\d{3}Z$/, "Z"))
|
|
2940
|
+
});
|
|
2941
|
+
updated = upsertTomlTableKeys(updated, `plugins."${CODEX_PLUGIN_NAME}@${CODEX_MARKETPLACE_NAME}"`, {
|
|
2942
|
+
enabled: "true"
|
|
2943
|
+
});
|
|
2944
|
+
if (updated !== original) {
|
|
2945
|
+
await writeFile(configPath, updated, "utf8");
|
|
2946
|
+
}
|
|
2947
|
+
return undefined;
|
|
2948
|
+
}
|
|
2949
|
+
catch (error) {
|
|
2950
|
+
return `${configPath} could not be updated with the Embed Labs plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
async function resolveEmbedCliMcpLauncher(embedCliBin) {
|
|
2954
|
+
const explicit = embedCliBin?.trim();
|
|
2955
|
+
if (explicit) {
|
|
2956
|
+
return { command: explicit, args: ["mcp", "start"] };
|
|
2957
|
+
}
|
|
2958
|
+
const pathBin = await resolveExecutableOnPath("embedlabs") || await resolveExecutableOnPath("embed");
|
|
2959
|
+
if (pathBin) {
|
|
2960
|
+
return { command: pathBin, args: ["mcp", "start"] };
|
|
2961
|
+
}
|
|
2962
|
+
return { command: process.execPath, args: [join(CLI_MODULE_DIR, "index.js"), "mcp", "start"] };
|
|
2963
|
+
}
|
|
2964
|
+
function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embedCliBin, mcpCommand, mcpArgs) {
|
|
1865
2965
|
try {
|
|
1866
2966
|
const parsed = JSON.parse(stdout);
|
|
1867
2967
|
const transport = parsed.transport;
|
|
1868
|
-
if (transport?.type !== "stdio"
|
|
1869
|
-
return false;
|
|
1870
|
-
if (!transport.args?.includes(bridgePath))
|
|
2968
|
+
if (transport?.type !== "stdio")
|
|
1871
2969
|
return false;
|
|
2970
|
+
const args = transport.args || [];
|
|
1872
2971
|
const env = transport.env || {};
|
|
1873
2972
|
if (env.EMBED_CLOUD_API_URL !== cloudUrl)
|
|
1874
2973
|
return false;
|
|
@@ -1876,13 +2975,16 @@ function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embed
|
|
|
1876
2975
|
return false;
|
|
1877
2976
|
if (embedCliBin && env.EMBED_CLI_BIN !== embedCliBin)
|
|
1878
2977
|
return false;
|
|
1879
|
-
|
|
2978
|
+
const directBridge = transport.command === process.execPath && args.includes(bridgePath);
|
|
2979
|
+
const cliMcp = transport.command === mcpCommand
|
|
2980
|
+
&& mcpArgs.every((arg, index) => args[index] === arg);
|
|
2981
|
+
return directBridge || cliMcp;
|
|
1880
2982
|
}
|
|
1881
2983
|
catch {
|
|
1882
2984
|
return false;
|
|
1883
2985
|
}
|
|
1884
2986
|
}
|
|
1885
|
-
async function upsertCodexMcpRuntimeConfig(
|
|
2987
|
+
async function upsertCodexMcpRuntimeConfig(command, args) {
|
|
1886
2988
|
const configPath = join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "config.toml");
|
|
1887
2989
|
try {
|
|
1888
2990
|
await mkdir(dirname(configPath), { recursive: true });
|
|
@@ -1894,8 +2996,8 @@ async function upsertCodexMcpRuntimeConfig(bridgePath) {
|
|
|
1894
2996
|
text = "";
|
|
1895
2997
|
}
|
|
1896
2998
|
const updated = upsertTomlTableKeys(text, "mcp_servers.embed-labs", {
|
|
1897
|
-
command: tomlString(
|
|
1898
|
-
args: `[${tomlString(
|
|
2999
|
+
command: tomlString(command),
|
|
3000
|
+
args: `[${args.map(tomlString).join(", ")}]`,
|
|
1899
3001
|
startup_timeout_sec: "120"
|
|
1900
3002
|
});
|
|
1901
3003
|
if (updated !== text) {
|
|
@@ -1972,11 +3074,229 @@ async function resolveExecutableOnPath(name) {
|
|
|
1972
3074
|
}
|
|
1973
3075
|
return undefined;
|
|
1974
3076
|
}
|
|
3077
|
+
async function runBridgeStart(parsed) {
|
|
3078
|
+
const host = stringFlag(parsed, "host");
|
|
3079
|
+
const port = numberFlag(parsed, "port");
|
|
3080
|
+
const bridge = await resolveBridgeLauncher();
|
|
3081
|
+
const args = [...bridge.args];
|
|
3082
|
+
if (host) {
|
|
3083
|
+
args.push("--host", host);
|
|
3084
|
+
}
|
|
3085
|
+
if (port !== undefined) {
|
|
3086
|
+
args.push("--port", String(port));
|
|
3087
|
+
}
|
|
3088
|
+
const env = {
|
|
3089
|
+
...process.env,
|
|
3090
|
+
...(host ? { EMBED_BRIDGE_HOST: host } : {}),
|
|
3091
|
+
...(port !== undefined ? { EMBED_BRIDGE_PORT: String(port) } : {})
|
|
3092
|
+
};
|
|
3093
|
+
const child = spawn(bridge.command, args, {
|
|
3094
|
+
stdio: "inherit",
|
|
3095
|
+
env
|
|
3096
|
+
});
|
|
3097
|
+
const forwardSignal = (signal) => {
|
|
3098
|
+
if (!child.killed) {
|
|
3099
|
+
child.kill(signal);
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
process.once("SIGINT", forwardSignal);
|
|
3103
|
+
process.once("SIGTERM", forwardSignal);
|
|
3104
|
+
return await new Promise((resolveCode) => {
|
|
3105
|
+
child.on("error", (error) => {
|
|
3106
|
+
process.off("SIGINT", forwardSignal);
|
|
3107
|
+
process.off("SIGTERM", forwardSignal);
|
|
3108
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3109
|
+
resolveCode(1);
|
|
3110
|
+
});
|
|
3111
|
+
child.on("close", (code, signal) => {
|
|
3112
|
+
process.off("SIGINT", forwardSignal);
|
|
3113
|
+
process.off("SIGTERM", forwardSignal);
|
|
3114
|
+
if (signal === "SIGINT" || signal === "SIGTERM") {
|
|
3115
|
+
resolveCode(0);
|
|
3116
|
+
}
|
|
3117
|
+
else {
|
|
3118
|
+
resolveCode(code ?? 0);
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
async function runMcpStart(parsed) {
|
|
3124
|
+
const unknownFlag = firstUnknownFlag(parsed, ["bridge-path"]);
|
|
3125
|
+
const unexpected = parsed.command.slice(2);
|
|
3126
|
+
if (unknownFlag || unexpected.length > 0) {
|
|
3127
|
+
console.error(unknownFlag ? `Unknown flag --${unknownFlag}. ${MCP_START_USAGE}` : MCP_START_USAGE);
|
|
3128
|
+
return 2;
|
|
3129
|
+
}
|
|
3130
|
+
const bridge = await resolveMcpBridgeLauncher(stringFlag(parsed, "bridge-path"));
|
|
3131
|
+
if (!bridge) {
|
|
3132
|
+
console.error([
|
|
3133
|
+
"Embed Labs MCP bridge was not found.",
|
|
3134
|
+
"Run npm run build in the source checkout, reinstall the embedlabs npm package, or set EMBED_MCP_BRIDGE_PATH to the bridge script.",
|
|
3135
|
+
"Expected command shape for MCP clients: embedlabs mcp start"
|
|
3136
|
+
].join("\n"));
|
|
3137
|
+
return 1;
|
|
3138
|
+
}
|
|
3139
|
+
const child = spawn(bridge.command, bridge.args, {
|
|
3140
|
+
stdio: "inherit",
|
|
3141
|
+
env: {
|
|
3142
|
+
...process.env,
|
|
3143
|
+
EMBED_CLIENT_KIND: process.env.EMBED_CLIENT_KIND || "mcp_client",
|
|
3144
|
+
EMBED_MCP_BRIDGE_PATH: bridge.bridgePath
|
|
3145
|
+
}
|
|
3146
|
+
});
|
|
3147
|
+
const forwardSignal = (signal) => {
|
|
3148
|
+
if (!child.killed) {
|
|
3149
|
+
child.kill(signal);
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
3152
|
+
process.once("SIGINT", forwardSignal);
|
|
3153
|
+
process.once("SIGTERM", forwardSignal);
|
|
3154
|
+
return await new Promise((resolveCode) => {
|
|
3155
|
+
child.on("error", (error) => {
|
|
3156
|
+
process.off("SIGINT", forwardSignal);
|
|
3157
|
+
process.off("SIGTERM", forwardSignal);
|
|
3158
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3159
|
+
resolveCode(1);
|
|
3160
|
+
});
|
|
3161
|
+
child.on("close", (code, signal) => {
|
|
3162
|
+
process.off("SIGINT", forwardSignal);
|
|
3163
|
+
process.off("SIGTERM", forwardSignal);
|
|
3164
|
+
if (signal === "SIGINT" || signal === "SIGTERM") {
|
|
3165
|
+
resolveCode(0);
|
|
3166
|
+
}
|
|
3167
|
+
else {
|
|
3168
|
+
resolveCode(code ?? 0);
|
|
3169
|
+
}
|
|
3170
|
+
});
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
async function resolveMcpBridgeLauncher(overridePath) {
|
|
3174
|
+
const explicitPath = overridePath?.trim() || process.env.EMBED_MCP_BRIDGE_PATH?.trim();
|
|
3175
|
+
const candidates = [
|
|
3176
|
+
explicitPath ? resolve(explicitPath) : "",
|
|
3177
|
+
join(CLI_MODULE_DIR, "embed-labs-mcp-bridge.mjs"),
|
|
3178
|
+
sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", "scripts", "embed-labs-mcp-bridge.mjs"),
|
|
3179
|
+
join(defaultCodexPluginRoot(), CODEX_PLUGIN_NAME, "scripts", "embed-labs-mcp-bridge.mjs")
|
|
3180
|
+
].filter(Boolean);
|
|
3181
|
+
for (const candidate of candidates) {
|
|
3182
|
+
try {
|
|
3183
|
+
await access(candidate, constants.R_OK);
|
|
3184
|
+
return { command: process.execPath, args: [candidate], bridgePath: candidate };
|
|
3185
|
+
}
|
|
3186
|
+
catch {
|
|
3187
|
+
// Keep looking.
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
return undefined;
|
|
3191
|
+
}
|
|
3192
|
+
async function resolveBridgeLauncher() {
|
|
3193
|
+
const explicitBinary = process.env.EMBED_LOCAL_BRIDGE_BINARY?.trim();
|
|
3194
|
+
if (explicitBinary) {
|
|
3195
|
+
try {
|
|
3196
|
+
await access(explicitBinary, constants.X_OK);
|
|
3197
|
+
return { command: explicitBinary, args: [] };
|
|
3198
|
+
}
|
|
3199
|
+
catch {
|
|
3200
|
+
// Fall through so the package launcher can print its clearer repair message.
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
const pathBinary = await resolveExecutableOnPath(process.platform === "win32" ? "embed-local-bridge.cmd" : "embed-local-bridge");
|
|
3204
|
+
if (pathBinary) {
|
|
3205
|
+
return { command: pathBinary, args: [] };
|
|
3206
|
+
}
|
|
3207
|
+
const packageLauncher = await resolveBridgePackageLauncher();
|
|
3208
|
+
if (packageLauncher) {
|
|
3209
|
+
return { command: process.execPath, args: [packageLauncher] };
|
|
3210
|
+
}
|
|
3211
|
+
return {
|
|
3212
|
+
command: process.execPath,
|
|
3213
|
+
args: [resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js")]
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
async function resolveBridgePackageLauncher() {
|
|
3217
|
+
const candidates = [];
|
|
3218
|
+
try {
|
|
3219
|
+
const packageJson = require.resolve("@embed-labs/local-bridge/package.json");
|
|
3220
|
+
candidates.push(join(dirname(packageJson), "dist", "index.js"));
|
|
3221
|
+
}
|
|
3222
|
+
catch {
|
|
3223
|
+
// Source checkout fallback below.
|
|
3224
|
+
}
|
|
3225
|
+
candidates.push(resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js"));
|
|
3226
|
+
for (const candidate of candidates) {
|
|
3227
|
+
try {
|
|
3228
|
+
await access(candidate, constants.R_OK);
|
|
3229
|
+
return candidate;
|
|
3230
|
+
}
|
|
3231
|
+
catch {
|
|
3232
|
+
// Keep looking.
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
return undefined;
|
|
3236
|
+
}
|
|
1975
3237
|
function defaultOpenCodeRoot() {
|
|
1976
|
-
return
|
|
3238
|
+
return globalOpenCodeRoot();
|
|
3239
|
+
}
|
|
3240
|
+
function globalOpenCodeRoot() {
|
|
3241
|
+
return join(homedir(), ".config", "opencode");
|
|
3242
|
+
}
|
|
3243
|
+
function isGlobalOpenCodeRoot(targetRoot) {
|
|
3244
|
+
return resolve(targetRoot) === resolve(globalOpenCodeRoot());
|
|
3245
|
+
}
|
|
3246
|
+
async function ensureOpenCodeGlobalPluginConfig() {
|
|
3247
|
+
const configPath = join(globalOpenCodeRoot(), "opencode.json");
|
|
3248
|
+
let existing = {};
|
|
3249
|
+
try {
|
|
3250
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
3251
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3252
|
+
existing = parsed;
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
catch {
|
|
3256
|
+
existing = {};
|
|
3257
|
+
}
|
|
3258
|
+
const configured = Array.isArray(existing.plugin)
|
|
3259
|
+
? existing.plugin.filter((item) => typeof item === "string")
|
|
3260
|
+
: Array.isArray(existing.plugins)
|
|
3261
|
+
? existing.plugins.filter((item) => typeof item === "string")
|
|
3262
|
+
: [];
|
|
3263
|
+
const removed = configured.filter(isLegacyOpenCodePluginConfigEntry);
|
|
3264
|
+
const cleaned = configured.filter((item) => !isLegacyOpenCodePluginConfigEntry(item));
|
|
3265
|
+
if (!cleaned.includes("embed-labs")) {
|
|
3266
|
+
cleaned.push("embed-labs");
|
|
3267
|
+
}
|
|
3268
|
+
await writeFile(configPath, `${JSON.stringify({
|
|
3269
|
+
...existing,
|
|
3270
|
+
plugin: cleaned,
|
|
3271
|
+
plugins: undefined
|
|
3272
|
+
}, null, 2)}\n`, "utf8");
|
|
3273
|
+
return removed;
|
|
3274
|
+
}
|
|
3275
|
+
function isLegacyOpenCodePluginConfigEntry(item) {
|
|
3276
|
+
const normalized = item.trim().replace(/\\/g, "/");
|
|
3277
|
+
const pathOnly = normalized.split(/[?#]/, 1)[0] || normalized;
|
|
3278
|
+
return normalized === "dbt-agent"
|
|
3279
|
+
|| normalized === "Dbt Agent"
|
|
3280
|
+
|| normalized === "deve"
|
|
3281
|
+
|| normalized === "development-board-toolchain"
|
|
3282
|
+
|| normalized === "development-board-toolchain-dev"
|
|
3283
|
+
|| normalized === "./plugins/deve"
|
|
3284
|
+
|| normalized === "./plugins/deve.js"
|
|
3285
|
+
|| /(?:^|\/)plugins\/deve(?:\.js)?$/.test(pathOnly)
|
|
3286
|
+
|| /(?:^|\/)plugins\/dbt-agent(?:\.js)?$/.test(pathOnly)
|
|
3287
|
+
|| normalized === "./plugins/development-board-toolchain"
|
|
3288
|
+
|| normalized === "./plugins/development-board-toolchain.js"
|
|
3289
|
+
|| normalized === "./plugins/development-board-toolchain-dev"
|
|
3290
|
+
|| normalized === "./plugins/development-board-toolchain-dev.js"
|
|
3291
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain")
|
|
3292
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain.js")
|
|
3293
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain-dev")
|
|
3294
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain-dev.js")
|
|
3295
|
+
|| normalized.includes("dbt-agent")
|
|
3296
|
+
|| normalized.includes("development-board-toolchain");
|
|
1977
3297
|
}
|
|
1978
3298
|
async function openCodeDuplicatePluginWarning(targetRoot) {
|
|
1979
|
-
const globalRoot =
|
|
3299
|
+
const globalRoot = globalOpenCodeRoot();
|
|
1980
3300
|
if (resolve(targetRoot) === resolve(globalRoot))
|
|
1981
3301
|
return undefined;
|
|
1982
3302
|
const configPath = join(globalRoot, "opencode.json");
|
|
@@ -2008,6 +3328,21 @@ async function localPluginVersion(kind) {
|
|
|
2008
3328
|
return undefined;
|
|
2009
3329
|
}
|
|
2010
3330
|
}
|
|
3331
|
+
async function installedCodexPluginVersion(pluginPath) {
|
|
3332
|
+
return await readPackageVersion(join(pluginPath, ".codex-plugin", "plugin.json"));
|
|
3333
|
+
}
|
|
3334
|
+
async function installedOpenCodePluginVersion(targetRoot) {
|
|
3335
|
+
return await readPackageVersion(join(targetRoot, "node_modules", "embed-labs", "package.json"));
|
|
3336
|
+
}
|
|
3337
|
+
async function readPackageVersion(filePath) {
|
|
3338
|
+
try {
|
|
3339
|
+
const parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
3340
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : undefined;
|
|
3341
|
+
}
|
|
3342
|
+
catch {
|
|
3343
|
+
return undefined;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
2011
3346
|
async function localPluginSourcesAvailable() {
|
|
2012
3347
|
return await pathExists(sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", ".codex-plugin", "plugin.json"))
|
|
2013
3348
|
&& await pathExists(sourceCheckoutPath("platform_plugins", "opencode_plugin", "package.json"));
|
|
@@ -2066,19 +3401,65 @@ async function parseErrorResponse(response) {
|
|
|
2066
3401
|
return undefined;
|
|
2067
3402
|
}
|
|
2068
3403
|
async function cloudAuthToken() {
|
|
3404
|
+
return (await cloudAuthConfig()).token;
|
|
3405
|
+
}
|
|
3406
|
+
async function cloudAuthConfig() {
|
|
2069
3407
|
const envToken = process.env.EMBED_API_TOKEN?.trim();
|
|
2070
3408
|
if (envToken) {
|
|
2071
|
-
|
|
3409
|
+
const fileConfig = await readLocalAuthFile();
|
|
3410
|
+
return {
|
|
3411
|
+
...fileConfig,
|
|
3412
|
+
token: envToken,
|
|
3413
|
+
profile: process.env.EMBED_AUTH_PROFILE ?? fileConfig.profile ?? "default",
|
|
3414
|
+
source: "env"
|
|
3415
|
+
};
|
|
2072
3416
|
}
|
|
3417
|
+
const fileConfig = await readLocalAuthFile();
|
|
3418
|
+
return {
|
|
3419
|
+
...fileConfig,
|
|
3420
|
+
token: fileConfig.token?.trim() || undefined,
|
|
3421
|
+
profile: fileConfig.profile ?? "default",
|
|
3422
|
+
source: fileConfig.token ? "file" : undefined
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
async function readLocalAuthFile() {
|
|
2073
3426
|
try {
|
|
2074
3427
|
const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
|
|
2075
|
-
|
|
2076
|
-
return fileToken || undefined;
|
|
3428
|
+
return normalizeLocalAuthFile(parsed);
|
|
2077
3429
|
}
|
|
2078
3430
|
catch {
|
|
2079
|
-
return
|
|
3431
|
+
return {};
|
|
2080
3432
|
}
|
|
2081
3433
|
}
|
|
3434
|
+
function normalizeLocalAuthFile(parsed) {
|
|
3435
|
+
const device = isJsonObject(parsed.device) ? parsed.device : undefined;
|
|
3436
|
+
const normalizedDevice = device && typeof device.device_id === "string" && typeof device.fingerprint_hash === "string" && typeof device.private_key_pem === "string"
|
|
3437
|
+
? {
|
|
3438
|
+
device_id: device.device_id,
|
|
3439
|
+
fingerprint_hash: device.fingerprint_hash,
|
|
3440
|
+
private_key_pem: device.private_key_pem,
|
|
3441
|
+
public_key_pem: typeof device.public_key_pem === "string" ? device.public_key_pem : undefined,
|
|
3442
|
+
label: typeof device.label === "string" ? device.label : undefined,
|
|
3443
|
+
platform: typeof device.platform === "string" ? device.platform : undefined,
|
|
3444
|
+
arch: typeof device.arch === "string" ? device.arch : undefined,
|
|
3445
|
+
hostname_hash: typeof device.hostname_hash === "string" ? device.hostname_hash : undefined,
|
|
3446
|
+
registered_at: typeof device.registered_at === "string" ? device.registered_at : undefined
|
|
3447
|
+
}
|
|
3448
|
+
: undefined;
|
|
3449
|
+
return {
|
|
3450
|
+
profile: typeof parsed.profile === "string" ? parsed.profile : undefined,
|
|
3451
|
+
token: typeof parsed.token === "string" ? parsed.token.trim() : undefined,
|
|
3452
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined,
|
|
3453
|
+
account_id: typeof parsed.account_id === "string" ? parsed.account_id : undefined,
|
|
3454
|
+
api_key_id: typeof parsed.api_key_id === "string" ? parsed.api_key_id : undefined,
|
|
3455
|
+
device: normalizedDevice
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
async function writeLocalAuthFile(config) {
|
|
3459
|
+
await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
|
|
3460
|
+
await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
3461
|
+
await chmod(DEFAULT_AUTH_FILE, 0o600).catch(() => undefined);
|
|
3462
|
+
}
|
|
2082
3463
|
function serviceBaseUrl(url) {
|
|
2083
3464
|
return url.replace(/\/+$/, "");
|
|
2084
3465
|
}
|
|
@@ -2101,9 +3482,16 @@ async function naturalLanguageQuery(parsed) {
|
|
|
2101
3482
|
}
|
|
2102
3483
|
const text = parsed.command.slice(1).join(" ").trim();
|
|
2103
3484
|
if (!text) {
|
|
2104
|
-
return { response: fail("invalid_args", QUERY_USAGE, { remediation: "Try: embed query \"
|
|
3485
|
+
return { response: fail("invalid_args", QUERY_USAGE, { remediation: "Try: embed query \"当前支持哪些开发板\"." }) };
|
|
2105
3486
|
}
|
|
2106
3487
|
const normalized = text.toLowerCase();
|
|
3488
|
+
if (isQuotaOrBillingIntent(normalized)) {
|
|
3489
|
+
return {
|
|
3490
|
+
response: fail("quota_not_supported", "当前 Embed Labs 服务器只提供 MCP 服务,不提供额度、充值、余额、账本或存储配额功能。", {
|
|
3491
|
+
remediation: "请使用 embedlabs auth status 查看登录和设备绑定状态,或在 MCP 客户端中直接调用开发板、工具链、知识库和本地硬件相关工具。"
|
|
3492
|
+
})
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
2107
3495
|
const accountId = await queryAccountId(parsed);
|
|
2108
3496
|
const needsAccount = queryNeedsAccount(normalized);
|
|
2109
3497
|
if (needsAccount && !accountId.ok) {
|
|
@@ -2122,7 +3510,7 @@ async function naturalLanguageQuery(parsed) {
|
|
|
2122
3510
|
if (!amountUsd) {
|
|
2123
3511
|
return {
|
|
2124
3512
|
response: fail("amount_required", "Recharge requests need an amount.", {
|
|
2125
|
-
remediation: "
|
|
3513
|
+
remediation: "当前 MCP 服务不提供充值流程。"
|
|
2126
3514
|
})
|
|
2127
3515
|
};
|
|
2128
3516
|
}
|
|
@@ -2215,13 +3603,8 @@ async function naturalLanguageQuery(parsed) {
|
|
|
2215
3603
|
response: fail("query_intent_unknown", "I could not map that request to a stable CLI action yet.", {
|
|
2216
3604
|
remediation: [
|
|
2217
3605
|
"Supported examples:",
|
|
2218
|
-
"embed query \"查一下我的额度\"",
|
|
2219
|
-
"embed query \"用 USDC 链上充值 100 美元\"",
|
|
2220
|
-
"embed query \"看一下 token 用量\"",
|
|
2221
|
-
"embed query \"看一下磁盘空间\"",
|
|
2222
3606
|
"embed query \"列出我的 API key\"",
|
|
2223
3607
|
"embed query \"有哪些开发板模板\"",
|
|
2224
|
-
"embed query \"当前可以用哪些模型\"",
|
|
2225
3608
|
"embed query \"本地有哪些工具能力\""
|
|
2226
3609
|
].join("\n")
|
|
2227
3610
|
})
|
|
@@ -2434,6 +3817,10 @@ function agentRunToolInputs(parsed) {
|
|
|
2434
3817
|
if (remotePath.error) {
|
|
2435
3818
|
return remotePath.error;
|
|
2436
3819
|
}
|
|
3820
|
+
const runCommand = optionalTrimmedStringFlag(parsed, "run-command");
|
|
3821
|
+
if (runCommand.error) {
|
|
3822
|
+
return runCommand.error;
|
|
3823
|
+
}
|
|
2437
3824
|
const user = optionalTrimmedStringFlag(parsed, "user");
|
|
2438
3825
|
if (user.error) {
|
|
2439
3826
|
return user.error;
|
|
@@ -2487,6 +3874,7 @@ function agentRunToolInputs(parsed) {
|
|
|
2487
3874
|
user: user.value,
|
|
2488
3875
|
artifact_path: artifact.value,
|
|
2489
3876
|
remote_path: remotePath.value,
|
|
3877
|
+
run_command: runCommand.value,
|
|
2490
3878
|
run: booleanFlag(parsed, "run") || undefined,
|
|
2491
3879
|
timeout_seconds: timeout.value
|
|
2492
3880
|
});
|
|
@@ -2567,13 +3955,15 @@ async function queryAccountId(parsed) {
|
|
|
2567
3955
|
return { ok: true, value: auth.data.account_id };
|
|
2568
3956
|
}
|
|
2569
3957
|
function queryNeedsAccount(normalized) {
|
|
3958
|
+
return isApiKeyIntent(normalized);
|
|
3959
|
+
}
|
|
3960
|
+
function isQuotaOrBillingIntent(normalized) {
|
|
2570
3961
|
return isRechargeIntent(normalized)
|
|
2571
3962
|
|| isBalanceIntent(normalized)
|
|
2572
3963
|
|| isUsageIntent(normalized)
|
|
2573
3964
|
|| isBillingStatementIntent(normalized)
|
|
2574
3965
|
|| isStorageSettlementIntent(normalized)
|
|
2575
|
-
|| isStorageIntent(normalized)
|
|
2576
|
-
|| isApiKeyIntent(normalized);
|
|
3966
|
+
|| isStorageIntent(normalized);
|
|
2577
3967
|
}
|
|
2578
3968
|
function isRechargeIntent(normalized) {
|
|
2579
3969
|
return /(充值|充钱|续费|购买额度|买额度|top\s*up|recharge|add credits|buy credits)/i.test(normalized);
|
|
@@ -3422,6 +4812,47 @@ function boardKnowledgeFileRequest(parsed) {
|
|
|
3422
4812
|
outputPath: outputPath.value
|
|
3423
4813
|
};
|
|
3424
4814
|
}
|
|
4815
|
+
function boardKnowledgeSearchRequest(parsed) {
|
|
4816
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "query", "source", "limit"]);
|
|
4817
|
+
if (unknownFlag) {
|
|
4818
|
+
return `Unknown flag --${unknownFlag}. ${BOARD_KNOWLEDGE_SEARCH_USAGE}`;
|
|
4819
|
+
}
|
|
4820
|
+
const templateId = parsed.command[3]?.trim();
|
|
4821
|
+
if (!templateId) {
|
|
4822
|
+
return BOARD_KNOWLEDGE_SEARCH_USAGE;
|
|
4823
|
+
}
|
|
4824
|
+
const extra = parsed.command.slice(4);
|
|
4825
|
+
if (extra.length > 0) {
|
|
4826
|
+
return `Unexpected argument: ${extra[0]}. ${BOARD_KNOWLEDGE_SEARCH_USAGE}`;
|
|
4827
|
+
}
|
|
4828
|
+
const query = optionalTrimmedStringFlag(parsed, "query");
|
|
4829
|
+
if (query.error) {
|
|
4830
|
+
return query.error;
|
|
4831
|
+
}
|
|
4832
|
+
if (!query.value) {
|
|
4833
|
+
return BOARD_KNOWLEDGE_SEARCH_USAGE;
|
|
4834
|
+
}
|
|
4835
|
+
const source = optionalTrimmedStringFlag(parsed, "source");
|
|
4836
|
+
if (source.error) {
|
|
4837
|
+
return source.error;
|
|
4838
|
+
}
|
|
4839
|
+
if (source.value && !["board_pack", "build_template", "registry"].includes(source.value)) {
|
|
4840
|
+
return BOARD_KNOWLEDGE_SEARCH_USAGE;
|
|
4841
|
+
}
|
|
4842
|
+
const limit = optionalPositiveIntegerFlag(parsed, "limit");
|
|
4843
|
+
if (limit.error) {
|
|
4844
|
+
return limit.error;
|
|
4845
|
+
}
|
|
4846
|
+
if (limit.value !== undefined && limit.value > 10) {
|
|
4847
|
+
return "--limit must be between 1 and 10.";
|
|
4848
|
+
}
|
|
4849
|
+
return {
|
|
4850
|
+
templateId,
|
|
4851
|
+
query: query.value,
|
|
4852
|
+
source: source.value,
|
|
4853
|
+
limit: limit.value ?? 5
|
|
4854
|
+
};
|
|
4855
|
+
}
|
|
3425
4856
|
function toolCallRequest(parsed) {
|
|
3426
4857
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "input-json", "approve"]);
|
|
3427
4858
|
if (unknownFlag) {
|
|
@@ -3469,7 +4900,7 @@ function isTaishanPiDeployCommand(parsed) {
|
|
|
3469
4900
|
|| area === "deploy";
|
|
3470
4901
|
}
|
|
3471
4902
|
function boardDeployTaishanPiRequest(parsed) {
|
|
3472
|
-
const unknownFlag = firstUnknownFlag(parsed, ["json", "host", "user", "artifact", "artifact-path", "remote-path", "run", "timeout", "approve", "approved"]);
|
|
4903
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "host", "user", "artifact", "artifact-path", "remote-path", "run-command", "run", "timeout", "approve", "approved"]);
|
|
3473
4904
|
if (unknownFlag) {
|
|
3474
4905
|
return `Unknown flag --${unknownFlag}. ${BOARD_DEPLOY_TAISHANPI_USAGE}`;
|
|
3475
4906
|
}
|
|
@@ -3511,6 +4942,10 @@ function boardDeployTaishanPiRequest(parsed) {
|
|
|
3511
4942
|
if (remotePath.error) {
|
|
3512
4943
|
return remotePath.error;
|
|
3513
4944
|
}
|
|
4945
|
+
const runCommand = optionalTrimmedStringFlag(parsed, "run-command");
|
|
4946
|
+
if (runCommand.error) {
|
|
4947
|
+
return runCommand.error;
|
|
4948
|
+
}
|
|
3514
4949
|
const timeout = optionalNonNegativeIntegerFlag(parsed, "timeout");
|
|
3515
4950
|
if (timeout.error) {
|
|
3516
4951
|
return timeout.error;
|
|
@@ -3525,6 +4960,7 @@ function boardDeployTaishanPiRequest(parsed) {
|
|
|
3525
4960
|
user: user.value,
|
|
3526
4961
|
artifact_path: artifact.value,
|
|
3527
4962
|
remote_path: remotePath.value,
|
|
4963
|
+
run_command: runCommand.value,
|
|
3528
4964
|
run: booleanFlag(parsed, "run") || undefined,
|
|
3529
4965
|
timeout_seconds: timeout.value,
|
|
3530
4966
|
approved: approval.value || undefined
|
|
@@ -3537,30 +4973,272 @@ async function authLogin(parsed) {
|
|
|
3537
4973
|
return fail("invalid_args", "Usage: embed auth login --token <token> [--profile default] [--json]");
|
|
3538
4974
|
}
|
|
3539
4975
|
const updatedAt = new Date().toISOString();
|
|
3540
|
-
|
|
3541
|
-
await
|
|
3542
|
-
|
|
4976
|
+
const current = await readLocalAuthFile();
|
|
4977
|
+
const localDevice = await buildLocalDeviceAuth(parsed, current.device);
|
|
4978
|
+
const registration = await registerLocalDevice(token.trim(), localDevice.registration);
|
|
4979
|
+
if (!registration.ok) {
|
|
4980
|
+
return fail(registration.error.code, registration.error.message, {
|
|
4981
|
+
remediation: registration.error.remediation,
|
|
4982
|
+
details: registration.error.details
|
|
4983
|
+
});
|
|
4984
|
+
}
|
|
4985
|
+
const device = {
|
|
4986
|
+
device_id: registration.data.device.device_id,
|
|
4987
|
+
fingerprint_hash: localDevice.device.fingerprint_hash,
|
|
4988
|
+
private_key_pem: localDevice.device.private_key_pem,
|
|
4989
|
+
public_key_pem: localDevice.device.public_key_pem,
|
|
4990
|
+
label: registration.data.device.label ?? localDevice.device.label,
|
|
4991
|
+
platform: registration.data.device.platform ?? localDevice.device.platform,
|
|
4992
|
+
arch: registration.data.device.arch ?? localDevice.device.arch,
|
|
4993
|
+
hostname_hash: registration.data.device.hostname_hash ?? localDevice.device.hostname_hash,
|
|
4994
|
+
registered_at: registration.data.device.first_seen_at
|
|
4995
|
+
};
|
|
4996
|
+
await writeLocalAuthFile({
|
|
4997
|
+
profile,
|
|
4998
|
+
token: token.trim(),
|
|
4999
|
+
updated_at: updatedAt,
|
|
5000
|
+
account_id: registration.data.device.account_id,
|
|
5001
|
+
api_key_id: registration.data.device.api_key_id,
|
|
5002
|
+
device
|
|
5003
|
+
});
|
|
5004
|
+
return ok({
|
|
5005
|
+
authenticated: true,
|
|
5006
|
+
profile,
|
|
5007
|
+
source: "file",
|
|
5008
|
+
updated_at: updatedAt,
|
|
5009
|
+
account_id: registration.data.device.account_id,
|
|
5010
|
+
api_key_id: registration.data.device.api_key_id,
|
|
5011
|
+
device_id: device.device_id,
|
|
5012
|
+
device_fingerprint_hash: device.fingerprint_hash,
|
|
5013
|
+
device_label: device.label,
|
|
5014
|
+
device_registered_at: device.registered_at,
|
|
5015
|
+
device_private_key_configured: true
|
|
5016
|
+
});
|
|
3543
5017
|
}
|
|
3544
5018
|
async function authStatus() {
|
|
3545
|
-
|
|
5019
|
+
const envToken = process.env.EMBED_API_TOKEN?.trim();
|
|
5020
|
+
const file = await readLocalAuthFile();
|
|
5021
|
+
const deviceIntegrity = file.device
|
|
5022
|
+
? (await validateLocalDeviceIntegrity(file.device)).ok ? "ok" : "failed"
|
|
5023
|
+
: "unbound";
|
|
5024
|
+
if (envToken) {
|
|
3546
5025
|
return {
|
|
3547
5026
|
authenticated: true,
|
|
3548
5027
|
profile: process.env.EMBED_AUTH_PROFILE ?? "default",
|
|
3549
|
-
source: "env"
|
|
5028
|
+
source: "env",
|
|
5029
|
+
account_id: file.account_id,
|
|
5030
|
+
api_key_id: file.api_key_id,
|
|
5031
|
+
device_id: file.device?.device_id,
|
|
5032
|
+
device_fingerprint_hash: file.device?.fingerprint_hash,
|
|
5033
|
+
device_label: file.device?.label,
|
|
5034
|
+
device_registered_at: file.device?.registered_at,
|
|
5035
|
+
device_private_key_configured: Boolean(file.device?.private_key_pem),
|
|
5036
|
+
device_integrity: deviceIntegrity
|
|
3550
5037
|
};
|
|
3551
5038
|
}
|
|
5039
|
+
return {
|
|
5040
|
+
authenticated: Boolean(file.token?.trim()),
|
|
5041
|
+
profile: file.profile ?? "default",
|
|
5042
|
+
source: file.token ? "file" : undefined,
|
|
5043
|
+
updated_at: file.updated_at,
|
|
5044
|
+
account_id: file.account_id,
|
|
5045
|
+
api_key_id: file.api_key_id,
|
|
5046
|
+
device_id: file.device?.device_id,
|
|
5047
|
+
device_fingerprint_hash: file.device?.fingerprint_hash,
|
|
5048
|
+
device_label: file.device?.label,
|
|
5049
|
+
device_registered_at: file.device?.registered_at,
|
|
5050
|
+
device_private_key_configured: Boolean(file.device?.private_key_pem),
|
|
5051
|
+
device_integrity: deviceIntegrity
|
|
5052
|
+
};
|
|
5053
|
+
}
|
|
5054
|
+
async function authDeviceStatus(parsed) {
|
|
5055
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
5056
|
+
const unexpected = parsed.command.slice(3);
|
|
5057
|
+
if (unknownFlag || unexpected.length > 0) {
|
|
5058
|
+
return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_STATUS_USAGE}` : AUTH_DEVICE_STATUS_USAGE);
|
|
5059
|
+
}
|
|
5060
|
+
const local = await authStatus();
|
|
5061
|
+
const remote = local.authenticated ? await cloudGet("/v1/me/devices") : undefined;
|
|
5062
|
+
if (remote && !remote.ok) {
|
|
5063
|
+
return ok({ local });
|
|
5064
|
+
}
|
|
5065
|
+
return ok({ local, remote: remote?.data });
|
|
5066
|
+
}
|
|
5067
|
+
async function authDeviceList(parsed) {
|
|
5068
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
5069
|
+
const unexpected = parsed.command.slice(3);
|
|
5070
|
+
if (unknownFlag || unexpected.length > 0) {
|
|
5071
|
+
return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_LIST_USAGE}` : AUTH_DEVICE_LIST_USAGE);
|
|
5072
|
+
}
|
|
5073
|
+
return await cloudGet("/v1/me/devices");
|
|
5074
|
+
}
|
|
5075
|
+
async function authDeviceRevoke(parsed) {
|
|
5076
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
5077
|
+
if (unknownFlag) {
|
|
5078
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_REVOKE_USAGE}`);
|
|
5079
|
+
}
|
|
5080
|
+
const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_REVOKE_USAGE);
|
|
5081
|
+
if (!id.ok) {
|
|
5082
|
+
return fail("invalid_args", id.error);
|
|
5083
|
+
}
|
|
5084
|
+
return await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}/revoke`, {});
|
|
5085
|
+
}
|
|
5086
|
+
async function authDeviceRename(parsed) {
|
|
5087
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "label"]);
|
|
5088
|
+
if (unknownFlag) {
|
|
5089
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_RENAME_USAGE}`);
|
|
5090
|
+
}
|
|
5091
|
+
const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_RENAME_USAGE);
|
|
5092
|
+
if (!id.ok) {
|
|
5093
|
+
return fail("invalid_args", id.error);
|
|
5094
|
+
}
|
|
5095
|
+
const label = stringFlag(parsed, "label");
|
|
5096
|
+
if (!label?.trim()) {
|
|
5097
|
+
return fail("invalid_args", AUTH_DEVICE_RENAME_USAGE);
|
|
5098
|
+
}
|
|
5099
|
+
const updated = await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}`, { label: label.trim() });
|
|
5100
|
+
if (updated.ok) {
|
|
5101
|
+
const auth = await readLocalAuthFile();
|
|
5102
|
+
if (auth.device?.device_id === updated.data.device_id) {
|
|
5103
|
+
await writeLocalAuthFile({ ...auth, device: { ...auth.device, label: updated.data.label } });
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
return updated;
|
|
5107
|
+
}
|
|
5108
|
+
async function buildLocalDeviceAuth(parsed, existing) {
|
|
5109
|
+
const fingerprint = await localHardwareFingerprint();
|
|
5110
|
+
const keyPair = existing?.fingerprint_hash === fingerprint.fingerprint_hash && existing.private_key_pem && existing.public_key_pem
|
|
5111
|
+
? { privateKeyPem: existing.private_key_pem, publicKeyPem: existing.public_key_pem }
|
|
5112
|
+
: generateLocalDeviceKeyPair();
|
|
5113
|
+
const label = stringFlag(parsed, "label")?.trim()
|
|
5114
|
+
|| existing?.label
|
|
5115
|
+
|| `${fingerprint.platform} ${fingerprint.arch}`;
|
|
5116
|
+
const device = {
|
|
5117
|
+
device_id: existing?.fingerprint_hash === fingerprint.fingerprint_hash ? existing.device_id : "",
|
|
5118
|
+
fingerprint_hash: fingerprint.fingerprint_hash,
|
|
5119
|
+
private_key_pem: keyPair.privateKeyPem,
|
|
5120
|
+
public_key_pem: keyPair.publicKeyPem,
|
|
5121
|
+
label,
|
|
5122
|
+
platform: fingerprint.platform,
|
|
5123
|
+
arch: fingerprint.arch,
|
|
5124
|
+
hostname_hash: fingerprint.hostname_hash,
|
|
5125
|
+
registered_at: existing?.registered_at
|
|
5126
|
+
};
|
|
5127
|
+
return {
|
|
5128
|
+
device,
|
|
5129
|
+
registration: {
|
|
5130
|
+
fingerprint_hash: fingerprint.fingerprint_hash,
|
|
5131
|
+
public_key: keyPair.publicKeyPem,
|
|
5132
|
+
label,
|
|
5133
|
+
platform: fingerprint.platform,
|
|
5134
|
+
arch: fingerprint.arch,
|
|
5135
|
+
hostname_hash: fingerprint.hostname_hash,
|
|
5136
|
+
client_name: EMBED_CLIENT_NAME,
|
|
5137
|
+
client_version: EMBED_CLIENT_VERSION,
|
|
5138
|
+
metadata: {
|
|
5139
|
+
fingerprint_version: "v1",
|
|
5140
|
+
fingerprint_source: fingerprint.source
|
|
5141
|
+
}
|
|
5142
|
+
}
|
|
5143
|
+
};
|
|
5144
|
+
}
|
|
5145
|
+
function generateLocalDeviceKeyPair() {
|
|
5146
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
5147
|
+
return {
|
|
5148
|
+
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
5149
|
+
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString()
|
|
5150
|
+
};
|
|
5151
|
+
}
|
|
5152
|
+
async function registerLocalDevice(token, body) {
|
|
3552
5153
|
try {
|
|
3553
|
-
const
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
source: "file",
|
|
3558
|
-
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined
|
|
5154
|
+
const bodyText = JSON.stringify(body);
|
|
5155
|
+
const headers = {
|
|
5156
|
+
"content-type": "application/json",
|
|
5157
|
+
authorization: `Bearer ${token}`
|
|
3559
5158
|
};
|
|
5159
|
+
addCloudRequestSignature(headers, "POST", "/v1/me/devices/register", bodyText, token);
|
|
5160
|
+
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/me/devices/register`, {
|
|
5161
|
+
method: "POST",
|
|
5162
|
+
headers,
|
|
5163
|
+
body: bodyText
|
|
5164
|
+
});
|
|
5165
|
+
const parsed = await response.json();
|
|
5166
|
+
return enrichCloudAuthFailure(parsed, true);
|
|
5167
|
+
}
|
|
5168
|
+
catch (error) {
|
|
5169
|
+
return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
|
|
5170
|
+
remediation: `Check that embed cloud-api is running at ${DEFAULT_CLOUD_API_URL}. Start it with: npm run cloud-api`
|
|
5171
|
+
});
|
|
5172
|
+
}
|
|
5173
|
+
}
|
|
5174
|
+
async function localHardwareFingerprint() {
|
|
5175
|
+
cachedLocalHardwareFingerprint ??= localHardwareFingerprintUncached();
|
|
5176
|
+
return await cachedLocalHardwareFingerprint;
|
|
5177
|
+
}
|
|
5178
|
+
async function localHardwareFingerprintUncached() {
|
|
5179
|
+
const platformName = platform();
|
|
5180
|
+
const archName = arch();
|
|
5181
|
+
const raw = await localHardwareId(platformName);
|
|
5182
|
+
const fingerprintHash = createHash("sha256")
|
|
5183
|
+
.update(`embed-labs:device:v1:${platformName}:${raw.value}`)
|
|
5184
|
+
.digest("hex");
|
|
5185
|
+
const hostnameHash = createHash("sha256")
|
|
5186
|
+
.update(`embed-labs:hostname:v1:${hostname()}`)
|
|
5187
|
+
.digest("hex");
|
|
5188
|
+
return {
|
|
5189
|
+
fingerprint_hash: fingerprintHash,
|
|
5190
|
+
platform: platformName,
|
|
5191
|
+
arch: archName,
|
|
5192
|
+
hostname_hash: hostnameHash,
|
|
5193
|
+
source: raw.source
|
|
5194
|
+
};
|
|
5195
|
+
}
|
|
5196
|
+
async function localHardwareId(platformName) {
|
|
5197
|
+
if (platformName === "darwin") {
|
|
5198
|
+
const result = await runLocalProcess("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
|
|
5199
|
+
const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(result.stdout);
|
|
5200
|
+
if (match?.[1]) {
|
|
5201
|
+
return { value: match[1], source: "macos_ioplatformuuid" };
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
if (platformName === "win32") {
|
|
5205
|
+
const result = await runLocalProcess("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
|
|
5206
|
+
const match = /MachineGuid\s+REG_\w+\s+([^\r\n]+)/.exec(result.stdout);
|
|
5207
|
+
if (match?.[1]?.trim()) {
|
|
5208
|
+
return { value: match[1].trim(), source: "windows_machineguid" };
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
if (platformName === "linux") {
|
|
5212
|
+
for (const pathValue of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
5213
|
+
try {
|
|
5214
|
+
const value = (await readFile(pathValue, "utf8")).trim();
|
|
5215
|
+
if (value) {
|
|
5216
|
+
return { value, source: `linux:${pathValue}` };
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
catch {
|
|
5220
|
+
// Try the next stable machine id location.
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
const generated = await localGeneratedInstallId();
|
|
5225
|
+
return { value: generated, source: "generated_install_id" };
|
|
5226
|
+
}
|
|
5227
|
+
async function localGeneratedInstallId() {
|
|
5228
|
+
try {
|
|
5229
|
+
const parsed = JSON.parse(await readFile(DEFAULT_DEVICE_FILE, "utf8"));
|
|
5230
|
+
if (typeof parsed.generated_install_id === "string" && parsed.generated_install_id.trim()) {
|
|
5231
|
+
return parsed.generated_install_id.trim();
|
|
5232
|
+
}
|
|
3560
5233
|
}
|
|
3561
5234
|
catch {
|
|
3562
|
-
|
|
5235
|
+
// Fall through and create a local-only fallback id.
|
|
3563
5236
|
}
|
|
5237
|
+
const generated = `install_${randomBytes(24).toString("base64url")}`;
|
|
5238
|
+
await mkdir(dirname(DEFAULT_DEVICE_FILE), { recursive: true });
|
|
5239
|
+
await writeFile(DEFAULT_DEVICE_FILE, `${JSON.stringify({ generated_install_id: generated, created_at: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
5240
|
+
await chmod(DEFAULT_DEVICE_FILE, 0o600).catch(() => undefined);
|
|
5241
|
+
return generated;
|
|
3564
5242
|
}
|
|
3565
5243
|
function accountCreateBody(parsed) {
|
|
3566
5244
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "email", "display-name"]);
|
|
@@ -3716,6 +5394,80 @@ function usageRecordBody(parsed) {
|
|
|
3716
5394
|
created_at: createdAtResult.value
|
|
3717
5395
|
});
|
|
3718
5396
|
}
|
|
5397
|
+
function mcpToolEventBody(parsed) {
|
|
5398
|
+
const unknownFlag = firstUnknownFlag(parsed, [
|
|
5399
|
+
"json",
|
|
5400
|
+
"account",
|
|
5401
|
+
"account-id",
|
|
5402
|
+
"tool",
|
|
5403
|
+
"client",
|
|
5404
|
+
"mode",
|
|
5405
|
+
"server-model-used",
|
|
5406
|
+
"success",
|
|
5407
|
+
"local-device-id",
|
|
5408
|
+
"local_device_id",
|
|
5409
|
+
"request-id",
|
|
5410
|
+
"duration-ms",
|
|
5411
|
+
"input-summary",
|
|
5412
|
+
"output-summary"
|
|
5413
|
+
]);
|
|
5414
|
+
if (unknownFlag) {
|
|
5415
|
+
return `Unknown flag --${unknownFlag}. ${MCP_TOOL_EVENT_USAGE}`;
|
|
5416
|
+
}
|
|
5417
|
+
const extra = parsed.command.slice(2);
|
|
5418
|
+
if (extra.length > 0) {
|
|
5419
|
+
return `Unexpected argument: ${extra[0]}. ${MCP_TOOL_EVENT_USAGE}`;
|
|
5420
|
+
}
|
|
5421
|
+
const toolResult = optionalTrimmedStringFlag(parsed, "tool");
|
|
5422
|
+
if (toolResult.error)
|
|
5423
|
+
return toolResult.error;
|
|
5424
|
+
if (!toolResult.value)
|
|
5425
|
+
return MCP_TOOL_EVENT_USAGE;
|
|
5426
|
+
const accountResult = optionalTrimmedStringAliasFlag(parsed, ["account", "account-id"], "account or account-id");
|
|
5427
|
+
if (accountResult.error)
|
|
5428
|
+
return accountResult.error;
|
|
5429
|
+
const clientResult = optionalTrimmedStringFlag(parsed, "client");
|
|
5430
|
+
if (clientResult.error)
|
|
5431
|
+
return clientResult.error;
|
|
5432
|
+
const modeResult = optionalTrimmedStringFlag(parsed, "mode");
|
|
5433
|
+
if (modeResult.error)
|
|
5434
|
+
return modeResult.error;
|
|
5435
|
+
const localDeviceResult = optionalTrimmedStringAliasFlag(parsed, ["local-device-id", "local_device_id"], "local-device-id");
|
|
5436
|
+
if (localDeviceResult.error)
|
|
5437
|
+
return localDeviceResult.error;
|
|
5438
|
+
const requestIdResult = optionalTrimmedStringFlag(parsed, "request-id");
|
|
5439
|
+
if (requestIdResult.error)
|
|
5440
|
+
return requestIdResult.error;
|
|
5441
|
+
const inputSummaryResult = optionalTrimmedStringFlag(parsed, "input-summary");
|
|
5442
|
+
if (inputSummaryResult.error)
|
|
5443
|
+
return inputSummaryResult.error;
|
|
5444
|
+
const outputSummaryResult = optionalTrimmedStringFlag(parsed, "output-summary");
|
|
5445
|
+
if (outputSummaryResult.error)
|
|
5446
|
+
return outputSummaryResult.error;
|
|
5447
|
+
const durationResult = optionalNonNegativeIntegerFlag(parsed, "duration-ms");
|
|
5448
|
+
if (durationResult.error)
|
|
5449
|
+
return durationResult.error;
|
|
5450
|
+
const serverModelUsed = optionalBooleanFlag(parsed, "server-model-used");
|
|
5451
|
+
if (typeof serverModelUsed === "string")
|
|
5452
|
+
return serverModelUsed;
|
|
5453
|
+
const success = optionalBooleanFlag(parsed, "success");
|
|
5454
|
+
if (typeof success === "string")
|
|
5455
|
+
return success;
|
|
5456
|
+
return compactBody({
|
|
5457
|
+
account_id: accountResult.value,
|
|
5458
|
+
tool_name: toolResult.value,
|
|
5459
|
+
client: clientResult.value,
|
|
5460
|
+
mode: modeResult.value,
|
|
5461
|
+
local_device_id: localDeviceResult.value,
|
|
5462
|
+
server_model_used: serverModelUsed,
|
|
5463
|
+
success,
|
|
5464
|
+
request_id: requestIdResult.value,
|
|
5465
|
+
duration_ms: durationResult.value,
|
|
5466
|
+
input_summary: inputSummaryResult.value,
|
|
5467
|
+
output_summary: outputSummaryResult.value,
|
|
5468
|
+
metadata: localDeviceResult.value ? { local_device_id: localDeviceResult.value } : undefined
|
|
5469
|
+
});
|
|
5470
|
+
}
|
|
3719
5471
|
function usageSummaryRequest(parsed) {
|
|
3720
5472
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "account", "account-id", "api-key-id", "from", "to"]);
|
|
3721
5473
|
if (unknownFlag) {
|
|
@@ -4051,6 +5803,29 @@ function billingSnapshotListRequest(parsed) {
|
|
|
4051
5803
|
}
|
|
4052
5804
|
return { path: `/v1/accounts/${encodeURIComponent(accountResult.value)}/billing/snapshots` };
|
|
4053
5805
|
}
|
|
5806
|
+
function localToolchainListRequest(parsed, usage = LOCAL_TOOLCHAIN_LIST_USAGE) {
|
|
5807
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "install-root"]);
|
|
5808
|
+
if (unknownFlag) {
|
|
5809
|
+
return `Unknown flag --${unknownFlag}. ${usage}`;
|
|
5810
|
+
}
|
|
5811
|
+
const extra = parsed.command.slice(3);
|
|
5812
|
+
if (extra.length > 0) {
|
|
5813
|
+
return `Unexpected argument: ${extra[0]}. ${usage}`;
|
|
5814
|
+
}
|
|
5815
|
+
const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
|
|
5816
|
+
if (board.error)
|
|
5817
|
+
return board.error;
|
|
5818
|
+
const channel = optionalTrimmedStringFlag(parsed, "channel");
|
|
5819
|
+
if (channel.error)
|
|
5820
|
+
return channel.error;
|
|
5821
|
+
const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
|
|
5822
|
+
if (metadataRoot.error)
|
|
5823
|
+
return metadataRoot.error;
|
|
5824
|
+
const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
|
|
5825
|
+
if (installRoot.error)
|
|
5826
|
+
return installRoot.error;
|
|
5827
|
+
return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value, installRoot: installRoot.value };
|
|
5828
|
+
}
|
|
4054
5829
|
function localToolchainLatestRequest(parsed) {
|
|
4055
5830
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root"]);
|
|
4056
5831
|
if (unknownFlag) {
|
|
@@ -4071,6 +5846,32 @@ function localToolchainLatestRequest(parsed) {
|
|
|
4071
5846
|
return metadataRoot.error;
|
|
4072
5847
|
return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value };
|
|
4073
5848
|
}
|
|
5849
|
+
function localWslInstallRequest(parsed) {
|
|
5850
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "distribution", "distro", "no-launch", "web-download", "timeout-ms"]);
|
|
5851
|
+
if (unknownFlag) {
|
|
5852
|
+
return `Unknown flag --${unknownFlag}. ${LOCAL_WSL_INSTALL_USAGE}`;
|
|
5853
|
+
}
|
|
5854
|
+
const extra = parsed.command.slice(3);
|
|
5855
|
+
if (extra.length > 0) {
|
|
5856
|
+
return `Unexpected argument: ${extra[0]}. ${LOCAL_WSL_INSTALL_USAGE}`;
|
|
5857
|
+
}
|
|
5858
|
+
const distribution = stringFlag(parsed, "distribution") ?? stringFlag(parsed, "distro");
|
|
5859
|
+
const noLaunch = optionalBooleanFlag(parsed, "no-launch");
|
|
5860
|
+
if (typeof noLaunch === "string")
|
|
5861
|
+
return noLaunch;
|
|
5862
|
+
const webDownload = optionalBooleanFlag(parsed, "web-download");
|
|
5863
|
+
if (typeof webDownload === "string")
|
|
5864
|
+
return webDownload;
|
|
5865
|
+
const timeoutMs = optionalIntegerFlag(parsed, "timeout-ms", 1_000, 3_600_000);
|
|
5866
|
+
if (timeoutMs.error)
|
|
5867
|
+
return timeoutMs.error;
|
|
5868
|
+
return {
|
|
5869
|
+
distribution,
|
|
5870
|
+
noLaunch,
|
|
5871
|
+
webDownload,
|
|
5872
|
+
timeoutMs: timeoutMs.value
|
|
5873
|
+
};
|
|
5874
|
+
}
|
|
4074
5875
|
function localToolchainCurrentRequest(parsed) {
|
|
4075
5876
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "install-root"]);
|
|
4076
5877
|
if (unknownFlag) {
|
|
@@ -4086,7 +5887,7 @@ function localToolchainCurrentRequest(parsed) {
|
|
|
4086
5887
|
return { installRoot: installRoot.value };
|
|
4087
5888
|
}
|
|
4088
5889
|
function localToolchainInstallRequest(parsed) {
|
|
4089
|
-
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "force"]);
|
|
5890
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "mode", "force"]);
|
|
4090
5891
|
if (unknownFlag) {
|
|
4091
5892
|
return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_INSTALL_USAGE}`;
|
|
4092
5893
|
}
|
|
@@ -4115,6 +5916,9 @@ function localToolchainInstallRequest(parsed) {
|
|
|
4115
5916
|
const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
|
|
4116
5917
|
if (installRoot.error)
|
|
4117
5918
|
return installRoot.error;
|
|
5919
|
+
const mode = optionalTrimmedStringFlag(parsed, "mode");
|
|
5920
|
+
if (mode.error)
|
|
5921
|
+
return mode.error;
|
|
4118
5922
|
return {
|
|
4119
5923
|
boardId: board.value,
|
|
4120
5924
|
channel: channel.value,
|
|
@@ -4122,11 +5926,32 @@ function localToolchainInstallRequest(parsed) {
|
|
|
4122
5926
|
sourceUrl: sourceUrl.value,
|
|
4123
5927
|
sourceReleaseRoot: sourceReleaseRoot.value,
|
|
4124
5928
|
installRoot: installRoot.value,
|
|
5929
|
+
mode: mode.value,
|
|
4125
5930
|
force: booleanFlag(parsed, "force")
|
|
4126
5931
|
};
|
|
4127
5932
|
}
|
|
5933
|
+
function localToolchainUninstallRequest(parsed) {
|
|
5934
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "install-root"]);
|
|
5935
|
+
if (unknownFlag) {
|
|
5936
|
+
return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_UNINSTALL_USAGE}`;
|
|
5937
|
+
}
|
|
5938
|
+
const extra = parsed.command.slice(3);
|
|
5939
|
+
if (extra.length > 0) {
|
|
5940
|
+
return `Unexpected argument: ${extra[0]}. ${LOCAL_TOOLCHAIN_UNINSTALL_USAGE}`;
|
|
5941
|
+
}
|
|
5942
|
+
const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
|
|
5943
|
+
if (board.error)
|
|
5944
|
+
return board.error;
|
|
5945
|
+
if (!board.value) {
|
|
5946
|
+
return LOCAL_TOOLCHAIN_UNINSTALL_USAGE;
|
|
5947
|
+
}
|
|
5948
|
+
const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
|
|
5949
|
+
if (installRoot.error)
|
|
5950
|
+
return installRoot.error;
|
|
5951
|
+
return { boardId: board.value, installRoot: installRoot.value };
|
|
5952
|
+
}
|
|
4128
5953
|
function localToolchainValidateRequest(parsed) {
|
|
4129
|
-
const unknownFlag = firstUnknownFlag(parsed, ["json", "release-root"]);
|
|
5954
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "release-root", "mode"]);
|
|
4130
5955
|
if (unknownFlag) {
|
|
4131
5956
|
return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_VALIDATE_USAGE}`;
|
|
4132
5957
|
}
|
|
@@ -4138,7 +5963,15 @@ function localToolchainValidateRequest(parsed) {
|
|
|
4138
5963
|
if (releaseRoot.error) {
|
|
4139
5964
|
return releaseRoot.error;
|
|
4140
5965
|
}
|
|
4141
|
-
|
|
5966
|
+
const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
|
|
5967
|
+
if (board.error) {
|
|
5968
|
+
return board.error;
|
|
5969
|
+
}
|
|
5970
|
+
const mode = optionalTrimmedStringFlag(parsed, "mode");
|
|
5971
|
+
if (mode.error) {
|
|
5972
|
+
return mode.error;
|
|
5973
|
+
}
|
|
5974
|
+
return { releaseRoot: releaseRoot.value, mode: mode.value, boardId: board.value };
|
|
4142
5975
|
}
|
|
4143
5976
|
function localCompileTaishanPiRequest(parsed, auth) {
|
|
4144
5977
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "source", "output", "release-root", "account", "account-id"]);
|
|
@@ -4223,7 +6056,9 @@ function localToolchainAuthContext(auth, accountId) {
|
|
|
4223
6056
|
authenticated: auth.authenticated,
|
|
4224
6057
|
profile: auth.profile,
|
|
4225
6058
|
source: auth.source,
|
|
4226
|
-
account_id: accountId
|
|
6059
|
+
account_id: accountId,
|
|
6060
|
+
api_key_id: auth.api_key_id,
|
|
6061
|
+
device_id: auth.device_id
|
|
4227
6062
|
};
|
|
4228
6063
|
}
|
|
4229
6064
|
function usageEventsRequest(parsed) {
|
|
@@ -4633,6 +6468,35 @@ function renderPluginList(result) {
|
|
|
4633
6468
|
`install="${plugin.install_command}"`
|
|
4634
6469
|
].filter(Boolean).join(" ")).join("\n");
|
|
4635
6470
|
}
|
|
6471
|
+
function renderPluginUpdateCheck(result) {
|
|
6472
|
+
const lines = [
|
|
6473
|
+
`release_url=${result.release_url}`,
|
|
6474
|
+
result.latest_version ? `latest_version=${result.latest_version}` : ""
|
|
6475
|
+
].filter(Boolean);
|
|
6476
|
+
if (result.release_notes.length > 0) {
|
|
6477
|
+
lines.push("release_notes:");
|
|
6478
|
+
for (const note of result.release_notes) {
|
|
6479
|
+
lines.push(` - ${note}`);
|
|
6480
|
+
}
|
|
6481
|
+
}
|
|
6482
|
+
for (const plugin of result.plugins) {
|
|
6483
|
+
lines.push("");
|
|
6484
|
+
lines.push(`${plugin.display_name} (${plugin.id})`);
|
|
6485
|
+
lines.push(` installed=${plugin.installed}`);
|
|
6486
|
+
lines.push(` installed_version=${plugin.installed_version ?? "none"}`);
|
|
6487
|
+
lines.push(` latest_version=${plugin.latest_version ?? "unknown"}`);
|
|
6488
|
+
lines.push(` update_available=${plugin.update_available}`);
|
|
6489
|
+
lines.push(` target=${plugin.target_path}`);
|
|
6490
|
+
lines.push(` update_command=${plugin.update_command}`);
|
|
6491
|
+
if (plugin.release_file) {
|
|
6492
|
+
lines.push(` release_file=${plugin.release_file}`);
|
|
6493
|
+
}
|
|
6494
|
+
for (const note of plugin.notes) {
|
|
6495
|
+
lines.push(` note=${note}`);
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
return lines.join("\n");
|
|
6499
|
+
}
|
|
4636
6500
|
function renderPluginInstall(result) {
|
|
4637
6501
|
const lines = ["Installed plugins:"];
|
|
4638
6502
|
for (const item of result.installed) {
|
|
@@ -4654,6 +6518,15 @@ function renderPluginInstall(result) {
|
|
|
4654
6518
|
if (item.mcp_warning) {
|
|
4655
6519
|
lines.push(` warning=${item.mcp_warning}`);
|
|
4656
6520
|
}
|
|
6521
|
+
if (item.marketplace_registered !== undefined) {
|
|
6522
|
+
lines.push(` codex_marketplace_registered=${item.marketplace_registered}`);
|
|
6523
|
+
}
|
|
6524
|
+
if (item.marketplace_path) {
|
|
6525
|
+
lines.push(` codex_marketplace=${item.marketplace_path}`);
|
|
6526
|
+
}
|
|
6527
|
+
if (item.marketplace_warning) {
|
|
6528
|
+
lines.push(` warning=${item.marketplace_warning}`);
|
|
6529
|
+
}
|
|
4657
6530
|
}
|
|
4658
6531
|
return lines.join("\n");
|
|
4659
6532
|
}
|
|
@@ -4685,9 +6558,53 @@ function renderAgentRunResult(result) {
|
|
|
4685
6558
|
return lines.join("\n");
|
|
4686
6559
|
}
|
|
4687
6560
|
function renderAuthStatus(status) {
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
6561
|
+
if (!status.authenticated) {
|
|
6562
|
+
return `Not authenticated profile=${status.profile}`;
|
|
6563
|
+
}
|
|
6564
|
+
return [
|
|
6565
|
+
`Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`,
|
|
6566
|
+
status.account_id ? `account=${status.account_id}` : "",
|
|
6567
|
+
status.api_key_id ? `api_key=${status.api_key_id}` : "",
|
|
6568
|
+
status.device_id ? `device=${status.device_id}` : "device=not_registered",
|
|
6569
|
+
status.device_label ? `device_label=${status.device_label}` : "",
|
|
6570
|
+
status.device_integrity ? `device_integrity=${status.device_integrity}` : "",
|
|
6571
|
+
status.device_private_key_configured === false ? "device_private_key=missing" : ""
|
|
6572
|
+
].filter(Boolean).join("\n");
|
|
6573
|
+
}
|
|
6574
|
+
function renderAuthDeviceStatus(status) {
|
|
6575
|
+
const lines = [renderAuthStatus(status.local)];
|
|
6576
|
+
if (status.remote) {
|
|
6577
|
+
const activeCount = status.remote.devices.filter((device) => device.status === "active").length;
|
|
6578
|
+
lines.push(`remote_devices=${activeCount}/${status.remote.device_limit}`);
|
|
6579
|
+
const localDevice = status.local.device_id
|
|
6580
|
+
? status.remote.devices.find((device) => device.device_id === status.local.device_id)
|
|
6581
|
+
: undefined;
|
|
6582
|
+
if (localDevice) {
|
|
6583
|
+
lines.push(`remote_current=${renderAuthDevice(localDevice)}`);
|
|
6584
|
+
}
|
|
6585
|
+
}
|
|
6586
|
+
return lines.join("\n");
|
|
6587
|
+
}
|
|
6588
|
+
function renderAuthDeviceList(result) {
|
|
6589
|
+
if (result.devices.length === 0) {
|
|
6590
|
+
return `No registered devices. device_limit=${result.device_limit}`;
|
|
6591
|
+
}
|
|
6592
|
+
return [
|
|
6593
|
+
`device_limit=${result.device_limit}`,
|
|
6594
|
+
...result.devices.map(renderAuthDevice)
|
|
6595
|
+
].join("\n");
|
|
6596
|
+
}
|
|
6597
|
+
function renderAuthDevice(device) {
|
|
6598
|
+
return [
|
|
6599
|
+
`${device.device_id} account=${device.account_id}`,
|
|
6600
|
+
device.api_key_id ? `api_key=${device.api_key_id}` : "",
|
|
6601
|
+
`status=${device.status}`,
|
|
6602
|
+
device.label ? `label=${device.label}` : "",
|
|
6603
|
+
device.platform ? `platform=${device.platform}` : "",
|
|
6604
|
+
device.arch ? `arch=${device.arch}` : "",
|
|
6605
|
+
`last_seen_at=${device.last_seen_at}`,
|
|
6606
|
+
device.revoked_at ? `revoked_at=${device.revoked_at}` : ""
|
|
6607
|
+
].filter(Boolean).join(" ");
|
|
4691
6608
|
}
|
|
4692
6609
|
function renderAccount(account) {
|
|
4693
6610
|
return [
|
|
@@ -4737,6 +6654,20 @@ function renderUsageRecord(record) {
|
|
|
4737
6654
|
`created_at=${record.created_at}`
|
|
4738
6655
|
].filter(Boolean).join(" ");
|
|
4739
6656
|
}
|
|
6657
|
+
function renderMcpToolEvent(event) {
|
|
6658
|
+
return [
|
|
6659
|
+
`${event.event_id} tool=${event.tool_name}`,
|
|
6660
|
+
event.account_id ? `account=${event.account_id}` : "",
|
|
6661
|
+
event.api_key_id ? `api_key=${event.api_key_id}` : "",
|
|
6662
|
+
`client=${event.client}`,
|
|
6663
|
+
`mode=${event.mode}`,
|
|
6664
|
+
`server_model_used=${event.server_model_used}`,
|
|
6665
|
+
`success=${event.success}`,
|
|
6666
|
+
event.request_id ? `request=${event.request_id}` : "",
|
|
6667
|
+
event.duration_ms !== undefined ? `duration_ms=${event.duration_ms}` : "",
|
|
6668
|
+
`created_at=${event.created_at}`
|
|
6669
|
+
].filter(Boolean).join(" ");
|
|
6670
|
+
}
|
|
4740
6671
|
function renderUsageSummary(summary) {
|
|
4741
6672
|
const lines = [
|
|
4742
6673
|
summary.account_id ? `account=${summary.account_id}` : "",
|
|
@@ -5048,6 +6979,20 @@ function renderBoardKnowledge(data) {
|
|
|
5048
6979
|
`title=${file.title}`
|
|
5049
6980
|
].join(" ")).join("\n");
|
|
5050
6981
|
}
|
|
6982
|
+
function renderBoardKnowledgeSearch(data) {
|
|
6983
|
+
const result = data;
|
|
6984
|
+
const matches = Array.isArray(result.matches) ? result.matches : [];
|
|
6985
|
+
if (matches.length === 0) {
|
|
6986
|
+
return "No matching board knowledge snippets.";
|
|
6987
|
+
}
|
|
6988
|
+
return matches.map((match, index) => [
|
|
6989
|
+
`#${index + 1}`,
|
|
6990
|
+
`${match.source}:${match.path}`,
|
|
6991
|
+
`score=${match.score}`,
|
|
6992
|
+
`title=${match.title}`,
|
|
6993
|
+
`excerpt=${match.excerpt.replace(/\s+/g, " ").trim()}`
|
|
6994
|
+
].join(" ")).join("\n");
|
|
6995
|
+
}
|
|
5051
6996
|
function renderBoardKnowledgeFile(file) {
|
|
5052
6997
|
return [
|
|
5053
6998
|
`template=${file.template_id}`,
|
|
@@ -5260,6 +7205,194 @@ function renderBuildWorkspaceSourcePatch(result) {
|
|
|
5260
7205
|
}
|
|
5261
7206
|
return lines.join("\n");
|
|
5262
7207
|
}
|
|
7208
|
+
function renderLocalToolchainList(result) {
|
|
7209
|
+
const installedCount = result.environments.filter((environment) => !!environment.installed).length;
|
|
7210
|
+
const availableCount = result.environments.filter((environment) => environment.status === "available").length;
|
|
7211
|
+
const updateCount = result.environments.filter((environment) => environment.status === "update_available").length;
|
|
7212
|
+
const lines = [
|
|
7213
|
+
`Local development environments: ${result.environments.length}`,
|
|
7214
|
+
`installed=${installedCount} available=${availableCount} updates=${updateCount}`,
|
|
7215
|
+
`host=${result.host}`,
|
|
7216
|
+
`channel=${result.channel}`,
|
|
7217
|
+
result.metadata_source === "local_override" ? `metadata_override=${result.metadata_root}` : "metadata=production/built-in",
|
|
7218
|
+
`install_root=${result.install_root}`,
|
|
7219
|
+
`registry=${result.registry_path}`
|
|
7220
|
+
];
|
|
7221
|
+
for (const environment of result.environments) {
|
|
7222
|
+
lines.push("");
|
|
7223
|
+
lines.push(`${environment.display_name} (${environment.board_id})`);
|
|
7224
|
+
lines.push(` status=${localToolchainStatusLabel(environment.status)}`);
|
|
7225
|
+
lines.push(` latest=${environment.latest.version}`);
|
|
7226
|
+
if (environment.installed) {
|
|
7227
|
+
lines.push(` installed=${environment.installed.version ?? "unknown"} mode=${environment.installed.mode ?? "unknown"}`);
|
|
7228
|
+
if (environment.installed.release_root) {
|
|
7229
|
+
lines.push(` release_root=${environment.installed.release_root}`);
|
|
7230
|
+
}
|
|
7231
|
+
}
|
|
7232
|
+
lines.push(` install_modes=${environment.install_modes.join(",")}`);
|
|
7233
|
+
if (environment.execution) {
|
|
7234
|
+
lines.push(` execution=${environment.execution.kind} supported=${environment.execution.supported}`);
|
|
7235
|
+
if (environment.execution.required_host) {
|
|
7236
|
+
lines.push(` execution_required_host=${environment.execution.required_host}`);
|
|
7237
|
+
}
|
|
7238
|
+
if (environment.execution.actual_host) {
|
|
7239
|
+
lines.push(` execution_actual_host=${environment.execution.actual_host}`);
|
|
7240
|
+
}
|
|
7241
|
+
if (environment.execution.reason) {
|
|
7242
|
+
lines.push(` execution_reason=${environment.execution.reason}`);
|
|
7243
|
+
}
|
|
7244
|
+
}
|
|
7245
|
+
lines.push(` install_command=${environment.install_command}`);
|
|
7246
|
+
if (environment.status === "update_available") {
|
|
7247
|
+
lines.push(` update_command=${environment.update_command}`);
|
|
7248
|
+
}
|
|
7249
|
+
if (environment.components?.length) {
|
|
7250
|
+
lines.push(` component_catalog=${localToolchainComponentSummary(environment.components)}`);
|
|
7251
|
+
if (environment.latest.default_mode) {
|
|
7252
|
+
lines.push(` default_mode_download=${environment.latest.default_mode}: ${localToolchainComponentSummaryForMode(environment.components, environment.latest.default_mode)}`);
|
|
7253
|
+
}
|
|
7254
|
+
if (environment.installed?.mode && environment.installed.mode !== environment.latest.default_mode) {
|
|
7255
|
+
lines.push(` installed_mode_components=${environment.installed.mode}: ${localToolchainComponentSummaryForMode(environment.components, environment.installed.mode)}`);
|
|
7256
|
+
}
|
|
7257
|
+
lines.push(` mode_downloads=${localToolchainModeSummaries(environment.install_modes, environment.components).join("; ")}`);
|
|
7258
|
+
lines.push(` package_groups=${localToolchainComponentGroups(environment.components).join(", ")}`);
|
|
7259
|
+
lines.push(` detail_command=embedlabs local toolchain latest --board ${environment.board_id}${result.channel === "stable" ? "" : ` --channel ${result.channel}`}`);
|
|
7260
|
+
}
|
|
7261
|
+
if (environment.notes.length > 0) {
|
|
7262
|
+
for (const note of environment.notes) {
|
|
7263
|
+
lines.push(` note=${note}`);
|
|
7264
|
+
}
|
|
7265
|
+
}
|
|
7266
|
+
}
|
|
7267
|
+
return lines.join("\n");
|
|
7268
|
+
}
|
|
7269
|
+
function localToolchainStatusLabel(status) {
|
|
7270
|
+
if (status === "installed")
|
|
7271
|
+
return "installed/已安装";
|
|
7272
|
+
if (status === "available")
|
|
7273
|
+
return "available/可安装";
|
|
7274
|
+
if (status === "update_available")
|
|
7275
|
+
return "update_available/可更新";
|
|
7276
|
+
if (status === "unsupported_host")
|
|
7277
|
+
return "unsupported_host/当前系统暂不支持";
|
|
7278
|
+
return status;
|
|
7279
|
+
}
|
|
7280
|
+
function renderWindowsWslStatus(result) {
|
|
7281
|
+
const lines = [
|
|
7282
|
+
"Windows WSL2 status",
|
|
7283
|
+
`host=${result.host}`,
|
|
7284
|
+
`applicable=${result.applicable}`,
|
|
7285
|
+
`wsl_available=${result.wsl_available}`,
|
|
7286
|
+
`usable=${result.usable}`,
|
|
7287
|
+
`taishanpi_execution=${result.taishanpi_execution.status} supported=${result.taishanpi_execution.supported}`,
|
|
7288
|
+
`taishanpi_required_host=${result.taishanpi_execution.required_host}`,
|
|
7289
|
+
result.taishanpi_execution.actual_host ? `taishanpi_actual_host=${result.taishanpi_execution.actual_host}` : "",
|
|
7290
|
+
`taishanpi_reason=${result.taishanpi_execution.reason}`,
|
|
7291
|
+
`checked_at=${result.checked_at}`,
|
|
7292
|
+
`status_command=${result.commands.status}`,
|
|
7293
|
+
`list_command=${result.commands.list}`,
|
|
7294
|
+
`list_online_command=${result.commands.list_online}`,
|
|
7295
|
+
`install_command=${result.commands.install_ubuntu}`
|
|
7296
|
+
].filter(Boolean);
|
|
7297
|
+
if (result.distributions.length > 0) {
|
|
7298
|
+
lines.push("distributions:");
|
|
7299
|
+
for (const distro of result.distributions) {
|
|
7300
|
+
lines.push(` ${distro.default ? "*" : "-"} ${distro.name} state=${distro.state ?? "unknown"} version=${distro.version ?? "unknown"}`);
|
|
7301
|
+
}
|
|
7302
|
+
}
|
|
7303
|
+
if (result.online_distributions.length > 0) {
|
|
7304
|
+
lines.push("online_distributions:");
|
|
7305
|
+
for (const distro of result.online_distributions) {
|
|
7306
|
+
lines.push(` ${distro.default ? "*" : "-"} ${distro.name}${distro.friendly_name ? ` (${distro.friendly_name})` : ""}`);
|
|
7307
|
+
}
|
|
7308
|
+
}
|
|
7309
|
+
for (const note of result.notes) {
|
|
7310
|
+
lines.push(`note=${note}`);
|
|
7311
|
+
}
|
|
7312
|
+
return lines.join("\n");
|
|
7313
|
+
}
|
|
7314
|
+
function renderWindowsWslInstall(result) {
|
|
7315
|
+
const lines = [
|
|
7316
|
+
"Windows WSL2 install",
|
|
7317
|
+
`host=${result.host}`,
|
|
7318
|
+
`command=${result.command.join(" ")}`,
|
|
7319
|
+
`exit_code=${result.exit_code}`,
|
|
7320
|
+
`usable_after=${result.status_after.usable}`
|
|
7321
|
+
];
|
|
7322
|
+
if (result.stdout_tail.length > 0) {
|
|
7323
|
+
lines.push("stdout_tail:");
|
|
7324
|
+
for (const line of result.stdout_tail) {
|
|
7325
|
+
lines.push(` ${line}`);
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
if (result.stderr_tail.length > 0) {
|
|
7329
|
+
lines.push("stderr_tail:");
|
|
7330
|
+
for (const line of result.stderr_tail) {
|
|
7331
|
+
lines.push(` ${line}`);
|
|
7332
|
+
}
|
|
7333
|
+
}
|
|
7334
|
+
for (const note of result.notes) {
|
|
7335
|
+
lines.push(`note=${note}`);
|
|
7336
|
+
}
|
|
7337
|
+
return lines.join("\n");
|
|
7338
|
+
}
|
|
7339
|
+
function localToolchainComponentSummary(components) {
|
|
7340
|
+
const totalBytes = components.reduce((total, component) => total + component.size_bytes, 0);
|
|
7341
|
+
return `${components.length} components, ${formatByteSize(totalBytes)}`;
|
|
7342
|
+
}
|
|
7343
|
+
function localToolchainComponentSummaryForMode(components, mode) {
|
|
7344
|
+
return localToolchainComponentSummary(localToolchainComponentsForMode(components, mode));
|
|
7345
|
+
}
|
|
7346
|
+
function localToolchainModeSummaries(modes, components) {
|
|
7347
|
+
return modes.map((mode) => `${mode}=${localToolchainComponentSummaryForMode(components, mode)}`);
|
|
7348
|
+
}
|
|
7349
|
+
function localToolchainComponentsForMode(components, mode) {
|
|
7350
|
+
return components.filter((component) => {
|
|
7351
|
+
if (!component.install_modes?.length) {
|
|
7352
|
+
return true;
|
|
7353
|
+
}
|
|
7354
|
+
return component.install_modes.includes(mode);
|
|
7355
|
+
});
|
|
7356
|
+
}
|
|
7357
|
+
function localToolchainComponentGroups(components) {
|
|
7358
|
+
const groups = new Set();
|
|
7359
|
+
for (const component of components) {
|
|
7360
|
+
const text = `${component.id} ${component.role ?? ""}`.toLowerCase();
|
|
7361
|
+
if (text.includes("arm-none-eabi") || text.includes("bare-metal") || text.includes("compiler"))
|
|
7362
|
+
groups.add("compiler/ARM 裸机编译器");
|
|
7363
|
+
if (text.includes("pico-sdk") || text.includes("sdk-core"))
|
|
7364
|
+
groups.add("pico-sdk/C/C++ SDK");
|
|
7365
|
+
if (text.includes("sysroot") || text.includes("cross"))
|
|
7366
|
+
groups.add("sysroot/交叉运行库");
|
|
7367
|
+
if (text.includes("qt"))
|
|
7368
|
+
groups.add("qt/Qt 应用支持");
|
|
7369
|
+
if (text.includes("rockchip") || text.includes("boot") || text.includes("resource"))
|
|
7370
|
+
groups.add("boot-flash/启动与烧写工具");
|
|
7371
|
+
if (text.includes("image") || text.includes("rootfs"))
|
|
7372
|
+
groups.add("images/镜像资源");
|
|
7373
|
+
if (text.includes("initial-firmware"))
|
|
7374
|
+
groups.add("initial-firmware/初始化镜像");
|
|
7375
|
+
if (text.includes("rp2350-monitor"))
|
|
7376
|
+
groups.add("rp2350-monitor/可选硬件监控镜像");
|
|
7377
|
+
if (text.includes("meta"))
|
|
7378
|
+
groups.add("metadata/知识与脚本元数据");
|
|
7379
|
+
}
|
|
7380
|
+
return groups.size > 0 ? [...groups] : ["runtime/运行时工具"];
|
|
7381
|
+
}
|
|
7382
|
+
function formatByteSize(bytes) {
|
|
7383
|
+
if (!Number.isFinite(bytes) || bytes < 0) {
|
|
7384
|
+
return "unknown size";
|
|
7385
|
+
}
|
|
7386
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
7387
|
+
let value = bytes;
|
|
7388
|
+
let unit = 0;
|
|
7389
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
7390
|
+
value /= 1024;
|
|
7391
|
+
unit += 1;
|
|
7392
|
+
}
|
|
7393
|
+
const fixed = unit === 0 || value >= 10 ? value.toFixed(0) : value.toFixed(1);
|
|
7394
|
+
return `${fixed} ${units[unit]}`;
|
|
7395
|
+
}
|
|
5263
7396
|
function renderLocalToolchainLatest(result) {
|
|
5264
7397
|
const lines = [
|
|
5265
7398
|
`board=${result.board_id}`,
|
|
@@ -5267,11 +7400,26 @@ function renderLocalToolchainLatest(result) {
|
|
|
5267
7400
|
`version=${result.version}`,
|
|
5268
7401
|
`host=${result.host}`,
|
|
5269
7402
|
result.metadata_root ? `metadata_root=${result.metadata_root}` : "metadata=built-in",
|
|
5270
|
-
result.download ? `download=${result.download.mirror_kind}:${result.download.source_url}` : "",
|
|
5271
|
-
result.download ? `archive_sha256=${result.download.archive.sha256}` : "",
|
|
5272
|
-
result.download ? `archive_size_bytes=${result.download.archive.size_bytes}` : "",
|
|
7403
|
+
result.download?.source_url ? `download=${result.download.mirror_kind}:${result.download.source_url}` : "",
|
|
7404
|
+
result.download?.archive ? `archive_sha256=${result.download.archive.sha256}` : "",
|
|
7405
|
+
result.download?.archive ? `archive_size_bytes=${result.download.archive.size_bytes}` : "",
|
|
7406
|
+
result.download?.components?.length ? `components=${result.download.components.length}` : "",
|
|
7407
|
+
result.download?.default_mode ? `default_mode=${result.download.default_mode}` : "",
|
|
5273
7408
|
result.download_error ? `download_error=${result.download_error}` : ""
|
|
5274
7409
|
].filter(Boolean);
|
|
7410
|
+
if (result.download?.components?.length) {
|
|
7411
|
+
const modes = [...new Set(result.download.components.flatMap((component) => component.install_modes ?? ["all"]))].filter((mode) => mode !== "all");
|
|
7412
|
+
if (result.download.default_mode) {
|
|
7413
|
+
lines.push(`default_mode_download=${result.download.default_mode}: ${localToolchainDownloadComponentSummaryForMode(result.download.components, result.download.default_mode)}`);
|
|
7414
|
+
}
|
|
7415
|
+
if (modes.length > 0) {
|
|
7416
|
+
lines.push(`mode_downloads=${modes.map((mode) => `${mode}=${localToolchainDownloadComponentSummaryForMode(result.download?.components ?? [], mode)}`).join("; ")}`);
|
|
7417
|
+
}
|
|
7418
|
+
lines.push("download_components:");
|
|
7419
|
+
for (const component of result.download.components) {
|
|
7420
|
+
lines.push(` ${component.id}@${component.version} modes=${component.install_modes?.join(",") || "all"} size=${formatByteSize(component.archive.size_bytes)} file=${component.archive.file}`);
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
5275
7423
|
if (result.packages.length > 0) {
|
|
5276
7424
|
lines.push("packages:");
|
|
5277
7425
|
for (const pkg of result.packages) {
|
|
@@ -5280,6 +7428,11 @@ function renderLocalToolchainLatest(result) {
|
|
|
5280
7428
|
}
|
|
5281
7429
|
return lines.join("\n");
|
|
5282
7430
|
}
|
|
7431
|
+
function localToolchainDownloadComponentSummaryForMode(components, mode) {
|
|
7432
|
+
const selected = localToolchainComponentsForMode(components ?? [], mode);
|
|
7433
|
+
const totalBytes = selected.reduce((total, component) => total + component.archive.size_bytes, 0);
|
|
7434
|
+
return `${selected.length} components, ${formatByteSize(totalBytes)}`;
|
|
7435
|
+
}
|
|
5283
7436
|
function renderLocalToolchainCurrent(result) {
|
|
5284
7437
|
if (!result.installed) {
|
|
5285
7438
|
return [
|
|
@@ -5294,6 +7447,7 @@ function renderLocalToolchainCurrent(result) {
|
|
|
5294
7447
|
`board=${result.board_id}`,
|
|
5295
7448
|
result.version ? `version=${result.version}` : "",
|
|
5296
7449
|
result.channel ? `channel=${result.channel}` : "",
|
|
7450
|
+
result.mode ? `mode=${result.mode}` : "",
|
|
5297
7451
|
result.release_root ? `release_root=${result.release_root}` : "",
|
|
5298
7452
|
`install_root=${result.install_root}`,
|
|
5299
7453
|
`registry=${result.registry_path}`
|
|
@@ -5306,13 +7460,21 @@ function renderLocalToolchainInstall(result) {
|
|
|
5306
7460
|
`version=${result.version}`,
|
|
5307
7461
|
`channel=${result.channel}`,
|
|
5308
7462
|
`host=${result.host}`,
|
|
7463
|
+
`mode=${result.mode}`,
|
|
5309
7464
|
`install_root=${result.install_root}`,
|
|
5310
7465
|
`release_root=${result.release_root}`,
|
|
5311
7466
|
`registry=${result.registry_path}`,
|
|
5312
7467
|
`source=${result.source.kind}:${result.source.value}`,
|
|
5313
7468
|
result.source.downloaded_path ? `downloaded=${result.source.downloaded_path}` : "",
|
|
7469
|
+
result.source.components?.length ? `components=${result.source.components.length}` : "",
|
|
5314
7470
|
`validation=${result.validation.ok ? "ok" : "failed"}`
|
|
5315
7471
|
].filter(Boolean);
|
|
7472
|
+
if (result.source.components?.length) {
|
|
7473
|
+
lines.push("installed_components:");
|
|
7474
|
+
for (const component of result.source.components) {
|
|
7475
|
+
lines.push(` ${component.id}@${component.version} ${component.mirror_kind || ""} bytes=${component.size_bytes}`);
|
|
7476
|
+
}
|
|
7477
|
+
}
|
|
5316
7478
|
if (result.installed_paths.length > 0) {
|
|
5317
7479
|
lines.push("installed_paths:");
|
|
5318
7480
|
for (const installedPath of result.installed_paths) {
|
|
@@ -5327,16 +7489,50 @@ function renderLocalToolchainInstall(result) {
|
|
|
5327
7489
|
}
|
|
5328
7490
|
return lines.join("\n");
|
|
5329
7491
|
}
|
|
7492
|
+
function renderLocalToolchainUninstall(result) {
|
|
7493
|
+
const lines = [
|
|
7494
|
+
result.removed ? "Local toolchain uninstalled." : "Local toolchain was not installed.",
|
|
7495
|
+
`board=${result.board_id}`,
|
|
7496
|
+
`install_root=${result.install_root}`,
|
|
7497
|
+
`registry=${result.registry_path}`,
|
|
7498
|
+
`removed_registry_entry=${result.removed_registry_entry}`,
|
|
7499
|
+
`observed_at=${result.observed_at}`
|
|
7500
|
+
];
|
|
7501
|
+
if (result.removed_paths.length > 0) {
|
|
7502
|
+
lines.push("removed_paths:");
|
|
7503
|
+
for (const removedPath of result.removed_paths) {
|
|
7504
|
+
lines.push(` ${removedPath}`);
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
if (result.remaining_installed_boards.length > 0) {
|
|
7508
|
+
lines.push(`remaining_installed_boards=${result.remaining_installed_boards.join(",")}`);
|
|
7509
|
+
}
|
|
7510
|
+
return lines.join("\n");
|
|
7511
|
+
}
|
|
5330
7512
|
function renderLocalToolchainValidation(result) {
|
|
5331
7513
|
const lines = [
|
|
5332
7514
|
result.ok ? "Local toolchain ready." : "Local toolchain not ready.",
|
|
5333
7515
|
`board=${result.board_id}`,
|
|
7516
|
+
`mode=${result.mode}`,
|
|
5334
7517
|
`host=${result.host.platform}/${result.host.arch}`,
|
|
5335
|
-
`release_root=${result.release_root}
|
|
7518
|
+
`release_root=${result.release_root}`,
|
|
7519
|
+
`summary=${result.summary_for_user}`
|
|
5336
7520
|
];
|
|
7521
|
+
if (!result.ok && result.missing_groups.length > 0) {
|
|
7522
|
+
lines.push(`missing_groups=${result.missing_groups.join(", ")}`);
|
|
7523
|
+
}
|
|
7524
|
+
if (result.repair_command) {
|
|
7525
|
+
lines.push(`repair_command=${result.repair_command}`);
|
|
7526
|
+
}
|
|
5337
7527
|
for (const check of result.checked_paths) {
|
|
5338
7528
|
lines.push(`${check.exists ? "ok" : "missing"} ${check.label}: ${check.path}`);
|
|
5339
7529
|
}
|
|
7530
|
+
if (result.path_leaks.length > 0) {
|
|
7531
|
+
lines.push("path_leaks:");
|
|
7532
|
+
for (const leak of result.path_leaks) {
|
|
7533
|
+
lines.push(` ${leak.label}: ${leak.path} contains ${leak.forbidden}`);
|
|
7534
|
+
}
|
|
7535
|
+
}
|
|
5340
7536
|
if (result.notes.length > 0) {
|
|
5341
7537
|
lines.push("notes:");
|
|
5342
7538
|
for (const note of result.notes) {
|
|
@@ -5785,6 +7981,22 @@ function stringFlag(parsed, name) {
|
|
|
5785
7981
|
function booleanFlag(parsed, name) {
|
|
5786
7982
|
return parsed.flags[name] === true;
|
|
5787
7983
|
}
|
|
7984
|
+
function optionalBooleanFlag(parsed, name) {
|
|
7985
|
+
const values = flagValues(parsed, name);
|
|
7986
|
+
if (values.length === 0)
|
|
7987
|
+
return undefined;
|
|
7988
|
+
const value = values[values.length - 1];
|
|
7989
|
+
if (value === true)
|
|
7990
|
+
return true;
|
|
7991
|
+
if (typeof value !== "string")
|
|
7992
|
+
return `--${name} must be true or false.`;
|
|
7993
|
+
const normalized = value.trim().toLowerCase();
|
|
7994
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized))
|
|
7995
|
+
return true;
|
|
7996
|
+
if (["0", "false", "no", "n", "off"].includes(normalized))
|
|
7997
|
+
return false;
|
|
7998
|
+
return `--${name} must be true or false.`;
|
|
7999
|
+
}
|
|
5788
8000
|
function switchFlag(parsed, name) {
|
|
5789
8001
|
const values = flagValues(parsed, name);
|
|
5790
8002
|
for (const value of values) {
|
|
@@ -5948,6 +8160,24 @@ function optionalPositiveIntegerFlag(parsed, name) {
|
|
|
5948
8160
|
}
|
|
5949
8161
|
return { value };
|
|
5950
8162
|
}
|
|
8163
|
+
function optionalIntegerFlag(parsed, name, min, max) {
|
|
8164
|
+
const values = flagValues(parsed, name);
|
|
8165
|
+
if (values.length === 0) {
|
|
8166
|
+
return {};
|
|
8167
|
+
}
|
|
8168
|
+
if (values.some((value) => typeof value !== "string")) {
|
|
8169
|
+
return { error: `--${name} requires a value.` };
|
|
8170
|
+
}
|
|
8171
|
+
const raw = values[values.length - 1].trim();
|
|
8172
|
+
if (!/^-?\d+$/.test(raw)) {
|
|
8173
|
+
return { error: `--${name} must be an integer.` };
|
|
8174
|
+
}
|
|
8175
|
+
const value = Number(raw);
|
|
8176
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
8177
|
+
return { error: `--${name} must be an integer from ${min} through ${max}.` };
|
|
8178
|
+
}
|
|
8179
|
+
return { value };
|
|
8180
|
+
}
|
|
5951
8181
|
function jsonObjectListFlag(parsed, name) {
|
|
5952
8182
|
const objects = [];
|
|
5953
8183
|
for (const raw of flagValues(parsed, name)) {
|
|
@@ -6065,12 +8295,17 @@ async function waitForever() {
|
|
|
6065
8295
|
});
|
|
6066
8296
|
return 0;
|
|
6067
8297
|
}
|
|
8298
|
+
function quotaAndBillingDisabled() {
|
|
8299
|
+
return fail("quota_not_supported", "当前 Embed Labs 服务器只提供 MCP 服务,不提供额度、充值、余额、账本、存储配额或用量计费功能。", {
|
|
8300
|
+
remediation: "请使用 auth、plugin、board、local toolchain、mcp start、device 和 Local Bridge 相关命令。"
|
|
8301
|
+
});
|
|
8302
|
+
}
|
|
6068
8303
|
function printHelp() {
|
|
6069
8304
|
printCliHelp(`embed CLI
|
|
6070
8305
|
|
|
6071
8306
|
Usage:
|
|
6072
8307
|
embed <command> [options]
|
|
6073
|
-
embed query "
|
|
8308
|
+
embed query "当前支持哪些开发板"
|
|
6074
8309
|
embed help getting-started
|
|
6075
8310
|
embed help commands
|
|
6076
8311
|
|
|
@@ -6080,68 +8315,57 @@ Main workflow:
|
|
|
6080
8315
|
2. Sign in with a token:
|
|
6081
8316
|
embed auth login --token <token>
|
|
6082
8317
|
# or set EMBED_API_TOKEN / create an account API key for automation.
|
|
6083
|
-
3.
|
|
8318
|
+
3. Install or update local AI client plugins:
|
|
6084
8319
|
embed plugin install codex
|
|
6085
8320
|
embed plugin install opencode
|
|
6086
|
-
embed
|
|
6087
|
-
embed model list
|
|
6088
|
-
embed model default
|
|
8321
|
+
embed plugin update check
|
|
6089
8322
|
4. Run a natural-language local tool loop:
|
|
6090
8323
|
embed agent run --prompt "验证开发板状态"
|
|
6091
8324
|
5. Validate or use the local TaishanPi toolchain:
|
|
8325
|
+
embed local toolchain list
|
|
8326
|
+
embed local toolchain installed
|
|
6092
8327
|
embed local toolchain latest
|
|
6093
8328
|
embed local toolchain install
|
|
8329
|
+
embed local toolchain uninstall --board pico2w-rp2350-monitor
|
|
6094
8330
|
embed local toolchain validate
|
|
6095
8331
|
embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
|
|
6096
8332
|
embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
|
|
6097
|
-
|
|
8333
|
+
embed local build qt-smoke --source ./taishanpi-app --target-name app --build-dir ./.embed-labs/build/app
|
|
8334
|
+
6. Query board knowledge and method metadata:
|
|
6098
8335
|
embed board registry list
|
|
6099
8336
|
embed board methods taishanpi-1m-rk3566
|
|
6100
8337
|
embed board knowledge taishanpi-1m-rk3566
|
|
8338
|
+
embed board knowledge search taishanpi-1m-rk3566 --query "UART pinout"
|
|
6101
8339
|
embed build template list
|
|
6102
8340
|
embed build template show <template_id>
|
|
6103
|
-
7.
|
|
6104
|
-
embed build workspace provision --account <account_id> --project <project_id> --template <template_id>
|
|
6105
|
-
embed build resource lease create --workspace <workspace_id> --execution-mode cloud_worker
|
|
6106
|
-
embed build workspace source put <workspace_id> --file ./main.c:src/main.c
|
|
6107
|
-
embed build workspace source list <workspace_id>
|
|
6108
|
-
embed build workspace source get <workspace_id> --path src/main.c --output ./main.c
|
|
6109
|
-
embed build workspace source search <workspace_id> --query init --glob "**/*.c"
|
|
6110
|
-
embed build workspace source patch <workspace_id> --patch ./fix.patch
|
|
6111
|
-
embed build workspace release <workspace_id> --dry-run
|
|
6112
|
-
8. Generate application source on the server and follow artifacts:
|
|
6113
|
-
embed build application generate --workspace <workspace_id> --prompt "Create a minimal Linux app" --provider bai --model gpt-5.2
|
|
6114
|
-
embed build application compile --workspace <workspace_id> --source app/generated.c --execution-mode docker_worker
|
|
6115
|
-
embed build image generate --workspace <workspace_id> --prompt "Generate a minimal TaishanPi image"
|
|
8341
|
+
7. Generate small image packages and compose locally:
|
|
6116
8342
|
embed build image boot-logo --logo ./logo.png --board taishanpi --variant 1M-RK3566 --output ./boot-logo-package.json
|
|
6117
8343
|
embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
|
|
6118
8344
|
embed cloud task artifacts <task_id>
|
|
6119
8345
|
embed artifact download <artifact_id> --output ./artifact.bin
|
|
6120
|
-
9. Check credits or create a recharge QR:
|
|
6121
|
-
embed billing balance --account <account_id>
|
|
6122
|
-
embed billing tokens --account <account_id>
|
|
6123
|
-
embed billing ledger --account <account_id>
|
|
6124
|
-
embed billing storage --account <account_id>
|
|
6125
|
-
embed billing storage settle --account <account_id> --dry-run
|
|
6126
|
-
embed billing recharge create --account <account_id> --amount-usd 10 --provider onchain --qr
|
|
6127
|
-
|
|
6128
8346
|
Local hardware:
|
|
6129
8347
|
embed bridge start
|
|
8348
|
+
embed mcp start
|
|
6130
8349
|
embed agent run --prompt "验证开发板状态"
|
|
6131
8350
|
embed agent run --prompt "部署泰山派应用" --host 198.19.77.2 --artifact ./artifact.bin --remote-path /userdata/embed-labs/apps/app --approve --run
|
|
6132
8351
|
embed agent run --prompt "部署生成的泰山派应用" --host 198.19.77.2 --artifact-task <task_id> --remote-path /userdata/embed-labs/apps/app --approve --run
|
|
6133
8352
|
embed tool list
|
|
6134
|
-
embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22
|
|
8353
|
+
embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22]}'
|
|
6135
8354
|
embed tool call wifi.scan --input-json '{"host":"198.19.77.2","user":"root"}'
|
|
8355
|
+
embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve
|
|
6136
8356
|
embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}'
|
|
6137
8357
|
embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}'
|
|
6138
8358
|
embed device list
|
|
8359
|
+
embed local toolchain list
|
|
8360
|
+
embed local toolchain installed
|
|
6139
8361
|
embed local toolchain latest
|
|
6140
8362
|
embed local toolchain current
|
|
6141
8363
|
embed local toolchain install
|
|
8364
|
+
embed local toolchain uninstall --board pico2w-rp2350-monitor
|
|
6142
8365
|
embed local toolchain validate
|
|
6143
8366
|
embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
|
|
6144
8367
|
embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
|
|
8368
|
+
embed local build qt-smoke --source ./taishanpi-app --target-name app --build-dir ./.embed-labs/build/app
|
|
6145
8369
|
embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
|
|
6146
8370
|
embed deploy taishanpi --host 198.19.77.2 --artifact ./artifact.bin --approve --run
|
|
6147
8371
|
embed flash plan --board <rp2350|taishanpi>
|
|
@@ -6152,16 +8376,14 @@ Help:
|
|
|
6152
8376
|
|
|
6153
8377
|
Environment:
|
|
6154
8378
|
EMBED_BRIDGE_URL=http://127.0.0.1:18083
|
|
6155
|
-
EMBED_CLOUD_API_URL=
|
|
8379
|
+
EMBED_CLOUD_API_URL=https://api.embedboard.com
|
|
6156
8380
|
EMBED_API_TOKEN=<token>
|
|
6157
8381
|
CODEX_HOME=~/.codex
|
|
6158
8382
|
|
|
6159
8383
|
Natural language:
|
|
6160
|
-
embed query "
|
|
6161
|
-
embed query "
|
|
6162
|
-
embed query "
|
|
6163
|
-
embed query "看一下磁盘空间"
|
|
6164
|
-
embed query "结算磁盘空间扣费"
|
|
8384
|
+
embed query "当前支持哪些开发板"
|
|
8385
|
+
embed query "验证开发板状态"
|
|
8386
|
+
embed query "安装泰山派本地开发环境"
|
|
6165
8387
|
`);
|
|
6166
8388
|
}
|
|
6167
8389
|
function printHelpTopic(topic) {
|
|
@@ -6207,20 +8429,21 @@ Install local AI client plugins explicitly:
|
|
|
6207
8429
|
embed plugin list
|
|
6208
8430
|
embed plugin install codex
|
|
6209
8431
|
embed plugin install opencode
|
|
8432
|
+
embed plugin update check
|
|
8433
|
+
embed plugin update all
|
|
6210
8434
|
|
|
6211
|
-
|
|
8435
|
+
Local MCP service path:
|
|
6212
8436
|
|
|
6213
|
-
embed service modes
|
|
6214
|
-
embed model list
|
|
6215
|
-
embed model default
|
|
6216
8437
|
embed agent run --prompt "验证开发板状态"
|
|
6217
8438
|
embed local toolchain validate
|
|
6218
8439
|
embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
|
|
6219
8440
|
embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
|
|
8441
|
+
embed local build qt-smoke --source ./taishanpi-app --target-name app --build-dir ./.embed-labs/build/app
|
|
6220
8442
|
embed board registry list
|
|
6221
8443
|
embed board registry show taishanpi-1m-rk3566
|
|
6222
8444
|
embed board methods taishanpi-1m-rk3566
|
|
6223
8445
|
embed board knowledge taishanpi-1m-rk3566
|
|
8446
|
+
embed board knowledge search taishanpi-1m-rk3566 --query "UART pinout"
|
|
6224
8447
|
embed build template list
|
|
6225
8448
|
embed build template show <template_id>
|
|
6226
8449
|
embed build workspace provision --account <account_id> --project <project_id> --template <template_id>
|
|
@@ -6231,9 +8454,6 @@ Cloud build path:
|
|
|
6231
8454
|
embed build workspace source search <workspace_id> --query init --glob "**/*.c"
|
|
6232
8455
|
embed build workspace source patch <workspace_id> --patch ./fix.patch
|
|
6233
8456
|
embed build workspace release <workspace_id> --dry-run
|
|
6234
|
-
embed build application generate --workspace <workspace_id> --prompt "Create a minimal Linux app" --provider bai --model gpt-5.2
|
|
6235
|
-
embed build application compile --workspace <workspace_id> --source app/generated.c --execution-mode docker_worker
|
|
6236
|
-
embed build image generate --workspace <workspace_id> --prompt "Generate a minimal TaishanPi image"
|
|
6237
8457
|
embed build image boot-logo --logo ./logo.png --board taishanpi --variant 1M-RK3566 --output ./boot-logo-package.json
|
|
6238
8458
|
embed image boot-logo compose --package ./boot-logo-package.json --base-image ./boot.img --output ./boot-logo.img
|
|
6239
8459
|
embed cloud task status <task_id>
|
|
@@ -6242,29 +8462,18 @@ Cloud build path:
|
|
|
6242
8462
|
|
|
6243
8463
|
Natural language shortcuts:
|
|
6244
8464
|
|
|
6245
|
-
embed query "
|
|
6246
|
-
embed query "
|
|
6247
|
-
embed query "
|
|
6248
|
-
embed query "看一下磁盘空间"
|
|
6249
|
-
|
|
6250
|
-
Billing path:
|
|
6251
|
-
|
|
6252
|
-
embed billing balance --account <account_id>
|
|
6253
|
-
embed billing tokens --account <account_id>
|
|
6254
|
-
embed billing ledger --account <account_id>
|
|
6255
|
-
embed billing storage --account <account_id>
|
|
6256
|
-
embed billing storage settle --account <account_id> --dry-run
|
|
6257
|
-
embed billing recharge create --account <account_id> --amount-usd 10 --provider onchain --qr
|
|
6258
|
-
embed billing recharge submit-tx <recharge_session_id> --tx-hash <hash>
|
|
6259
|
-
embed billing recharge list --account <account_id>
|
|
8465
|
+
embed query "当前支持哪些开发板"
|
|
8466
|
+
embed query "验证开发板状态"
|
|
8467
|
+
embed query "安装泰山派本地开发环境"
|
|
6260
8468
|
|
|
6261
8469
|
Local hardware path:
|
|
6262
8470
|
|
|
6263
8471
|
embed bridge start
|
|
8472
|
+
embed mcp start
|
|
6264
8473
|
embed run "验证开发板状态"
|
|
6265
8474
|
embed tool list
|
|
6266
8475
|
embed tool call debug.tools.scan
|
|
6267
|
-
embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22
|
|
8476
|
+
embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22]}'
|
|
6268
8477
|
embed device list
|
|
6269
8478
|
embed deploy taishanpi --host 198.19.77.2 --artifact ./artifact.bin --approve --run
|
|
6270
8479
|
embed flash plan --board <rp2350|taishanpi> --artifact ./artifact.bin
|
|
@@ -6280,11 +8489,14 @@ Usage:
|
|
|
6280
8489
|
embed query <natural language request> [--account <account_id>] [--qr] [--json]
|
|
6281
8490
|
embed bridge start [--host 127.0.0.1] [--port 18083]
|
|
6282
8491
|
embed bridge status [--json]
|
|
8492
|
+
embed mcp start [--bridge-path <path>]
|
|
6283
8493
|
embed auth login --token <token> [--profile default] [--json]
|
|
6284
8494
|
embed auth status [--json]
|
|
6285
8495
|
embed auth logout [--json]
|
|
6286
8496
|
embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]
|
|
6287
8497
|
embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]
|
|
8498
|
+
embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
|
|
8499
|
+
embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
|
|
6288
8500
|
embed service modes [--json]
|
|
6289
8501
|
embed model list [--json]
|
|
6290
8502
|
embed model default [--json]
|
|
@@ -6292,6 +8504,7 @@ Usage:
|
|
|
6292
8504
|
embed board registry show <template_id> [--json]
|
|
6293
8505
|
embed board methods <template_id> [--json]
|
|
6294
8506
|
embed board knowledge <template_id> [--json]
|
|
8507
|
+
embed board knowledge search <template_id> --query <text> [--source board_pack|build_template|registry] [--limit 5] [--json]
|
|
6295
8508
|
embed board knowledge file <template_id> --source board_pack|build_template|registry --path <relative_path> [--output <local_path>] [--json]
|
|
6296
8509
|
embed agent run --prompt <request> [--account <account_id>] [--workspace <workspace_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--max-tool-calls 6] [--host <ip>] [--ports 22,15301] [--artifact <local_file>|--artifact-id <artifact_id>|--artifact-task <task_id>] [--artifact-output <path>] [--remote-path <path>] [--run] [--approve] [--json]
|
|
6297
8510
|
embed run <natural language request> [--provider stub|openai|bai|cc|claude-code] [--approve] [--json]
|
|
@@ -6302,23 +8515,6 @@ Usage:
|
|
|
6302
8515
|
embed account keys create --account <account_id> [--name <name>] [--json]
|
|
6303
8516
|
embed account keys list --account <account_id> [--json]
|
|
6304
8517
|
embed account keys revoke <api_key_id> [--json]
|
|
6305
|
-
embed usage record [--api-key <key>|--api-key-id <api_key_id>] --model <model> --input-tokens <n> --output-tokens <n> [--provider <name>] [--operation <name>] [--task <task_id>] [--request-id <id>] [--json]
|
|
6306
|
-
embed usage summary --account <account_id>|--api-key-id <api_key_id> [--from <iso>] [--to <iso>] [--json]
|
|
6307
|
-
embed usage events --account <account_id> [--api-key-id <api_key_id>] [--from <iso>] [--to <iso>] [--limit 100] [--json]
|
|
6308
|
-
embed billing statement --account <account_id> [--from <iso>] [--to <iso>] [--json]
|
|
6309
|
-
embed billing balance --account <account_id> [--json]
|
|
6310
|
-
embed billing tokens --account <account_id> [--json]
|
|
6311
|
-
embed billing ledger --account <account_id> [--json]
|
|
6312
|
-
embed billing storage --account <account_id> [--json]
|
|
6313
|
-
embed billing storage settle --account <account_id> [--dry-run] [--json]
|
|
6314
|
-
embed billing recharge create --account <account_id> --amount-usd <amount> [--provider mock|stripe|onchain] [--chain <chain>] [--token <symbol>] [--success-url <url>] [--cancel-url <url>] [--qr] [--json]
|
|
6315
|
-
embed billing recharge list --account <account_id> [--json]
|
|
6316
|
-
embed billing recharge show <recharge_session_id> [--json]
|
|
6317
|
-
embed billing recharge submit-tx <recharge_session_id> --tx-hash <hash> [--json]
|
|
6318
|
-
embed billing recharge confirm <recharge_session_id> [--json]
|
|
6319
|
-
embed billing snapshot create --account <account_id> [--from <iso>] [--to <iso>] [--json]
|
|
6320
|
-
embed billing snapshot list --account <account_id> [--json]
|
|
6321
|
-
embed billing snapshot show <billing_snapshot_id> [--json]
|
|
6322
8518
|
embed service modes [--json]
|
|
6323
8519
|
embed build template list [--json]
|
|
6324
8520
|
embed build template show <template_id> [--json]
|
|
@@ -6340,13 +8536,16 @@ Usage:
|
|
|
6340
8536
|
embed build image generate --workspace <workspace_id> --prompt <request> [--account <account_id>] [--image-profile <profile_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--execution-mode cloud_worker|dry_run] [--worker-pool <pool>] [--json]
|
|
6341
8537
|
embed build image boot-logo --logo <local_image> [--account <account_id>] [--project <project_id>] [--board taishanpi] [--variant 1M-RK3566] [--kernel-logo <local_image>] [--rotate -90] [--scale 100] [--output <package.json>] [--json]
|
|
6342
8538
|
embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]
|
|
6343
|
-
embed local toolchain
|
|
8539
|
+
embed local toolchain list [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
|
|
8540
|
+
embed local toolchain installed [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
|
|
8541
|
+
embed local toolchain latest [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--json]
|
|
6344
8542
|
embed local toolchain current [--install-root <path>] [--json]
|
|
6345
|
-
embed local toolchain install [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--force] [--json]
|
|
8543
|
+
embed local toolchain install [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--force] [--json]
|
|
6346
8544
|
Defaults to the production download channel at download.embedboard.com.
|
|
6347
|
-
embed local toolchain
|
|
8545
|
+
embed local toolchain uninstall --board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor [--install-root <path>] [--json]
|
|
8546
|
+
embed local toolchain validate [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--release-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--json]
|
|
6348
8547
|
embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]
|
|
6349
|
-
embed local build qt-smoke --build-dir <dir> [--source <qt-
|
|
8548
|
+
embed local build qt-smoke --build-dir <dir> [--source <qt-cmake-dir>] [--target-name <executable>] [--release-root <path>] [--account <account_id>] [--json]
|
|
6350
8549
|
embed debug tools [--json]
|
|
6351
8550
|
embed tool list [--json]
|
|
6352
8551
|
embed tool call wifi.scan --input-json '{"host":"198.19.77.2","user":"root"}' [--json]
|
|
@@ -6355,6 +8554,18 @@ Usage:
|
|
|
6355
8554
|
embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}' [--json]
|
|
6356
8555
|
embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
|
|
6357
8556
|
embed tool call qml.runtime.start --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
|
|
8557
|
+
embed tool call rp2350.monitor.capabilities [--json]
|
|
8558
|
+
embed tool call rp2350.monitor.status [--json]
|
|
8559
|
+
embed tool call rp2350.monitor.gpio.read --input-json '{"pins":[16,17]}' --approve [--json]
|
|
8560
|
+
embed tool call rp2350.monitor.gpio.write --input-json '{"pin":16,"level":true}' --approve [--json]
|
|
8561
|
+
embed tool call rp2350.monitor.uart.write --input-json '{"baud":115200,"text":"hello","line_ending":"lf"}' --approve [--json]
|
|
8562
|
+
embed tool call rp2350.monitor.i2c.transfer --input-json '{"address":"0x50","write":"00","read_len":4}' --approve [--json]
|
|
8563
|
+
embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve [--json]
|
|
8564
|
+
embed tool call rp2350.monitor.logic.capture --input-json '{"pin_base":16,"pin_count":4,"sample_rate":1000000,"samples":4096}' --approve [--json]
|
|
8565
|
+
embed tool call rp2350.monitor.logic.decode --input-json '{"input_path":".embed-labs/rp2350-monitor/captures/logic.jsonl","decoder":"summary"}' [--json]
|
|
8566
|
+
embed tool call rp2350.monitor.wifi.manage --input-json '{"action":"scan"}' --approve [--json]
|
|
8567
|
+
embed tool call rp2350.monitor.probe.debug --input-json '{"action":"status"}' --approve [--json]
|
|
8568
|
+
embed tool call rp2350.monitor.operation --input-json '{"action":"logic.stop","params":{}}' --approve [--json]
|
|
6358
8569
|
embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
|
|
6359
8570
|
embed board deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
|
|
6360
8571
|
embed device list [--json]
|
|
@@ -6388,7 +8599,7 @@ Usage:
|
|
|
6388
8599
|
|
|
6389
8600
|
Environment:
|
|
6390
8601
|
EMBED_BRIDGE_URL=http://127.0.0.1:18083
|
|
6391
|
-
EMBED_CLOUD_API_URL=
|
|
8602
|
+
EMBED_CLOUD_API_URL=https://api.embedboard.com
|
|
6392
8603
|
EMBED_API_TOKEN=<token>
|
|
6393
8604
|
CODEX_HOME=~/.codex
|
|
6394
8605
|
`);
|