@kvell007/embed-labs-cli 0.1.0-alpha.5 → 0.1.0-alpha.50
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 +51 -3
- package/dist/index.js +2048 -86
- package/dist/index.js.map +1 -1
- package/dist/install-progress.d.ts +2 -0
- package/dist/install-progress.js +74 -0
- package/dist/install-progress.js.map +1 -0
- package/dist/local-toolchain.d.ts +135 -2
- package/dist/local-toolchain.js +1121 -64
- 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, validateLocalToolchain } from "./local-toolchain.js";
|
|
13
12
|
import { fail, ok } from "@embed-labs/protocol";
|
|
14
13
|
const require = createRequire(import.meta.url);
|
|
15
14
|
const CLI_MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
16
15
|
const SOURCE_CHECKOUT_ROOT = resolve(CLI_MODULE_DIR, "..", "..", "..");
|
|
17
16
|
const qrcodeTerminal = require("qrcode-terminal");
|
|
18
17
|
const DEFAULT_BRIDGE_URL = process.env.EMBED_BRIDGE_URL ?? "http://127.0.0.1:18083";
|
|
19
|
-
const DEFAULT_CLOUD_API_URL = process.env.EMBED_CLOUD_API_URL ?? "
|
|
18
|
+
const DEFAULT_CLOUD_API_URL = process.env.EMBED_CLOUD_API_URL ?? "https://api.embedboard.com";
|
|
20
19
|
const DEFAULT_AUTH_FILE = process.env.EMBED_AUTH_FILE ?? ".embed-labs/auth.json";
|
|
20
|
+
const DEFAULT_DEVICE_FILE = process.env.EMBED_DEVICE_FILE ?? join(dirname(DEFAULT_AUTH_FILE), "device.json");
|
|
21
|
+
const DEFAULT_DASHBOARD_URL = process.env.EMBED_DASHBOARD_URL ?? "https://api.embedboard.com/dashboard";
|
|
22
|
+
const EMBED_CLIENT_NAME = "embedlabs-cli";
|
|
23
|
+
const EMBED_CLIENT_VERSION = process.env.EMBED_CLIENT_VERSION ?? "0.1.0";
|
|
21
24
|
const DEFAULT_AGENT_ARTIFACT_DIR = process.env.EMBED_AGENT_ARTIFACT_DIR ?? ".embed-labs/artifacts";
|
|
25
|
+
const TOOL_INTEGRITY_RELOGIN_MESSAGE = "工具完整性校验失败,请在当前电脑重新执行 embedlabs auth login --token <key>";
|
|
22
26
|
const DOCTOR_USAGE = "Usage: embed doctor [--json]";
|
|
23
27
|
const DOCTOR_HTTP_TIMEOUT_MS = 8000;
|
|
24
28
|
const MIN_NODE_MAJOR = 20;
|
|
@@ -27,7 +31,19 @@ const DEVICE_PROBE_USAGE = "Usage: embed device probe --host <host> --ports 22,1
|
|
|
27
31
|
const QUERY_USAGE = "Usage: embed query <natural language request> [--account <account_id>] [--qr] [--json]";
|
|
28
32
|
const DEFAULT_PLUGIN_RELEASE_URL = process.env.EMBED_PLUGIN_RELEASE_URL?.trim() || "https://api.embedboard.com/plugin-releases/agent-plugins/latest";
|
|
29
33
|
const PLUGIN_LIST_USAGE = "Usage: embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]";
|
|
34
|
+
const CODEX_PLUGIN_NAME = "embed-labs";
|
|
35
|
+
const CODEX_MARKETPLACE_NAME = "embed-labs";
|
|
36
|
+
const LEGACY_CODEX_PLUGIN_NAMES = [
|
|
37
|
+
"dbt-agent",
|
|
38
|
+
"Dbt Agent",
|
|
39
|
+
"development-board-toolchain",
|
|
40
|
+
"development-board-toolchain-dev",
|
|
41
|
+
"deve"
|
|
42
|
+
];
|
|
43
|
+
const LEGACY_CODEX_MARKETPLACE_NAMES = new Set(["embed-labs-plugins", "plugins", "Plugins", "deve"]);
|
|
30
44
|
const PLUGIN_INSTALL_USAGE = "Usage: embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]";
|
|
45
|
+
const PLUGIN_UPDATE_CHECK_USAGE = "Usage: embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
|
|
46
|
+
const PLUGIN_UPDATE_USAGE = "Usage: embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]";
|
|
31
47
|
const CLOUD_TASK_ARTIFACTS_USAGE = "Usage: embed cloud task artifacts <task_id> [--json]";
|
|
32
48
|
const CLOUD_TASK_EVIDENCE_USAGE = "Usage: embed cloud task evidence <task_id> [--json]";
|
|
33
49
|
const ARTIFACT_STATUS_USAGE = "Usage: embed artifact status <artifact_id> [--json]";
|
|
@@ -81,23 +97,32 @@ const BUILD_IMAGE_BOOT_LOGO_USAGE = "Usage: embed build image boot-logo --logo <
|
|
|
81
97
|
const IMAGE_BOOT_LOGO_COMPOSE_USAGE = "Usage: embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]";
|
|
82
98
|
const BUILD_IMAGE_DTB_USAGE = "Usage: embed build image dtb --dtb <local.dtb|local.dts> [--input-format auto|dtb|dts] [--account <account_id>] [--project <project_id>] [--board taishanpi] [--variant 1M-RK3566] [--output <package.json>] [--json]";
|
|
83
99
|
const IMAGE_DTB_COMPOSE_USAGE = "Usage: embed image dtb compose --package <dtb-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]";
|
|
84
|
-
const
|
|
100
|
+
const LOCAL_TOOLCHAIN_LIST_USAGE = "Usage: embed local toolchain list [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]";
|
|
101
|
+
const LOCAL_TOOLCHAIN_INSTALLED_USAGE = "Usage: embed local toolchain installed [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]";
|
|
102
|
+
const LOCAL_TOOLCHAIN_LATEST_USAGE = "Usage: embed local toolchain latest [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--json]";
|
|
85
103
|
const LOCAL_TOOLCHAIN_CURRENT_USAGE = "Usage: embed local toolchain current [--install-root <path>] [--json]";
|
|
86
|
-
const LOCAL_TOOLCHAIN_INSTALL_USAGE = "Usage: embed local toolchain install [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--force] [--json]";
|
|
87
|
-
const LOCAL_TOOLCHAIN_VALIDATE_USAGE = "Usage: embed local toolchain validate [--release-root <path>] [--json]";
|
|
104
|
+
const LOCAL_TOOLCHAIN_INSTALL_USAGE = "Usage: embed local toolchain install [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--force] [--json]\nDefault source: the production download channel at download.embedboard.com.";
|
|
105
|
+
const LOCAL_TOOLCHAIN_VALIDATE_USAGE = "Usage: embed local toolchain validate [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--release-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--json]";
|
|
88
106
|
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
107
|
const LOCAL_BUILD_QT_SMOKE_USAGE = "Usage: embed local build qt-smoke --build-dir <dir> [--source <qt-smoke-dir>] [--release-root <path>] [--account <account_id>] [--json]";
|
|
108
|
+
const AUTH_DEVICE_STATUS_USAGE = "Usage: embed auth device status [--json]";
|
|
109
|
+
const AUTH_DEVICE_LIST_USAGE = "Usage: embed auth device list [--json]";
|
|
110
|
+
const AUTH_DEVICE_REVOKE_USAGE = "Usage: embed auth device revoke <device_id> [--json]";
|
|
111
|
+
const AUTH_DEVICE_RENAME_USAGE = "Usage: embed auth device rename <device_id> --label <name> [--json]";
|
|
90
112
|
const BOARD_REGISTRY_LIST_USAGE = "Usage: embed board registry list [--json]";
|
|
91
113
|
const BOARD_REGISTRY_SHOW_USAGE = "Usage: embed board registry show <template_id> [--json]";
|
|
92
114
|
const BOARD_METHODS_USAGE = "Usage: embed board methods <template_id> [--json]";
|
|
93
115
|
const BOARD_KNOWLEDGE_USAGE = "Usage: embed board knowledge <template_id> [--json]";
|
|
116
|
+
const BOARD_KNOWLEDGE_SEARCH_USAGE = "Usage: embed board knowledge search <template_id> --query <text> [--source board_pack|build_template|registry] [--limit 5] [--json]";
|
|
94
117
|
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
118
|
const MODEL_LIST_USAGE = "Usage: embed model list [--json]";
|
|
96
119
|
const MODEL_DEFAULT_USAGE = "Usage: embed model default [--json]";
|
|
97
120
|
const SERVICE_MODES_USAGE = "Usage: embed service modes [--json]";
|
|
98
121
|
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]";
|
|
122
|
+
let cachedLocalHardwareFingerprint;
|
|
99
123
|
const TOOL_LIST_USAGE = "Usage: embed tool list [--json]";
|
|
100
124
|
const TOOL_CALL_USAGE = "Usage: embed tool call <capability_id> [--input-json '<json>'] [--approve] [--json]";
|
|
125
|
+
const MCP_TOOL_EVENT_USAGE = "Usage: embed mcp log --tool <tool_name> [--client codex|opencode] [--mode local_ai|server_ai] [--local-device-id <id>] [--server-model-used true|false] [--success true|false] [--request-id <id>] [--duration-ms <ms>] [--input-summary <text>] [--output-summary <text>] [--json]";
|
|
101
126
|
const BOARD_DEPLOY_TAISHANPI_USAGE = "Usage: embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--user root] [--remote-path /userdata/embed-labs/apps/app] [--run] [--timeout 30] [--json]";
|
|
102
127
|
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
128
|
const TASK_STATES = new Set([
|
|
@@ -147,11 +172,7 @@ async function main(argv) {
|
|
|
147
172
|
return output(parsed, await bridgePost("/v1/board/taishanpi/deploy", request), renderBoardDeployResult);
|
|
148
173
|
}
|
|
149
174
|
if (area === "bridge" && action === "start") {
|
|
150
|
-
|
|
151
|
-
host: stringFlag(parsed, "host"),
|
|
152
|
-
port: numberFlag(parsed, "port")
|
|
153
|
-
});
|
|
154
|
-
return await waitForever();
|
|
175
|
+
return await runBridgeStart(parsed);
|
|
155
176
|
}
|
|
156
177
|
if (area === "bridge" && action === "status") {
|
|
157
178
|
return output(parsed, await bridgeGet("/healthz"), renderBridgeStatus);
|
|
@@ -216,6 +237,17 @@ async function main(argv) {
|
|
|
216
237
|
return output(parsed, await cloudGet(`/v1/board-registry/templates/${encodeURIComponent(idResult.value)}/methods`), renderBoardMethods);
|
|
217
238
|
}
|
|
218
239
|
if (action === "knowledge") {
|
|
240
|
+
if (parsed.command[2] === "search") {
|
|
241
|
+
const request = boardKnowledgeSearchRequest(parsed);
|
|
242
|
+
if (typeof request === "string") {
|
|
243
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
244
|
+
}
|
|
245
|
+
const params = new URLSearchParams({ q: request.query, limit: String(request.limit) });
|
|
246
|
+
if (request.source) {
|
|
247
|
+
params.set("source", request.source);
|
|
248
|
+
}
|
|
249
|
+
return output(parsed, await cloudGet(`/v1/board-registry/templates/${encodeURIComponent(request.templateId)}/knowledge-search?${params.toString()}`), renderBoardKnowledgeSearch);
|
|
250
|
+
}
|
|
219
251
|
if (parsed.command[2] === "file") {
|
|
220
252
|
const request = boardKnowledgeFileRequest(parsed);
|
|
221
253
|
if (typeof request === "string") {
|
|
@@ -241,6 +273,7 @@ async function main(argv) {
|
|
|
241
273
|
BOARD_REGISTRY_SHOW_USAGE,
|
|
242
274
|
BOARD_METHODS_USAGE,
|
|
243
275
|
BOARD_KNOWLEDGE_USAGE,
|
|
276
|
+
BOARD_KNOWLEDGE_SEARCH_USAGE,
|
|
244
277
|
BOARD_KNOWLEDGE_FILE_USAGE
|
|
245
278
|
].join("\n")), undefined, 2);
|
|
246
279
|
}
|
|
@@ -279,7 +312,18 @@ async function main(argv) {
|
|
|
279
312
|
const result = await pluginInstall(parsed);
|
|
280
313
|
return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
|
|
281
314
|
}
|
|
282
|
-
|
|
315
|
+
if (action === "update") {
|
|
316
|
+
if (parsed.command[2] === "check") {
|
|
317
|
+
const result = await pluginUpdateCheck(parsed);
|
|
318
|
+
return output(parsed, result, renderPluginUpdateCheck, result.ok ? 0 : 2);
|
|
319
|
+
}
|
|
320
|
+
if (["codex", "opencode", "all"].includes(parsed.command[2] ?? "")) {
|
|
321
|
+
const result = await pluginUpdate(parsed);
|
|
322
|
+
return output(parsed, result, renderPluginInstall, result.ok ? 0 : 2);
|
|
323
|
+
}
|
|
324
|
+
return output(parsed, fail("invalid_args", [PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
|
|
325
|
+
}
|
|
326
|
+
return output(parsed, fail("invalid_args", [PLUGIN_LIST_USAGE, PLUGIN_INSTALL_USAGE, PLUGIN_UPDATE_CHECK_USAGE, PLUGIN_UPDATE_USAGE].join("\n")), undefined, 2);
|
|
283
327
|
}
|
|
284
328
|
if (area === "auth" && action === "login") {
|
|
285
329
|
const result = await authLogin(parsed);
|
|
@@ -288,6 +332,31 @@ async function main(argv) {
|
|
|
288
332
|
if (area === "auth" && action === "status") {
|
|
289
333
|
return output(parsed, ok(await authStatus()), renderAuthStatus);
|
|
290
334
|
}
|
|
335
|
+
if (area === "auth" && action === "device") {
|
|
336
|
+
const deviceAction = parsed.command[2] ?? "status";
|
|
337
|
+
if (deviceAction === "status") {
|
|
338
|
+
const result = await authDeviceStatus(parsed);
|
|
339
|
+
return output(parsed, result, renderAuthDeviceStatus, result.ok ? 0 : 2);
|
|
340
|
+
}
|
|
341
|
+
if (deviceAction === "list") {
|
|
342
|
+
const result = await authDeviceList(parsed);
|
|
343
|
+
return output(parsed, result, renderAuthDeviceList, result.ok ? 0 : 2);
|
|
344
|
+
}
|
|
345
|
+
if (deviceAction === "revoke") {
|
|
346
|
+
const result = await authDeviceRevoke(parsed);
|
|
347
|
+
return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
|
|
348
|
+
}
|
|
349
|
+
if (deviceAction === "rename") {
|
|
350
|
+
const result = await authDeviceRename(parsed);
|
|
351
|
+
return output(parsed, result, renderAuthDevice, result.ok ? 0 : 2);
|
|
352
|
+
}
|
|
353
|
+
return output(parsed, fail("invalid_args", [
|
|
354
|
+
AUTH_DEVICE_STATUS_USAGE,
|
|
355
|
+
AUTH_DEVICE_LIST_USAGE,
|
|
356
|
+
AUTH_DEVICE_REVOKE_USAGE,
|
|
357
|
+
AUTH_DEVICE_RENAME_USAGE
|
|
358
|
+
].join("\n")), undefined, 2);
|
|
359
|
+
}
|
|
291
360
|
if (area === "auth" && action === "logout") {
|
|
292
361
|
await rm(DEFAULT_AUTH_FILE, { force: true });
|
|
293
362
|
return output(parsed, ok(await authStatus()), renderAuthStatus);
|
|
@@ -367,6 +436,16 @@ async function main(argv) {
|
|
|
367
436
|
USAGE_EVENTS_USAGE
|
|
368
437
|
].join("\n")), undefined, 2);
|
|
369
438
|
}
|
|
439
|
+
if (area === "mcp") {
|
|
440
|
+
if (action === "log" || action === "tool-event") {
|
|
441
|
+
const body = mcpToolEventBody(parsed);
|
|
442
|
+
if (typeof body === "string") {
|
|
443
|
+
return output(parsed, fail("invalid_args", body), undefined, 2);
|
|
444
|
+
}
|
|
445
|
+
return output(parsed, await cloudPost("/v1/mcp/tool-events", body), renderMcpToolEvent);
|
|
446
|
+
}
|
|
447
|
+
return output(parsed, fail("invalid_args", MCP_TOOL_EVENT_USAGE), undefined, 2);
|
|
448
|
+
}
|
|
370
449
|
if (area === "billing") {
|
|
371
450
|
if (action === "statement") {
|
|
372
451
|
const request = billingStatementRequest(parsed);
|
|
@@ -506,6 +585,20 @@ async function main(argv) {
|
|
|
506
585
|
return output(parsed, fail("invalid_args", [IMAGE_BOOT_LOGO_COMPOSE_USAGE, IMAGE_DTB_COMPOSE_USAGE].join("\n")), undefined, 2);
|
|
507
586
|
}
|
|
508
587
|
if (area === "local") {
|
|
588
|
+
if (action === "toolchain" && parsed.command[2] === "list") {
|
|
589
|
+
const request = localToolchainListRequest(parsed);
|
|
590
|
+
if (typeof request === "string") {
|
|
591
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
592
|
+
}
|
|
593
|
+
return output(parsed, ok(await listLocalToolchainEnvironments(request)), renderLocalToolchainList);
|
|
594
|
+
}
|
|
595
|
+
if (action === "toolchain" && parsed.command[2] === "installed") {
|
|
596
|
+
const request = localToolchainListRequest(parsed, LOCAL_TOOLCHAIN_INSTALLED_USAGE);
|
|
597
|
+
if (typeof request === "string") {
|
|
598
|
+
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
599
|
+
}
|
|
600
|
+
return output(parsed, ok(await listLocalToolchainEnvironments({ ...request, installedOnly: true })), renderLocalToolchainList);
|
|
601
|
+
}
|
|
509
602
|
if (action === "toolchain" && parsed.command[2] === "latest") {
|
|
510
603
|
const request = localToolchainLatestRequest(parsed);
|
|
511
604
|
if (typeof request === "string") {
|
|
@@ -532,7 +625,7 @@ async function main(argv) {
|
|
|
532
625
|
if (typeof request === "string") {
|
|
533
626
|
return output(parsed, fail("invalid_args", request), undefined, 2);
|
|
534
627
|
}
|
|
535
|
-
return output(parsed, ok(await validateLocalToolchain(request
|
|
628
|
+
return output(parsed, ok(await validateLocalToolchain(request)), renderLocalToolchainValidation);
|
|
536
629
|
}
|
|
537
630
|
if (action === "compile" && parsed.command[2] === "taishanpi") {
|
|
538
631
|
const request = localCompileTaishanPiRequest(parsed, await authStatus());
|
|
@@ -549,6 +642,8 @@ async function main(argv) {
|
|
|
549
642
|
return output(parsed, ok(await buildTaishanPiQtSmoke(request)), renderLocalCompileResult);
|
|
550
643
|
}
|
|
551
644
|
return output(parsed, fail("invalid_args", [
|
|
645
|
+
LOCAL_TOOLCHAIN_LIST_USAGE,
|
|
646
|
+
LOCAL_TOOLCHAIN_INSTALLED_USAGE,
|
|
552
647
|
LOCAL_TOOLCHAIN_LATEST_USAGE,
|
|
553
648
|
LOCAL_TOOLCHAIN_CURRENT_USAGE,
|
|
554
649
|
LOCAL_TOOLCHAIN_INSTALL_USAGE,
|
|
@@ -1126,11 +1221,11 @@ async function doctor() {
|
|
|
1126
1221
|
const bridgeHealth = await apiDoctorCheck("bridge_health", "Local Bridge health", `${bridgeBaseUrl}/healthz`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderHealthSummary, healthStatus);
|
|
1127
1222
|
checks.push(bridgeHealth);
|
|
1128
1223
|
if (isUsableDoctorCheck(bridgeHealth)) {
|
|
1129
|
-
checks.push(await apiDoctorCheck("device_scan", "Device
|
|
1224
|
+
checks.push(await apiDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDeviceScanSummary, warningIfWarnings));
|
|
1130
1225
|
checks.push(await apiDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "bridge_unreachable", `Local Bridge is unreachable at ${bridgeBaseUrl}.`, renderDebugToolScanSummary, warningIfWarnings));
|
|
1131
1226
|
}
|
|
1132
1227
|
else {
|
|
1133
|
-
checks.push(dependentDoctorCheck("device_scan", "Device
|
|
1228
|
+
checks.push(dependentDoctorCheck("device_scan", "Device inventory", `${bridgeBaseUrl}/v1/devices`, "Device inventory requires a reachable Local Bridge."));
|
|
1134
1229
|
checks.push(dependentDoctorCheck("debug_tools", "Debug tool scan", `${bridgeBaseUrl}/v1/debug/tools`, "Debug tool scan requires a reachable Local Bridge."));
|
|
1135
1230
|
}
|
|
1136
1231
|
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 +1284,8 @@ function authDoctorCheck(status) {
|
|
|
1189
1284
|
: {
|
|
1190
1285
|
code: "auth_not_ready",
|
|
1191
1286
|
message: "No CLI auth token is configured.",
|
|
1192
|
-
remediation:
|
|
1287
|
+
remediation: cloudAuthSetupRemediation(),
|
|
1288
|
+
details: cloudAuthSetupDetails()
|
|
1193
1289
|
}
|
|
1194
1290
|
};
|
|
1195
1291
|
}
|
|
@@ -1322,7 +1418,7 @@ function warningIfWarnings(data) {
|
|
|
1322
1418
|
}
|
|
1323
1419
|
function renderDeviceScanSummary(result) {
|
|
1324
1420
|
const warningText = result.warnings?.length ? ` ${result.warnings.length} warning(s).` : "";
|
|
1325
|
-
return `Device
|
|
1421
|
+
return `Device inventory snapshot: ${result.devices.length} device(s), ${result.usb.length} USB item(s), ${result.serial.length} serial port(s).${warningText}`;
|
|
1326
1422
|
}
|
|
1327
1423
|
function renderDebugToolScanSummary(result) {
|
|
1328
1424
|
const available = result.tools.filter((tool) => tool.available).length;
|
|
@@ -1340,16 +1436,142 @@ function isApiResponse(value) {
|
|
|
1340
1436
|
return isJsonObject(error) && typeof error.code === "string" && typeof error.message === "string";
|
|
1341
1437
|
}
|
|
1342
1438
|
async function bridgeGet(path) {
|
|
1343
|
-
|
|
1344
|
-
return await response.json();
|
|
1439
|
+
return await bridgeRequest("GET", path);
|
|
1345
1440
|
}
|
|
1346
1441
|
async function bridgePost(path, body) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1442
|
+
return await bridgeRequest("POST", path, body);
|
|
1443
|
+
}
|
|
1444
|
+
async function bridgeRequest(method, path, body) {
|
|
1445
|
+
const bodyText = body === undefined ? "" : JSON.stringify(body);
|
|
1446
|
+
const makeRequest = async () => {
|
|
1447
|
+
const response = await fetch(`${DEFAULT_BRIDGE_URL}${path}`, {
|
|
1448
|
+
method,
|
|
1449
|
+
headers: bridgeHeaders(method, path, method === "POST" ? bodyText : "", method === "POST" ? { "content-type": "application/json" } : {}),
|
|
1450
|
+
body: method === "POST" ? bodyText : undefined
|
|
1451
|
+
});
|
|
1452
|
+
return await response.json();
|
|
1453
|
+
};
|
|
1454
|
+
try {
|
|
1455
|
+
return await makeRequest();
|
|
1456
|
+
}
|
|
1457
|
+
catch (error) {
|
|
1458
|
+
if (!isBridgeConnectionFailure(error)) {
|
|
1459
|
+
throw error;
|
|
1460
|
+
}
|
|
1461
|
+
const started = await ensureBridgeStartedForRequest();
|
|
1462
|
+
if (!started.ok) {
|
|
1463
|
+
return started;
|
|
1464
|
+
}
|
|
1465
|
+
return await makeRequest();
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function bridgeHeaders(method, path, bodyText, base = {}) {
|
|
1469
|
+
const token = process.env.EMBED_BRIDGE_TOKEN?.trim();
|
|
1470
|
+
if (!token) {
|
|
1471
|
+
return base;
|
|
1472
|
+
}
|
|
1473
|
+
const headers = {
|
|
1474
|
+
...base,
|
|
1475
|
+
authorization: `Bearer ${token}`
|
|
1476
|
+
};
|
|
1477
|
+
addBridgeRequestSignature(headers, method, path, bodyText, token);
|
|
1478
|
+
return headers;
|
|
1479
|
+
}
|
|
1480
|
+
function addBridgeRequestSignature(headers, method, pathWithQuery, bodyText, token) {
|
|
1481
|
+
if (process.env.EMBED_BRIDGE_SIGNING === "0") {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
1485
|
+
const nonce = randomBytes(16).toString("hex");
|
|
1486
|
+
const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
|
|
1487
|
+
const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
1488
|
+
const canonical = cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
|
|
1489
|
+
headers["x-embed-key-id"] = keyId;
|
|
1490
|
+
headers["x-embed-timestamp"] = timestamp;
|
|
1491
|
+
headers["x-embed-nonce"] = nonce;
|
|
1492
|
+
headers["x-embed-body-sha256"] = bodySha256;
|
|
1493
|
+
headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
|
|
1494
|
+
}
|
|
1495
|
+
function isBridgeConnectionFailure(error) {
|
|
1496
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1497
|
+
return message.includes("fetch failed") ||
|
|
1498
|
+
message.includes("ECONNREFUSED") ||
|
|
1499
|
+
message.includes("ECONNRESET") ||
|
|
1500
|
+
message.includes("UND_ERR_SOCKET");
|
|
1501
|
+
}
|
|
1502
|
+
async function ensureBridgeStartedForRequest() {
|
|
1503
|
+
if (process.env.EMBED_BRIDGE_AUTO_START === "0") {
|
|
1504
|
+
return fail("bridge_unavailable", `embed-local-bridge is not running at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1505
|
+
remediation: `Start it with: embed bridge start`
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
let bridgeURL;
|
|
1509
|
+
try {
|
|
1510
|
+
bridgeURL = new URL(DEFAULT_BRIDGE_URL);
|
|
1511
|
+
}
|
|
1512
|
+
catch {
|
|
1513
|
+
return fail("bridge_url_invalid", `EMBED_BRIDGE_URL is not a valid URL: ${DEFAULT_BRIDGE_URL}`);
|
|
1514
|
+
}
|
|
1515
|
+
if (!isLocalBridgeURL(bridgeURL)) {
|
|
1516
|
+
return fail("bridge_unavailable", `embed-local-bridge is not reachable at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1517
|
+
remediation: `Start the bridge for that host, or set EMBED_BRIDGE_URL to a local bridge URL.`
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
const launcher = await resolveBridgeLauncher();
|
|
1521
|
+
const host = bridgeURL.hostname === "::1" ? "::1" : bridgeURL.hostname || "127.0.0.1";
|
|
1522
|
+
const port = bridgeURL.port || "18083";
|
|
1523
|
+
const env = {
|
|
1524
|
+
...process.env,
|
|
1525
|
+
EMBED_BRIDGE_HOST: host,
|
|
1526
|
+
EMBED_BRIDGE_PORT: port
|
|
1527
|
+
};
|
|
1528
|
+
const child = spawn(launcher.command, [...launcher.args, "--host", host, "--port", port], {
|
|
1529
|
+
cwd: process.cwd(),
|
|
1530
|
+
detached: true,
|
|
1531
|
+
stdio: "ignore",
|
|
1532
|
+
env
|
|
1351
1533
|
});
|
|
1352
|
-
|
|
1534
|
+
child.unref();
|
|
1535
|
+
const ready = await waitForBridgeHealth(bridgeURL, 8000);
|
|
1536
|
+
if (!ready.ok) {
|
|
1537
|
+
return ready;
|
|
1538
|
+
}
|
|
1539
|
+
return ok({
|
|
1540
|
+
started: true,
|
|
1541
|
+
bridge_url: DEFAULT_BRIDGE_URL,
|
|
1542
|
+
command: launcher.command
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
function isLocalBridgeURL(url) {
|
|
1546
|
+
const host = url.hostname.toLowerCase();
|
|
1547
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1548
|
+
}
|
|
1549
|
+
async function waitForBridgeHealth(bridgeURL, timeoutMs) {
|
|
1550
|
+
const deadline = Date.now() + timeoutMs;
|
|
1551
|
+
let lastError = "";
|
|
1552
|
+
while (Date.now() < deadline) {
|
|
1553
|
+
try {
|
|
1554
|
+
const response = await fetch(new URL("/healthz", bridgeURL), {
|
|
1555
|
+
headers: bridgeHeaders("GET", "/healthz", "")
|
|
1556
|
+
});
|
|
1557
|
+
const parsed = await response.json();
|
|
1558
|
+
if (parsed.ok) {
|
|
1559
|
+
return parsed;
|
|
1560
|
+
}
|
|
1561
|
+
lastError = parsed.error?.message ?? `HTTP ${response.status}`;
|
|
1562
|
+
}
|
|
1563
|
+
catch (error) {
|
|
1564
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
1565
|
+
}
|
|
1566
|
+
await delay(100);
|
|
1567
|
+
}
|
|
1568
|
+
return fail("bridge_start_failed", `embed-local-bridge did not become healthy at ${DEFAULT_BRIDGE_URL}.`, {
|
|
1569
|
+
remediation: `Run embed bridge start in a separate terminal and retry.`,
|
|
1570
|
+
details: { last_error: lastError }
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
function delay(ms) {
|
|
1574
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
1353
1575
|
}
|
|
1354
1576
|
async function cloudGet(path) {
|
|
1355
1577
|
return await cloudRequest("GET", path);
|
|
@@ -1360,16 +1582,23 @@ async function cloudPost(path, body) {
|
|
|
1360
1582
|
async function cloudDownloadArtifact(artifactId, outputPath) {
|
|
1361
1583
|
try {
|
|
1362
1584
|
const headers = {};
|
|
1363
|
-
const
|
|
1364
|
-
if (token) {
|
|
1365
|
-
|
|
1585
|
+
const auth = await cloudAuthConfig();
|
|
1586
|
+
if (auth.token) {
|
|
1587
|
+
if (auth.device) {
|
|
1588
|
+
const integrity = await validateLocalDeviceIntegrity(auth.device);
|
|
1589
|
+
if (!integrity.ok) {
|
|
1590
|
+
return integrity;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
headers.authorization = `Bearer ${auth.token}`;
|
|
1594
|
+
addCloudRequestSignature(headers, "GET", `/v1/artifacts/${encodeURIComponent(artifactId)}/download`, "", auth.token, auth.device);
|
|
1366
1595
|
}
|
|
1367
1596
|
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/artifacts/${encodeURIComponent(artifactId)}/download`, {
|
|
1368
1597
|
headers: Object.keys(headers).length > 0 ? headers : undefined
|
|
1369
1598
|
});
|
|
1370
1599
|
if (!response.ok) {
|
|
1371
1600
|
const parsed = await parseErrorResponse(response);
|
|
1372
|
-
return parsed
|
|
1601
|
+
return parsed ? enrichCloudAuthFailure(parsed, Boolean(auth.token)) : fail("artifact_download_failed", `Artifact download failed with HTTP ${response.status}.`);
|
|
1373
1602
|
}
|
|
1374
1603
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
1375
1604
|
const expectedSha256 = response.headers.get("x-embed-artifact-sha256")?.trim();
|
|
@@ -1398,19 +1627,28 @@ async function cloudDownloadArtifact(artifactId, outputPath) {
|
|
|
1398
1627
|
async function cloudRequest(method, path, body) {
|
|
1399
1628
|
try {
|
|
1400
1629
|
const headers = {};
|
|
1401
|
-
|
|
1630
|
+
const bodyText = body === undefined ? "" : JSON.stringify(body);
|
|
1631
|
+
if (bodyText) {
|
|
1402
1632
|
headers["content-type"] = "application/json";
|
|
1403
1633
|
}
|
|
1404
|
-
const
|
|
1405
|
-
if (token) {
|
|
1406
|
-
|
|
1634
|
+
const auth = await cloudAuthConfig();
|
|
1635
|
+
if (auth.token) {
|
|
1636
|
+
if (auth.device) {
|
|
1637
|
+
const integrity = await validateLocalDeviceIntegrity(auth.device);
|
|
1638
|
+
if (!integrity.ok) {
|
|
1639
|
+
return integrity;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
headers.authorization = `Bearer ${auth.token}`;
|
|
1643
|
+
addCloudRequestSignature(headers, method, path, bodyText, auth.token, auth.device);
|
|
1407
1644
|
}
|
|
1408
1645
|
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}${path}`, {
|
|
1409
1646
|
method,
|
|
1410
1647
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
1411
|
-
body: body === undefined ? undefined :
|
|
1648
|
+
body: body === undefined ? undefined : bodyText
|
|
1412
1649
|
});
|
|
1413
|
-
|
|
1650
|
+
const parsed = await response.json();
|
|
1651
|
+
return enrichCloudAuthFailure(parsed, Boolean(auth.token));
|
|
1414
1652
|
}
|
|
1415
1653
|
catch (error) {
|
|
1416
1654
|
return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
|
|
@@ -1418,6 +1656,129 @@ async function cloudRequest(method, path, body) {
|
|
|
1418
1656
|
});
|
|
1419
1657
|
}
|
|
1420
1658
|
}
|
|
1659
|
+
async function validateLocalDeviceIntegrity(device) {
|
|
1660
|
+
const current = await localHardwareFingerprint();
|
|
1661
|
+
if (current.fingerprint_hash === device.fingerprint_hash) {
|
|
1662
|
+
return ok(undefined);
|
|
1663
|
+
}
|
|
1664
|
+
return fail("tool_integrity_check_failed", TOOL_INTEGRITY_RELOGIN_MESSAGE, {
|
|
1665
|
+
remediation: [
|
|
1666
|
+
"当前 Embed Labs CLI/插件配置绑定的电脑与本机硬件唯一码不一致。",
|
|
1667
|
+
TOOL_INTEGRITY_RELOGIN_MESSAGE,
|
|
1668
|
+
"如果账号设备数量已达上限,请先在原电脑或用户后台撤销旧设备。"
|
|
1669
|
+
].join("\n"),
|
|
1670
|
+
details: {
|
|
1671
|
+
expected_fingerprint_hash: device.fingerprint_hash,
|
|
1672
|
+
current_fingerprint_hash: current.fingerprint_hash,
|
|
1673
|
+
platform: current.platform,
|
|
1674
|
+
arch: current.arch,
|
|
1675
|
+
fingerprint_source: current.source
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
function enrichCloudAuthFailure(response, hadToken) {
|
|
1680
|
+
if (response.ok) {
|
|
1681
|
+
return response;
|
|
1682
|
+
}
|
|
1683
|
+
if (response.error.code.startsWith("device_") || response.error.code.startsWith("request_signature_")) {
|
|
1684
|
+
return fail(response.error.code, response.error.message, {
|
|
1685
|
+
remediation: [
|
|
1686
|
+
"This computer is not fully registered for the configured Embed Labs API Token.",
|
|
1687
|
+
"Run: embedlabs auth login --token <your_token>",
|
|
1688
|
+
"Then verify with: embedlabs auth device status",
|
|
1689
|
+
"If the account already has too many devices, revoke one with: embedlabs auth device revoke <device_id>"
|
|
1690
|
+
].join("\n"),
|
|
1691
|
+
details: response.error.details
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
if (response.error.code !== "unauthorized") {
|
|
1695
|
+
return response;
|
|
1696
|
+
}
|
|
1697
|
+
if (!hadToken) {
|
|
1698
|
+
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.", {
|
|
1699
|
+
remediation: cloudAuthSetupRemediation(),
|
|
1700
|
+
details: cloudAuthSetupDetails()
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
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.", {
|
|
1704
|
+
remediation: cloudAuthSetupRemediation(),
|
|
1705
|
+
details: cloudAuthSetupDetails()
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
function cloudAuthSetupRemediation() {
|
|
1709
|
+
return [
|
|
1710
|
+
`1. Open ${DEFAULT_DASHBOARD_URL} and register or sign in.`,
|
|
1711
|
+
"2. Create or copy your Embed Labs API Token from the user dashboard.",
|
|
1712
|
+
"3. Run: embedlabs auth login --token <your_token>",
|
|
1713
|
+
"4. For automation, set: EMBED_API_TOKEN=<your_token>",
|
|
1714
|
+
"5. Verify with: embedlabs auth status"
|
|
1715
|
+
].join("\n");
|
|
1716
|
+
}
|
|
1717
|
+
function cloudAuthSetupDetails() {
|
|
1718
|
+
return {
|
|
1719
|
+
dashboard_url: DEFAULT_DASHBOARD_URL,
|
|
1720
|
+
login_command: "embedlabs auth login --token <your_token>",
|
|
1721
|
+
env_var: "EMBED_API_TOKEN",
|
|
1722
|
+
auth_status_command: "embedlabs auth status",
|
|
1723
|
+
auth_file: DEFAULT_AUTH_FILE
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function addCloudRequestSignature(headers, method, pathWithQuery, bodyText, token, device) {
|
|
1727
|
+
if (process.env.EMBED_CLOUD_API_SIGNING === "0") {
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
1731
|
+
const nonce = randomBytes(16).toString("hex");
|
|
1732
|
+
const bodySha256 = createHash("sha256").update(bodyText).digest("hex");
|
|
1733
|
+
const keyId = createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
1734
|
+
const canonical = device
|
|
1735
|
+
? cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, EMBED_CLIENT_NAME, EMBED_CLIENT_VERSION)
|
|
1736
|
+
: cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256);
|
|
1737
|
+
headers["x-embed-key-id"] = keyId;
|
|
1738
|
+
headers["x-embed-timestamp"] = timestamp;
|
|
1739
|
+
headers["x-embed-nonce"] = nonce;
|
|
1740
|
+
headers["x-embed-body-sha256"] = bodySha256;
|
|
1741
|
+
if (device) {
|
|
1742
|
+
headers["x-embed-signature-version"] = "v2";
|
|
1743
|
+
headers["x-embed-device-id"] = device.device_id;
|
|
1744
|
+
headers["x-embed-device-fingerprint-sha256"] = device.fingerprint_hash;
|
|
1745
|
+
headers["x-embed-client-name"] = EMBED_CLIENT_NAME;
|
|
1746
|
+
headers["x-embed-client-version"] = EMBED_CLIENT_VERSION;
|
|
1747
|
+
headers["x-embed-device-signature"] = signCrypto(null, Buffer.from(canonical), device.private_key_pem).toString("base64url");
|
|
1748
|
+
}
|
|
1749
|
+
headers["x-embed-signature"] = createHmac("sha256", token).update(canonical).digest("hex");
|
|
1750
|
+
}
|
|
1751
|
+
function cloudRequestCanonicalString(method, pathWithQuery, timestamp, nonce, bodySha256) {
|
|
1752
|
+
return [
|
|
1753
|
+
method.toUpperCase(),
|
|
1754
|
+
normalizeCloudPathForSignature(pathWithQuery),
|
|
1755
|
+
timestamp,
|
|
1756
|
+
nonce,
|
|
1757
|
+
bodySha256
|
|
1758
|
+
].join("\n");
|
|
1759
|
+
}
|
|
1760
|
+
function cloudRequestCanonicalStringV2(method, pathWithQuery, timestamp, nonce, bodySha256, device, clientName, clientVersion) {
|
|
1761
|
+
return [
|
|
1762
|
+
method.toUpperCase(),
|
|
1763
|
+
normalizeCloudPathForSignature(pathWithQuery),
|
|
1764
|
+
timestamp,
|
|
1765
|
+
nonce,
|
|
1766
|
+
bodySha256,
|
|
1767
|
+
device.device_id,
|
|
1768
|
+
device.fingerprint_hash,
|
|
1769
|
+
clientName,
|
|
1770
|
+
clientVersion
|
|
1771
|
+
].join("\n");
|
|
1772
|
+
}
|
|
1773
|
+
function normalizeCloudPathForSignature(pathWithQuery) {
|
|
1774
|
+
try {
|
|
1775
|
+
const parsed = new URL(pathWithQuery, "http://embed.local");
|
|
1776
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
1777
|
+
}
|
|
1778
|
+
catch {
|
|
1779
|
+
return pathWithQuery.startsWith("/") ? pathWithQuery : `/${pathWithQuery}`;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1421
1782
|
async function pluginList(parsed) {
|
|
1422
1783
|
const releaseDir = stringFlag(parsed, "release-dir");
|
|
1423
1784
|
const manifest = releaseDir ? await readPluginReleaseManifest(releaseDir) : undefined;
|
|
@@ -1517,13 +1878,112 @@ async function pluginInstall(parsed) {
|
|
|
1517
1878
|
await rm(tempDir, { recursive: true, force: true });
|
|
1518
1879
|
}
|
|
1519
1880
|
}
|
|
1881
|
+
async function pluginUpdateCheck(parsed) {
|
|
1882
|
+
const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
|
|
1883
|
+
if (unknownFlag) {
|
|
1884
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
|
|
1885
|
+
}
|
|
1886
|
+
const unexpected = parsed.command.slice(3);
|
|
1887
|
+
if (unexpected.length > 0) {
|
|
1888
|
+
return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_CHECK_USAGE}`);
|
|
1889
|
+
}
|
|
1890
|
+
const remoteManifest = await fetchRemotePluginManifest(parsed);
|
|
1891
|
+
if (!remoteManifest.ok) {
|
|
1892
|
+
return remoteManifest;
|
|
1893
|
+
}
|
|
1894
|
+
const manifest = remoteManifest.data;
|
|
1895
|
+
const codexPackage = manifest.packages?.find((item) => item.id === "codex-embed-labs");
|
|
1896
|
+
const opencodePackage = manifest.packages?.find((item) => item.id === "opencode-embed-labs");
|
|
1897
|
+
const codexTarget = join(codexPluginTargetRoot(parsed, true), CODEX_PLUGIN_NAME);
|
|
1898
|
+
const openCodeTarget = openCodePluginTargetRoot(parsed, true);
|
|
1899
|
+
return ok({
|
|
1900
|
+
release_url: pluginReleaseBaseUrl(parsed),
|
|
1901
|
+
latest_version: manifest.version,
|
|
1902
|
+
release_notes: normalizedReleaseNotes(manifest.release_notes),
|
|
1903
|
+
plugins: [
|
|
1904
|
+
await pluginUpdateItem({
|
|
1905
|
+
id: "codex",
|
|
1906
|
+
displayName: "Embed Labs Codex plugin",
|
|
1907
|
+
targetPath: codexTarget,
|
|
1908
|
+
installedVersion: await installedCodexPluginVersion(codexTarget),
|
|
1909
|
+
latestVersion: codexPackage?.version ?? manifest.version,
|
|
1910
|
+
releaseFile: codexPackage?.file,
|
|
1911
|
+
updateCommand: "embedlabs plugin update codex"
|
|
1912
|
+
}),
|
|
1913
|
+
await pluginUpdateItem({
|
|
1914
|
+
id: "opencode",
|
|
1915
|
+
displayName: "Embed Labs OpenCode plugin",
|
|
1916
|
+
targetPath: openCodeTarget,
|
|
1917
|
+
installedVersion: await installedOpenCodePluginVersion(openCodeTarget),
|
|
1918
|
+
latestVersion: opencodePackage?.version ?? manifest.version,
|
|
1919
|
+
releaseFile: opencodePackage?.file,
|
|
1920
|
+
updateCommand: "embedlabs plugin update opencode"
|
|
1921
|
+
})
|
|
1922
|
+
]
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
function normalizedReleaseNotes(notes) {
|
|
1926
|
+
if (!Array.isArray(notes)) {
|
|
1927
|
+
return [];
|
|
1928
|
+
}
|
|
1929
|
+
return notes
|
|
1930
|
+
.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
1931
|
+
.map((item) => item.trim());
|
|
1932
|
+
}
|
|
1933
|
+
async function pluginUpdate(parsed) {
|
|
1934
|
+
const unknownFlag = firstUnknownFlag(parsed, ["release-url", "target", "codex-target", "opencode-target", "json"]);
|
|
1935
|
+
if (unknownFlag) {
|
|
1936
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${PLUGIN_UPDATE_USAGE}`);
|
|
1937
|
+
}
|
|
1938
|
+
const target = parsed.command[2];
|
|
1939
|
+
if (!target || !["codex", "opencode", "all"].includes(target)) {
|
|
1940
|
+
return fail("invalid_args", PLUGIN_UPDATE_USAGE);
|
|
1941
|
+
}
|
|
1942
|
+
const unexpected = parsed.command.slice(3);
|
|
1943
|
+
if (unexpected.length > 0) {
|
|
1944
|
+
return fail("invalid_args", `Unexpected argument: ${unexpected[0]}. ${PLUGIN_UPDATE_USAGE}`);
|
|
1945
|
+
}
|
|
1946
|
+
const installParsed = {
|
|
1947
|
+
...parsed,
|
|
1948
|
+
command: ["plugin", "install", target],
|
|
1949
|
+
flags: { ...parsed.flags, force: true }
|
|
1950
|
+
};
|
|
1951
|
+
return await pluginInstall(installParsed);
|
|
1952
|
+
}
|
|
1953
|
+
async function pluginUpdateItem(input) {
|
|
1954
|
+
const installed = !!input.installedVersion;
|
|
1955
|
+
const updateAvailable = !!input.latestVersion && input.installedVersion !== input.latestVersion;
|
|
1956
|
+
const notes = [];
|
|
1957
|
+
if (!installed) {
|
|
1958
|
+
notes.push("Plugin is not installed in the selected target.");
|
|
1959
|
+
}
|
|
1960
|
+
else if (updateAvailable) {
|
|
1961
|
+
notes.push("A newer plugin release is available. Run the update command, then restart Codex/OpenCode.");
|
|
1962
|
+
}
|
|
1963
|
+
else {
|
|
1964
|
+
notes.push("Plugin is up to date for the selected release channel.");
|
|
1965
|
+
}
|
|
1966
|
+
return {
|
|
1967
|
+
id: input.id,
|
|
1968
|
+
display_name: input.displayName,
|
|
1969
|
+
installed,
|
|
1970
|
+
installed_version: input.installedVersion,
|
|
1971
|
+
latest_version: input.latestVersion,
|
|
1972
|
+
update_available: updateAvailable,
|
|
1973
|
+
target_path: input.targetPath,
|
|
1974
|
+
release_file: input.releaseFile,
|
|
1975
|
+
update_command: input.updateCommand,
|
|
1976
|
+
notes
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1520
1979
|
async function installCodexPlugin(parsed, context) {
|
|
1521
1980
|
const source = await resolveCodexPluginSource(context);
|
|
1522
1981
|
if (!source.ok) {
|
|
1523
1982
|
return source;
|
|
1524
1983
|
}
|
|
1525
1984
|
const targetRoot = codexPluginTargetRoot(parsed, context.installingAll);
|
|
1526
|
-
const targetPath = join(targetRoot,
|
|
1985
|
+
const targetPath = join(targetRoot, CODEX_PLUGIN_NAME);
|
|
1986
|
+
const legacyCleanup = await cleanupLegacyCodexPluginRemnants(targetRoot);
|
|
1527
1987
|
if (await pathExists(targetPath) && !booleanFlag(parsed, "force")) {
|
|
1528
1988
|
return fail("plugin_already_installed", `Codex plugin already exists at ${targetPath}.`, {
|
|
1529
1989
|
remediation: "Pass --force to replace it, or pass --codex-target/--target to install into a different directory."
|
|
@@ -1533,16 +1993,24 @@ async function installCodexPlugin(parsed, context) {
|
|
|
1533
1993
|
await mkdir(targetRoot, { recursive: true });
|
|
1534
1994
|
await cp(source.data.sourcePath, targetPath, { recursive: true });
|
|
1535
1995
|
const mcpRegistration = await maybeRegisterCodexMcp(parsed, targetRoot, targetPath);
|
|
1996
|
+
const marketplaceRegistration = await maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath);
|
|
1536
1997
|
return ok({
|
|
1537
1998
|
id: "codex",
|
|
1538
1999
|
target_path: targetPath,
|
|
1539
2000
|
source: source.data.sourceLabel,
|
|
1540
2001
|
version: source.data.version,
|
|
1541
2002
|
command_hint: mcpRegistration.registered
|
|
1542
|
-
?
|
|
2003
|
+
? (marketplaceRegistration.registered
|
|
2004
|
+
? "Codex MCP and plugin marketplace entry were registered. Fully restart Codex to reload @Embed Labs."
|
|
2005
|
+
: "Codex MCP was registered. Start a new Codex session to reload tools.")
|
|
1543
2006
|
: mcpRegistration.hint,
|
|
2007
|
+
warning: legacyCodexCleanupWarning(legacyCleanup),
|
|
1544
2008
|
mcp_registered: mcpRegistration.registered,
|
|
1545
|
-
mcp_warning: mcpRegistration.warning
|
|
2009
|
+
mcp_warning: mcpRegistration.warning,
|
|
2010
|
+
marketplace_registered: marketplaceRegistration.registered,
|
|
2011
|
+
marketplace_path: marketplaceRegistration.marketplacePath,
|
|
2012
|
+
marketplace_warning: marketplaceRegistration.warning,
|
|
2013
|
+
cleanup: legacyCleanup
|
|
1546
2014
|
});
|
|
1547
2015
|
}
|
|
1548
2016
|
async function installOpenCodePlugin(parsed, context) {
|
|
@@ -1551,8 +2019,10 @@ async function installOpenCodePlugin(parsed, context) {
|
|
|
1551
2019
|
return source;
|
|
1552
2020
|
}
|
|
1553
2021
|
const targetRoot = openCodePluginTargetRoot(parsed, context.installingAll);
|
|
1554
|
-
const
|
|
1555
|
-
|
|
2022
|
+
const globalInstall = isGlobalOpenCodeRoot(targetRoot);
|
|
2023
|
+
const wrapperPath = join(targetRoot, "plugins", "embed-labs.js");
|
|
2024
|
+
const legacyCleanup = await cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall);
|
|
2025
|
+
if (!globalInstall && await pathExists(wrapperPath) && !booleanFlag(parsed, "force")) {
|
|
1556
2026
|
return fail("plugin_already_installed", `OpenCode plugin wrapper already exists at ${wrapperPath}.`, {
|
|
1557
2027
|
remediation: "Pass --force to replace it, or pass --opencode-target/--target to install into a different directory."
|
|
1558
2028
|
});
|
|
@@ -1578,15 +2048,25 @@ async function installOpenCodePlugin(parsed, context) {
|
|
|
1578
2048
|
});
|
|
1579
2049
|
}
|
|
1580
2050
|
await ensureOpenCodeInstallPackageJson(targetRoot);
|
|
1581
|
-
|
|
2051
|
+
if (globalInstall) {
|
|
2052
|
+
await rm(wrapperPath, { force: true });
|
|
2053
|
+
legacyCleanup.legacy_removed_config_entries?.push(...await ensureOpenCodeGlobalPluginConfig());
|
|
2054
|
+
}
|
|
2055
|
+
else {
|
|
2056
|
+
await writeFile(wrapperPath, `export { default, DevelopmentBoardToolchainPlugin } from "embed-labs";\n`, "utf8");
|
|
2057
|
+
}
|
|
1582
2058
|
const duplicateWarning = await openCodeDuplicatePluginWarning(targetRoot);
|
|
2059
|
+
const cleanupWarning = legacyOpenCodeCleanupWarning(legacyCleanup);
|
|
1583
2060
|
return ok({
|
|
1584
2061
|
id: "opencode",
|
|
1585
2062
|
target_path: targetRoot,
|
|
1586
2063
|
source: source.data.sourceLabel,
|
|
1587
2064
|
version: source.data.version,
|
|
1588
|
-
command_hint:
|
|
1589
|
-
|
|
2065
|
+
command_hint: globalInstall
|
|
2066
|
+
? "Restart OpenCode so the global embed-labs package plugin is reloaded."
|
|
2067
|
+
: "Start OpenCode from the project containing this .opencode directory.",
|
|
2068
|
+
warning: combineWarnings(cleanupWarning, duplicateWarning),
|
|
2069
|
+
cleanup: legacyCleanup
|
|
1590
2070
|
});
|
|
1591
2071
|
}
|
|
1592
2072
|
async function resolveCodexPluginSource(context) {
|
|
@@ -1800,7 +2280,458 @@ function openCodePluginTargetRoot(parsed, installingAll) {
|
|
|
1800
2280
|
return resolve(target ?? defaultOpenCodeRoot());
|
|
1801
2281
|
}
|
|
1802
2282
|
function defaultCodexPluginRoot() {
|
|
1803
|
-
return join(
|
|
2283
|
+
return join(defaultCodexHome(), "plugins");
|
|
2284
|
+
}
|
|
2285
|
+
function defaultCodexHome() {
|
|
2286
|
+
return resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
2287
|
+
}
|
|
2288
|
+
function codexConfigPath() {
|
|
2289
|
+
return join(defaultCodexHome(), "config.toml");
|
|
2290
|
+
}
|
|
2291
|
+
async function cleanupLegacyCodexPluginRemnants(targetRoot) {
|
|
2292
|
+
const removedPaths = [];
|
|
2293
|
+
const removedConfigTables = [];
|
|
2294
|
+
const warnings = [];
|
|
2295
|
+
const stoppedProcesses = await stopLegacyCodexPluginProcesses(warnings);
|
|
2296
|
+
const legacyPaths = [
|
|
2297
|
+
join(targetRoot, "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME)
|
|
2298
|
+
];
|
|
2299
|
+
for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
|
|
2300
|
+
legacyPaths.push(join(targetRoot, "cache", marketplaceName, CODEX_PLUGIN_NAME));
|
|
2301
|
+
}
|
|
2302
|
+
for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
|
|
2303
|
+
legacyPaths.push(join(targetRoot, pluginName));
|
|
2304
|
+
legacyPaths.push(join(targetRoot, "cache", pluginName));
|
|
2305
|
+
for (const marketplaceName of [CODEX_MARKETPLACE_NAME, ...LEGACY_CODEX_MARKETPLACE_NAMES]) {
|
|
2306
|
+
legacyPaths.push(join(targetRoot, "cache", marketplaceName, pluginName));
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
legacyPaths.push(...await discoverLegacyCodexCachePaths(targetRoot));
|
|
2310
|
+
if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
|
|
2311
|
+
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"));
|
|
2312
|
+
legacyPaths.push(...legacyCodexLocalMarketplacePaths(), ...await discoverLegacyHomeAgentsMarketplacePaths(warnings), ...await discoverLegacyCodexProjectMarketplacePaths(warnings), ...legacyDevelopmentBoardRuntimePluginPaths());
|
|
2313
|
+
}
|
|
2314
|
+
for (const candidate of legacyPaths) {
|
|
2315
|
+
try {
|
|
2316
|
+
if (await pathExists(candidate)) {
|
|
2317
|
+
await rm(candidate, { recursive: true, force: true });
|
|
2318
|
+
removedPaths.push(candidate);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
catch (error) {
|
|
2322
|
+
warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
if (resolve(targetRoot) === resolve(defaultCodexPluginRoot())) {
|
|
2326
|
+
const configPath = codexConfigPath();
|
|
2327
|
+
try {
|
|
2328
|
+
if (await pathExists(configPath)) {
|
|
2329
|
+
const current = await readFile(configPath, "utf8");
|
|
2330
|
+
const updated = removeLegacyCodexConfigTables(current);
|
|
2331
|
+
if (updated.text !== current) {
|
|
2332
|
+
await writeFile(configPath, updated.text, "utf8");
|
|
2333
|
+
}
|
|
2334
|
+
removedConfigTables.push(...updated.removedTables);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
catch (error) {
|
|
2338
|
+
warnings.push(`Could not update ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
const removedHistoryEntries = await cleanupLegacyCodexTextState(warnings);
|
|
2342
|
+
return {
|
|
2343
|
+
legacy_removed_paths: Array.from(new Set(removedPaths)),
|
|
2344
|
+
legacy_removed_config_tables: removedConfigTables,
|
|
2345
|
+
legacy_removed_history_entries: removedHistoryEntries,
|
|
2346
|
+
legacy_stopped_processes: stoppedProcesses,
|
|
2347
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
async function stopLegacyCodexPluginProcesses(warnings) {
|
|
2351
|
+
if (process.platform === "win32")
|
|
2352
|
+
return 0;
|
|
2353
|
+
try {
|
|
2354
|
+
const ps = await runLocalProcess("ps", ["-axo", "pid=,command="]);
|
|
2355
|
+
if (ps.code !== 0)
|
|
2356
|
+
return 0;
|
|
2357
|
+
let stopped = 0;
|
|
2358
|
+
for (const line of ps.stdout.split("\n")) {
|
|
2359
|
+
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
2360
|
+
if (!match)
|
|
2361
|
+
continue;
|
|
2362
|
+
const pid = Number(match[1]);
|
|
2363
|
+
const command = match[2] || "";
|
|
2364
|
+
if (!isLegacyCodexPluginProcess(command))
|
|
2365
|
+
continue;
|
|
2366
|
+
try {
|
|
2367
|
+
process.kill(pid, "SIGTERM");
|
|
2368
|
+
stopped += 1;
|
|
2369
|
+
}
|
|
2370
|
+
catch (error) {
|
|
2371
|
+
warnings.push(`Could not stop legacy Codex plugin process ${pid}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
return stopped;
|
|
2375
|
+
}
|
|
2376
|
+
catch (error) {
|
|
2377
|
+
warnings.push(`Could not scan legacy Codex plugin processes: ${error instanceof Error ? error.message : String(error)}`);
|
|
2378
|
+
return 0;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
function isLegacyCodexPluginProcess(command) {
|
|
2382
|
+
const trimmed = command.trim();
|
|
2383
|
+
return /^\/.*\/dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
|
|
2384
|
+
|| /^dbt-agent-mcp-bridge(?:\s|$)/.test(trimmed)
|
|
2385
|
+
|| legacyDevelopmentBoardRuntimeProcessPatterns().some((pattern) => pattern.test(trimmed));
|
|
2386
|
+
}
|
|
2387
|
+
function legacyDevelopmentBoardRuntimeProcessPatterns() {
|
|
2388
|
+
const home = escapeRegExp(homedir());
|
|
2389
|
+
return [
|
|
2390
|
+
new RegExp(`^${home}/Library/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
|
|
2391
|
+
new RegExp(`^${home}/Library/Application Support/development-board-toolchain/agent/bin/dbt-agentd(?:\\s|$)`),
|
|
2392
|
+
new RegExp(`^${home}/Library/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
|
|
2393
|
+
new RegExp(`^${home}/Library/Application Support/development-board-toolchain/runtime/dbtctl\\s+status(?:\\s|$)`),
|
|
2394
|
+
new RegExp(`^${home}/.*?/DBT-Agent\\.app/Contents/MacOS/DBT-Agent(?:\\s|$)`)
|
|
2395
|
+
];
|
|
2396
|
+
}
|
|
2397
|
+
function legacyDevelopmentBoardRuntimePluginPaths() {
|
|
2398
|
+
return [
|
|
2399
|
+
join(homedir(), "Library", "development-board-toolchain", "runtime", "editor_plugins"),
|
|
2400
|
+
join(homedir(), "Library", "development-board-toolchain", "runtime", "opencode_plugin"),
|
|
2401
|
+
join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "editor_plugins"),
|
|
2402
|
+
join(homedir(), "Library", "Application Support", "development-board-toolchain", "runtime", "opencode_plugin")
|
|
2403
|
+
];
|
|
2404
|
+
}
|
|
2405
|
+
function legacyCodexLocalMarketplacePaths() {
|
|
2406
|
+
return Array.from(LEGACY_CODEX_MARKETPLACE_NAMES)
|
|
2407
|
+
.filter((name) => name !== CODEX_MARKETPLACE_NAME)
|
|
2408
|
+
.map((name) => join(defaultCodexHome(), "local-marketplaces", name));
|
|
2409
|
+
}
|
|
2410
|
+
async function discoverLegacyHomeAgentsMarketplacePaths(warnings) {
|
|
2411
|
+
const paths = [];
|
|
2412
|
+
const pluginRoot = join(homedir(), ".agents", "plugins");
|
|
2413
|
+
try {
|
|
2414
|
+
const entries = await readdir(pluginRoot, { withFileTypes: true });
|
|
2415
|
+
for (const entry of entries) {
|
|
2416
|
+
if (!entry.isFile() || !entry.name.startsWith("marketplace.json"))
|
|
2417
|
+
continue;
|
|
2418
|
+
const filePath = join(pluginRoot, entry.name);
|
|
2419
|
+
try {
|
|
2420
|
+
const current = await readFile(filePath, "utf8");
|
|
2421
|
+
if (isLegacyHomeAgentsMarketplace(current)) {
|
|
2422
|
+
paths.push(filePath);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
catch (error) {
|
|
2426
|
+
warnings.push(`Could not inspect legacy Codex home marketplace ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
catch {
|
|
2431
|
+
return paths;
|
|
2432
|
+
}
|
|
2433
|
+
return paths;
|
|
2434
|
+
}
|
|
2435
|
+
function isLegacyHomeAgentsMarketplace(text) {
|
|
2436
|
+
return marketplaceTextHasLegacyCodexPlugin(text);
|
|
2437
|
+
}
|
|
2438
|
+
async function discoverLegacyCodexProjectMarketplacePaths(warnings) {
|
|
2439
|
+
const configPath = codexConfigPath();
|
|
2440
|
+
let text = "";
|
|
2441
|
+
try {
|
|
2442
|
+
text = await readFile(configPath, "utf8");
|
|
2443
|
+
}
|
|
2444
|
+
catch {
|
|
2445
|
+
return [];
|
|
2446
|
+
}
|
|
2447
|
+
const paths = new Set();
|
|
2448
|
+
for (const projectPath of legacyCodexProjectPathsFromConfig(text)) {
|
|
2449
|
+
for (const candidate of [
|
|
2450
|
+
join(projectPath, ".agents", "plugins", "marketplace.json"),
|
|
2451
|
+
join(projectPath, "platform_plugin", ".agents", "plugins", "marketplace.json"),
|
|
2452
|
+
join(projectPath, "platform_plugins", "codex_plugin", ".agents", "plugins", "marketplace.json")
|
|
2453
|
+
]) {
|
|
2454
|
+
try {
|
|
2455
|
+
if (!await pathExists(candidate))
|
|
2456
|
+
continue;
|
|
2457
|
+
const current = await readFile(candidate, "utf8");
|
|
2458
|
+
if (marketplaceTextHasLegacyCodexPlugin(current)) {
|
|
2459
|
+
paths.add(candidate);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
catch (error) {
|
|
2463
|
+
warnings.push(`Could not inspect legacy Codex project marketplace ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return Array.from(paths);
|
|
2468
|
+
}
|
|
2469
|
+
function legacyCodexProjectPathsFromConfig(text) {
|
|
2470
|
+
const paths = [];
|
|
2471
|
+
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
2472
|
+
for (const line of lines) {
|
|
2473
|
+
const table = parseTomlTableHeader(line);
|
|
2474
|
+
const match = table ? /^projects\."([^"]+)"$/.exec(table) : undefined;
|
|
2475
|
+
if (match?.[1] && /DBT-Agent-Project|development-board-toolchain|dbt-agent/i.test(match[1])) {
|
|
2476
|
+
paths.push(match[1].replace(/\\"/g, '"'));
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return paths;
|
|
2480
|
+
}
|
|
2481
|
+
function marketplaceTextHasLegacyCodexPlugin(text) {
|
|
2482
|
+
try {
|
|
2483
|
+
const parsed = JSON.parse(text);
|
|
2484
|
+
const marketplaceName = typeof parsed.name === "string" ? parsed.name : "";
|
|
2485
|
+
const marketplaceDisplayName = typeof parsed.interface?.displayName === "string" ? parsed.interface.displayName : "";
|
|
2486
|
+
const marketplaceLooksLegacy = legacyTextHasCodexPluginResidue(marketplaceName)
|
|
2487
|
+
|| legacyTextHasCodexPluginResidue(marketplaceDisplayName)
|
|
2488
|
+
|| LEGACY_CODEX_MARKETPLACE_NAMES.has(marketplaceName);
|
|
2489
|
+
return (parsed.plugins ?? []).some((plugin) => {
|
|
2490
|
+
const values = [
|
|
2491
|
+
plugin.name,
|
|
2492
|
+
plugin.category,
|
|
2493
|
+
plugin.source?.path,
|
|
2494
|
+
plugin.interface?.displayName
|
|
2495
|
+
].filter((value) => typeof value === "string");
|
|
2496
|
+
return values.some(legacyTextHasCodexPluginResidue) || marketplaceLooksLegacy && values.some((value) => /embed-labs|dbt|development-board/i.test(value));
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
catch {
|
|
2500
|
+
return legacyTextHasCodexPluginResidue(text);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
async function discoverLegacyCodexCachePaths(targetRoot) {
|
|
2504
|
+
const paths = [];
|
|
2505
|
+
const cacheRoot = join(targetRoot, "cache");
|
|
2506
|
+
try {
|
|
2507
|
+
const marketplaces = await readdir(cacheRoot, { withFileTypes: true });
|
|
2508
|
+
for (const entry of marketplaces) {
|
|
2509
|
+
if (!entry.isDirectory())
|
|
2510
|
+
continue;
|
|
2511
|
+
if (LEGACY_CODEX_MARKETPLACE_NAMES.has(entry.name)) {
|
|
2512
|
+
paths.push(join(cacheRoot, entry.name, CODEX_PLUGIN_NAME));
|
|
2513
|
+
}
|
|
2514
|
+
for (const pluginName of LEGACY_CODEX_PLUGIN_NAMES) {
|
|
2515
|
+
paths.push(join(cacheRoot, entry.name, pluginName));
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
catch {
|
|
2520
|
+
return paths;
|
|
2521
|
+
}
|
|
2522
|
+
return paths;
|
|
2523
|
+
}
|
|
2524
|
+
async function cleanupLegacyCodexTextState(warnings) {
|
|
2525
|
+
let removed = 0;
|
|
2526
|
+
removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "history.jsonl"), warnings);
|
|
2527
|
+
removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "session_index.jsonl"), warnings);
|
|
2528
|
+
removed += await cleanupLegacyCodexTextFile(join(defaultCodexHome(), "rules", "default.rules"), warnings);
|
|
2529
|
+
return removed;
|
|
2530
|
+
}
|
|
2531
|
+
async function cleanupLegacyCodexTextFile(filePath, warnings) {
|
|
2532
|
+
try {
|
|
2533
|
+
if (!await pathExists(filePath))
|
|
2534
|
+
return 0;
|
|
2535
|
+
const current = await readFile(filePath, "utf8");
|
|
2536
|
+
const lines = current.split("\n");
|
|
2537
|
+
let removed = 0;
|
|
2538
|
+
const kept = lines.filter((line) => {
|
|
2539
|
+
if (!line)
|
|
2540
|
+
return true;
|
|
2541
|
+
if (isLegacyCodexHistoryMention(line)) {
|
|
2542
|
+
removed += 1;
|
|
2543
|
+
return false;
|
|
2544
|
+
}
|
|
2545
|
+
return true;
|
|
2546
|
+
});
|
|
2547
|
+
if (removed > 0) {
|
|
2548
|
+
await writeFile(filePath, kept.join("\n"), "utf8");
|
|
2549
|
+
}
|
|
2550
|
+
return removed;
|
|
2551
|
+
}
|
|
2552
|
+
catch (error) {
|
|
2553
|
+
warnings.push(`Could not clean Codex legacy text state ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2554
|
+
return 0;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
function isLegacyCodexHistoryMention(line) {
|
|
2558
|
+
return line.includes("plugin://dbt-agent@plugins")
|
|
2559
|
+
|| line.includes("plugin://dbt-agent@embed-labs")
|
|
2560
|
+
|| line.includes("development-board-toolchain-dev")
|
|
2561
|
+
|| line.includes("development-board-toolchain")
|
|
2562
|
+
|| /plugin:\/\/Dbt Agent@/i.test(line)
|
|
2563
|
+
|| /plugin:\/\/deve@/i.test(line)
|
|
2564
|
+
|| /dbt-agent/i.test(line);
|
|
2565
|
+
}
|
|
2566
|
+
function removeLegacyCodexConfigTables(text) {
|
|
2567
|
+
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
2568
|
+
const output = [];
|
|
2569
|
+
const removedTables = [];
|
|
2570
|
+
let skipping = false;
|
|
2571
|
+
for (const line of lines) {
|
|
2572
|
+
const table = parseTomlTableHeader(line);
|
|
2573
|
+
if (table) {
|
|
2574
|
+
skipping = isLegacyCodexConfigTable(table);
|
|
2575
|
+
if (skipping) {
|
|
2576
|
+
removedTables.push(table);
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
if (!skipping) {
|
|
2581
|
+
output.push(line);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
return { text: output.join("").replace(/\n{3,}/g, "\n\n"), removedTables };
|
|
2585
|
+
}
|
|
2586
|
+
function parseTomlTableHeader(line) {
|
|
2587
|
+
const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line);
|
|
2588
|
+
return match?.[1]?.trim();
|
|
2589
|
+
}
|
|
2590
|
+
function isLegacyCodexConfigTable(table) {
|
|
2591
|
+
return /^plugins\."dbt-agent@[^"]+"$/.test(table)
|
|
2592
|
+
|| /^plugins\."Dbt Agent@[^"]+"$/i.test(table)
|
|
2593
|
+
|| /^plugins\."deve@[^"]+"$/i.test(table)
|
|
2594
|
+
|| isLegacyEmbedLabsCodexMarketplaceConfigTable(table)
|
|
2595
|
+
|| table === "mcp_servers.dbt-agent"
|
|
2596
|
+
|| table.startsWith("mcp_servers.dbt-agent.")
|
|
2597
|
+
|| table === 'mcp_servers."dbt-agent"'
|
|
2598
|
+
|| table.startsWith('mcp_servers."dbt-agent".')
|
|
2599
|
+
|| table === "mcp_servers.deve"
|
|
2600
|
+
|| table.startsWith("mcp_servers.deve.")
|
|
2601
|
+
|| /^projects\."[^"]*\/DBT-Agent-Project(?:\/[^"]*)?"$/.test(table);
|
|
2602
|
+
}
|
|
2603
|
+
function isLegacyEmbedLabsCodexMarketplaceConfigTable(table) {
|
|
2604
|
+
for (const marketplaceName of LEGACY_CODEX_MARKETPLACE_NAMES) {
|
|
2605
|
+
if (table === `marketplaces.${marketplaceName}`) {
|
|
2606
|
+
return true;
|
|
2607
|
+
}
|
|
2608
|
+
if (table === `plugins."${CODEX_PLUGIN_NAME}@${marketplaceName}"`) {
|
|
2609
|
+
return true;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return false;
|
|
2613
|
+
}
|
|
2614
|
+
function legacyTextHasCodexPluginResidue(value) {
|
|
2615
|
+
return /dbt-agent|Dbt Agent|development-board-toolchain|development-board-toolchain-dev/i.test(value)
|
|
2616
|
+
|| value === "deve"
|
|
2617
|
+
|| value.replace(/\\/g, "/").includes("/plugins/deve")
|
|
2618
|
+
|| value.includes("plugin://deve@");
|
|
2619
|
+
}
|
|
2620
|
+
function legacyCodexCleanupWarning(cleanup) {
|
|
2621
|
+
const parts = [];
|
|
2622
|
+
if (cleanup.legacy_removed_paths.length > 0) {
|
|
2623
|
+
parts.push(`removed ${cleanup.legacy_removed_paths.length} stale/legacy Codex plugin path(s)`);
|
|
2624
|
+
}
|
|
2625
|
+
if (cleanup.legacy_removed_config_tables?.length) {
|
|
2626
|
+
parts.push(`removed ${cleanup.legacy_removed_config_tables.length} legacy Codex config table(s)`);
|
|
2627
|
+
}
|
|
2628
|
+
if (cleanup.legacy_removed_history_entries) {
|
|
2629
|
+
parts.push(`removed ${cleanup.legacy_removed_history_entries} legacy Codex text-state mention(s)`);
|
|
2630
|
+
}
|
|
2631
|
+
if (cleanup.legacy_stopped_processes) {
|
|
2632
|
+
parts.push(`stopped ${cleanup.legacy_stopped_processes} legacy Codex plugin process(es)`);
|
|
2633
|
+
}
|
|
2634
|
+
if (cleanup.warnings?.length) {
|
|
2635
|
+
parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
|
|
2636
|
+
}
|
|
2637
|
+
return parts.length > 0 ? `Codex plugin cleanup: ${parts.join(", ")}.` : undefined;
|
|
2638
|
+
}
|
|
2639
|
+
async function cleanupLegacyOpenCodePluginRemnants(targetRoot, globalInstall) {
|
|
2640
|
+
const removedPaths = [];
|
|
2641
|
+
const warnings = [];
|
|
2642
|
+
const legacyPaths = [
|
|
2643
|
+
join(targetRoot, "plugins", "development-board-toolchain.js"),
|
|
2644
|
+
join(targetRoot, "plugins", "development-board-toolchain-dev.js"),
|
|
2645
|
+
join(targetRoot, "plugins", "dbt-agent.js"),
|
|
2646
|
+
join(targetRoot, "plugins", "Dbt Agent.js"),
|
|
2647
|
+
join(targetRoot, "plugins", "deve.js"),
|
|
2648
|
+
join(targetRoot, "plugins", "deve"),
|
|
2649
|
+
join(targetRoot, "node_modules", "development-board-toolchain"),
|
|
2650
|
+
join(targetRoot, "node_modules", "development-board-toolchain-dev"),
|
|
2651
|
+
join(targetRoot, "node_modules", "dbt-agent")
|
|
2652
|
+
];
|
|
2653
|
+
if (globalInstall) {
|
|
2654
|
+
legacyPaths.push(join(targetRoot, "plugins", "embed-labs.js"));
|
|
2655
|
+
legacyPaths.push(...await discoverLegacyOpenCodeBackupPaths(targetRoot, warnings));
|
|
2656
|
+
legacyPaths.push(...await discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings));
|
|
2657
|
+
}
|
|
2658
|
+
for (const candidate of legacyPaths) {
|
|
2659
|
+
try {
|
|
2660
|
+
if (await pathExists(candidate)) {
|
|
2661
|
+
await rm(candidate, { recursive: true, force: true });
|
|
2662
|
+
removedPaths.push(candidate);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
catch (error) {
|
|
2666
|
+
warnings.push(`Could not remove ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
return {
|
|
2670
|
+
legacy_removed_paths: removedPaths,
|
|
2671
|
+
legacy_removed_config_entries: [],
|
|
2672
|
+
...(warnings.length > 0 ? { warnings } : {})
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
async function discoverLegacyOpenCodeBackupPaths(targetRoot, warnings) {
|
|
2676
|
+
const paths = [];
|
|
2677
|
+
try {
|
|
2678
|
+
const entries = await readdir(targetRoot, { withFileTypes: true });
|
|
2679
|
+
for (const entry of entries) {
|
|
2680
|
+
if (!entry.isFile() || !entry.name.includes(".bak"))
|
|
2681
|
+
continue;
|
|
2682
|
+
const filePath = join(targetRoot, entry.name);
|
|
2683
|
+
try {
|
|
2684
|
+
const current = await readFile(filePath, "utf8");
|
|
2685
|
+
if (legacyTextHasCodexPluginResidue(current)) {
|
|
2686
|
+
paths.push(filePath);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
catch (error) {
|
|
2690
|
+
warnings.push(`Could not inspect legacy OpenCode backup ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
catch {
|
|
2695
|
+
return paths;
|
|
2696
|
+
}
|
|
2697
|
+
return paths;
|
|
2698
|
+
}
|
|
2699
|
+
async function discoverLegacyOpenCodePluginCachePaths(targetRoot, warnings) {
|
|
2700
|
+
const paths = [];
|
|
2701
|
+
const cacheRoot = join(targetRoot, ".embed-labs", "plugin-cache");
|
|
2702
|
+
try {
|
|
2703
|
+
const entries = await readdir(cacheRoot, { withFileTypes: true });
|
|
2704
|
+
for (const entry of entries) {
|
|
2705
|
+
if (!entry.isFile())
|
|
2706
|
+
continue;
|
|
2707
|
+
if (/^(embed-labs|embed-labs-opencode-plugin)-\d+\.\d+\.\d+\.tgz$/.test(entry.name)) {
|
|
2708
|
+
paths.push(join(cacheRoot, entry.name));
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
catch (error) {
|
|
2713
|
+
if (error.code !== "ENOENT") {
|
|
2714
|
+
warnings.push(`Could not inspect OpenCode plugin cache ${cacheRoot}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
return paths;
|
|
2718
|
+
}
|
|
2719
|
+
function legacyOpenCodeCleanupWarning(cleanup) {
|
|
2720
|
+
const parts = [];
|
|
2721
|
+
if (cleanup.legacy_removed_paths.length > 0) {
|
|
2722
|
+
parts.push(`removed ${cleanup.legacy_removed_paths.length} legacy OpenCode plugin path(s)`);
|
|
2723
|
+
}
|
|
2724
|
+
if (cleanup.legacy_removed_config_entries?.length) {
|
|
2725
|
+
parts.push(`removed ${cleanup.legacy_removed_config_entries.length} legacy OpenCode config entry(s)`);
|
|
2726
|
+
}
|
|
2727
|
+
if (cleanup.warnings?.length) {
|
|
2728
|
+
parts.push(`cleanup warning(s): ${cleanup.warnings.join("; ")}`);
|
|
2729
|
+
}
|
|
2730
|
+
return parts.length > 0 ? `Legacy OpenCode cleanup: ${parts.join(", ")}.` : undefined;
|
|
2731
|
+
}
|
|
2732
|
+
function combineWarnings(...warnings) {
|
|
2733
|
+
const actual = warnings.filter((warning) => Boolean(warning));
|
|
2734
|
+
return actual.length > 0 ? actual.join(" ") : undefined;
|
|
1804
2735
|
}
|
|
1805
2736
|
async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
|
|
1806
2737
|
const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
|
|
@@ -1861,6 +2792,115 @@ async function maybeRegisterCodexMcp(parsed, targetRoot, targetPath) {
|
|
|
1861
2792
|
const warning = await upsertCodexMcpRuntimeConfig(bridgePath);
|
|
1862
2793
|
return warning ? { registered: true, warning } : { registered: true };
|
|
1863
2794
|
}
|
|
2795
|
+
async function maybeRegisterCodexMarketplace(parsed, targetRoot, targetPath) {
|
|
2796
|
+
const explicitTarget = Boolean(stringFlag(parsed, "target") || stringFlag(parsed, "codex-target"));
|
|
2797
|
+
if (explicitTarget && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
|
|
2798
|
+
return {
|
|
2799
|
+
registered: false,
|
|
2800
|
+
warning: "Codex plugin marketplace entry was not registered because a custom target was used. Set EMBED_CODEX_MARKETPLACE_REGISTER=1 to register it anyway."
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
if (resolve(targetRoot) !== resolve(defaultCodexPluginRoot()) && process.env.EMBED_CODEX_MARKETPLACE_REGISTER !== "1") {
|
|
2804
|
+
return {
|
|
2805
|
+
registered: false,
|
|
2806
|
+
warning: "Codex plugin marketplace entry was not registered because the install target is not the default Codex plugin root."
|
|
2807
|
+
};
|
|
2808
|
+
}
|
|
2809
|
+
const marketplacePath = defaultCodexLocalMarketplaceRoot();
|
|
2810
|
+
const marketplacePluginPath = join(marketplacePath, "plugins", CODEX_PLUGIN_NAME);
|
|
2811
|
+
try {
|
|
2812
|
+
if (!await pathExists(join(targetPath, ".codex-plugin", "plugin.json"))) {
|
|
2813
|
+
return {
|
|
2814
|
+
registered: false,
|
|
2815
|
+
warning: `Codex plugin manifest was not found at ${join(targetPath, ".codex-plugin", "plugin.json")}.`
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
await rm(marketplacePluginPath, { recursive: true, force: true });
|
|
2819
|
+
await mkdir(dirname(marketplacePluginPath), { recursive: true });
|
|
2820
|
+
await cp(targetPath, marketplacePluginPath, { recursive: true });
|
|
2821
|
+
await refreshCodexPluginCache(targetPath);
|
|
2822
|
+
await writeCodexLocalMarketplaceManifest(marketplacePath);
|
|
2823
|
+
const warning = await upsertCodexPluginMarketplaceConfig(marketplacePath);
|
|
2824
|
+
return warning ? { registered: true, marketplacePath, warning } : { registered: true, marketplacePath };
|
|
2825
|
+
}
|
|
2826
|
+
catch (error) {
|
|
2827
|
+
return {
|
|
2828
|
+
registered: false,
|
|
2829
|
+
marketplacePath,
|
|
2830
|
+
warning: `Could not register Codex plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
function defaultCodexLocalMarketplaceRoot() {
|
|
2835
|
+
return join(defaultCodexHome(), "local-marketplaces", CODEX_PLUGIN_NAME);
|
|
2836
|
+
}
|
|
2837
|
+
async function refreshCodexPluginCache(targetPath) {
|
|
2838
|
+
const manifestPath = join(targetPath, ".codex-plugin", "plugin.json");
|
|
2839
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
2840
|
+
const version = typeof manifest.version === "string" && manifest.version.trim()
|
|
2841
|
+
? manifest.version.trim()
|
|
2842
|
+
: "local";
|
|
2843
|
+
const cachePluginRoot = join(defaultCodexPluginRoot(), "cache", CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME);
|
|
2844
|
+
const cacheVersionPath = join(cachePluginRoot, version);
|
|
2845
|
+
await rm(cachePluginRoot, { recursive: true, force: true });
|
|
2846
|
+
await mkdir(dirname(cacheVersionPath), { recursive: true });
|
|
2847
|
+
await cp(targetPath, cacheVersionPath, { recursive: true });
|
|
2848
|
+
}
|
|
2849
|
+
async function writeCodexLocalMarketplaceManifest(marketplacePath) {
|
|
2850
|
+
const manifestPath = join(marketplacePath, ".agents", "plugins", "marketplace.json");
|
|
2851
|
+
const manifest = {
|
|
2852
|
+
name: CODEX_MARKETPLACE_NAME,
|
|
2853
|
+
interface: {
|
|
2854
|
+
displayName: "Embed Labs"
|
|
2855
|
+
},
|
|
2856
|
+
plugins: [
|
|
2857
|
+
{
|
|
2858
|
+
name: CODEX_PLUGIN_NAME,
|
|
2859
|
+
source: {
|
|
2860
|
+
source: "local",
|
|
2861
|
+
path: `./plugins/${CODEX_PLUGIN_NAME}`
|
|
2862
|
+
},
|
|
2863
|
+
policy: {
|
|
2864
|
+
installation: "AVAILABLE",
|
|
2865
|
+
authentication: "ON_USE"
|
|
2866
|
+
},
|
|
2867
|
+
category: "Developer Tools"
|
|
2868
|
+
}
|
|
2869
|
+
]
|
|
2870
|
+
};
|
|
2871
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
2872
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
2873
|
+
}
|
|
2874
|
+
async function upsertCodexPluginMarketplaceConfig(marketplacePath) {
|
|
2875
|
+
const configPath = codexConfigPath();
|
|
2876
|
+
try {
|
|
2877
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
2878
|
+
let text = "";
|
|
2879
|
+
try {
|
|
2880
|
+
text = await readFile(configPath, "utf8");
|
|
2881
|
+
}
|
|
2882
|
+
catch {
|
|
2883
|
+
text = "";
|
|
2884
|
+
}
|
|
2885
|
+
const original = text;
|
|
2886
|
+
const cleaned = removeLegacyCodexConfigTables(text).text;
|
|
2887
|
+
let updated = upsertTomlTableKeys(cleaned, `marketplaces.${CODEX_MARKETPLACE_NAME}`, {
|
|
2888
|
+
source_type: tomlString("local"),
|
|
2889
|
+
source: tomlString(marketplacePath),
|
|
2890
|
+
last_updated: tomlString(new Date().toISOString().replace(/\.\d{3}Z$/, "Z"))
|
|
2891
|
+
});
|
|
2892
|
+
updated = upsertTomlTableKeys(updated, `plugins."${CODEX_PLUGIN_NAME}@${CODEX_MARKETPLACE_NAME}"`, {
|
|
2893
|
+
enabled: "true"
|
|
2894
|
+
});
|
|
2895
|
+
if (updated !== original) {
|
|
2896
|
+
await writeFile(configPath, updated, "utf8");
|
|
2897
|
+
}
|
|
2898
|
+
return undefined;
|
|
2899
|
+
}
|
|
2900
|
+
catch (error) {
|
|
2901
|
+
return `${configPath} could not be updated with the Embed Labs plugin marketplace entry: ${error instanceof Error ? error.message : String(error)}`;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
1864
2904
|
function codexMcpAlreadyRegistered(stdout, bridgePath, cloudUrl, authFile, embedCliBin) {
|
|
1865
2905
|
try {
|
|
1866
2906
|
const parsed = JSON.parse(stdout);
|
|
@@ -1959,24 +2999,173 @@ async function resolveExecutableOnPath(name) {
|
|
|
1959
2999
|
return undefined;
|
|
1960
3000
|
}
|
|
1961
3001
|
}
|
|
1962
|
-
const pathEntries = (process.env.PATH || "").split(delimiter).filter(Boolean);
|
|
1963
|
-
for (const entry of pathEntries) {
|
|
1964
|
-
const candidate = join(entry, name);
|
|
3002
|
+
const pathEntries = (process.env.PATH || "").split(delimiter).filter(Boolean);
|
|
3003
|
+
for (const entry of pathEntries) {
|
|
3004
|
+
const candidate = join(entry, name);
|
|
3005
|
+
try {
|
|
3006
|
+
await access(candidate, constants.X_OK);
|
|
3007
|
+
return candidate;
|
|
3008
|
+
}
|
|
3009
|
+
catch {
|
|
3010
|
+
// Keep searching PATH.
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
return undefined;
|
|
3014
|
+
}
|
|
3015
|
+
async function runBridgeStart(parsed) {
|
|
3016
|
+
const host = stringFlag(parsed, "host");
|
|
3017
|
+
const port = numberFlag(parsed, "port");
|
|
3018
|
+
const bridge = await resolveBridgeLauncher();
|
|
3019
|
+
const args = [...bridge.args];
|
|
3020
|
+
if (host) {
|
|
3021
|
+
args.push("--host", host);
|
|
3022
|
+
}
|
|
3023
|
+
if (port !== undefined) {
|
|
3024
|
+
args.push("--port", String(port));
|
|
3025
|
+
}
|
|
3026
|
+
const env = {
|
|
3027
|
+
...process.env,
|
|
3028
|
+
...(host ? { EMBED_BRIDGE_HOST: host } : {}),
|
|
3029
|
+
...(port !== undefined ? { EMBED_BRIDGE_PORT: String(port) } : {})
|
|
3030
|
+
};
|
|
3031
|
+
const child = spawn(bridge.command, args, {
|
|
3032
|
+
stdio: "inherit",
|
|
3033
|
+
env
|
|
3034
|
+
});
|
|
3035
|
+
const forwardSignal = (signal) => {
|
|
3036
|
+
if (!child.killed) {
|
|
3037
|
+
child.kill(signal);
|
|
3038
|
+
}
|
|
3039
|
+
};
|
|
3040
|
+
process.once("SIGINT", forwardSignal);
|
|
3041
|
+
process.once("SIGTERM", forwardSignal);
|
|
3042
|
+
return await new Promise((resolveCode) => {
|
|
3043
|
+
child.on("error", (error) => {
|
|
3044
|
+
process.off("SIGINT", forwardSignal);
|
|
3045
|
+
process.off("SIGTERM", forwardSignal);
|
|
3046
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3047
|
+
resolveCode(1);
|
|
3048
|
+
});
|
|
3049
|
+
child.on("close", (code, signal) => {
|
|
3050
|
+
process.off("SIGINT", forwardSignal);
|
|
3051
|
+
process.off("SIGTERM", forwardSignal);
|
|
3052
|
+
if (signal === "SIGINT" || signal === "SIGTERM") {
|
|
3053
|
+
resolveCode(0);
|
|
3054
|
+
}
|
|
3055
|
+
else {
|
|
3056
|
+
resolveCode(code ?? 0);
|
|
3057
|
+
}
|
|
3058
|
+
});
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
3061
|
+
async function resolveBridgeLauncher() {
|
|
3062
|
+
const explicitBinary = process.env.EMBED_LOCAL_BRIDGE_BINARY?.trim();
|
|
3063
|
+
if (explicitBinary) {
|
|
3064
|
+
try {
|
|
3065
|
+
await access(explicitBinary, constants.X_OK);
|
|
3066
|
+
return { command: explicitBinary, args: [] };
|
|
3067
|
+
}
|
|
3068
|
+
catch {
|
|
3069
|
+
// Fall through so the package launcher can print its clearer repair message.
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
const pathBinary = await resolveExecutableOnPath(process.platform === "win32" ? "embed-local-bridge.cmd" : "embed-local-bridge");
|
|
3073
|
+
if (pathBinary) {
|
|
3074
|
+
return { command: pathBinary, args: [] };
|
|
3075
|
+
}
|
|
3076
|
+
const packageLauncher = await resolveBridgePackageLauncher();
|
|
3077
|
+
if (packageLauncher) {
|
|
3078
|
+
return { command: process.execPath, args: [packageLauncher] };
|
|
3079
|
+
}
|
|
3080
|
+
return {
|
|
3081
|
+
command: process.execPath,
|
|
3082
|
+
args: [resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js")]
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
async function resolveBridgePackageLauncher() {
|
|
3086
|
+
const candidates = [];
|
|
3087
|
+
try {
|
|
3088
|
+
const packageJson = require.resolve("@embed-labs/local-bridge/package.json");
|
|
3089
|
+
candidates.push(join(dirname(packageJson), "dist", "index.js"));
|
|
3090
|
+
}
|
|
3091
|
+
catch {
|
|
3092
|
+
// Source checkout fallback below.
|
|
3093
|
+
}
|
|
3094
|
+
candidates.push(resolve(SOURCE_CHECKOUT_ROOT, "packages", "local-bridge", "dist", "index.js"));
|
|
3095
|
+
for (const candidate of candidates) {
|
|
1965
3096
|
try {
|
|
1966
|
-
await access(candidate, constants.
|
|
3097
|
+
await access(candidate, constants.R_OK);
|
|
1967
3098
|
return candidate;
|
|
1968
3099
|
}
|
|
1969
3100
|
catch {
|
|
1970
|
-
// Keep
|
|
3101
|
+
// Keep looking.
|
|
1971
3102
|
}
|
|
1972
3103
|
}
|
|
1973
3104
|
return undefined;
|
|
1974
3105
|
}
|
|
1975
3106
|
function defaultOpenCodeRoot() {
|
|
1976
|
-
return
|
|
3107
|
+
return globalOpenCodeRoot();
|
|
3108
|
+
}
|
|
3109
|
+
function globalOpenCodeRoot() {
|
|
3110
|
+
return join(homedir(), ".config", "opencode");
|
|
3111
|
+
}
|
|
3112
|
+
function isGlobalOpenCodeRoot(targetRoot) {
|
|
3113
|
+
return resolve(targetRoot) === resolve(globalOpenCodeRoot());
|
|
3114
|
+
}
|
|
3115
|
+
async function ensureOpenCodeGlobalPluginConfig() {
|
|
3116
|
+
const configPath = join(globalOpenCodeRoot(), "opencode.json");
|
|
3117
|
+
let existing = {};
|
|
3118
|
+
try {
|
|
3119
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
3120
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3121
|
+
existing = parsed;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
catch {
|
|
3125
|
+
existing = {};
|
|
3126
|
+
}
|
|
3127
|
+
const configured = Array.isArray(existing.plugin)
|
|
3128
|
+
? existing.plugin.filter((item) => typeof item === "string")
|
|
3129
|
+
: Array.isArray(existing.plugins)
|
|
3130
|
+
? existing.plugins.filter((item) => typeof item === "string")
|
|
3131
|
+
: [];
|
|
3132
|
+
const removed = configured.filter(isLegacyOpenCodePluginConfigEntry);
|
|
3133
|
+
const cleaned = configured.filter((item) => !isLegacyOpenCodePluginConfigEntry(item));
|
|
3134
|
+
if (!cleaned.includes("embed-labs")) {
|
|
3135
|
+
cleaned.push("embed-labs");
|
|
3136
|
+
}
|
|
3137
|
+
await writeFile(configPath, `${JSON.stringify({
|
|
3138
|
+
...existing,
|
|
3139
|
+
plugin: cleaned,
|
|
3140
|
+
plugins: undefined
|
|
3141
|
+
}, null, 2)}\n`, "utf8");
|
|
3142
|
+
return removed;
|
|
3143
|
+
}
|
|
3144
|
+
function isLegacyOpenCodePluginConfigEntry(item) {
|
|
3145
|
+
const normalized = item.trim().replace(/\\/g, "/");
|
|
3146
|
+
const pathOnly = normalized.split(/[?#]/, 1)[0] || normalized;
|
|
3147
|
+
return normalized === "dbt-agent"
|
|
3148
|
+
|| normalized === "Dbt Agent"
|
|
3149
|
+
|| normalized === "deve"
|
|
3150
|
+
|| normalized === "development-board-toolchain"
|
|
3151
|
+
|| normalized === "development-board-toolchain-dev"
|
|
3152
|
+
|| normalized === "./plugins/deve"
|
|
3153
|
+
|| normalized === "./plugins/deve.js"
|
|
3154
|
+
|| /(?:^|\/)plugins\/deve(?:\.js)?$/.test(pathOnly)
|
|
3155
|
+
|| /(?:^|\/)plugins\/dbt-agent(?:\.js)?$/.test(pathOnly)
|
|
3156
|
+
|| normalized === "./plugins/development-board-toolchain"
|
|
3157
|
+
|| normalized === "./plugins/development-board-toolchain.js"
|
|
3158
|
+
|| normalized === "./plugins/development-board-toolchain-dev"
|
|
3159
|
+
|| normalized === "./plugins/development-board-toolchain-dev.js"
|
|
3160
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain")
|
|
3161
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain.js")
|
|
3162
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain-dev")
|
|
3163
|
+
|| pathOnly.endsWith("/plugins/development-board-toolchain-dev.js")
|
|
3164
|
+
|| normalized.includes("dbt-agent")
|
|
3165
|
+
|| normalized.includes("development-board-toolchain");
|
|
1977
3166
|
}
|
|
1978
3167
|
async function openCodeDuplicatePluginWarning(targetRoot) {
|
|
1979
|
-
const globalRoot =
|
|
3168
|
+
const globalRoot = globalOpenCodeRoot();
|
|
1980
3169
|
if (resolve(targetRoot) === resolve(globalRoot))
|
|
1981
3170
|
return undefined;
|
|
1982
3171
|
const configPath = join(globalRoot, "opencode.json");
|
|
@@ -2008,6 +3197,21 @@ async function localPluginVersion(kind) {
|
|
|
2008
3197
|
return undefined;
|
|
2009
3198
|
}
|
|
2010
3199
|
}
|
|
3200
|
+
async function installedCodexPluginVersion(pluginPath) {
|
|
3201
|
+
return await readPackageVersion(join(pluginPath, ".codex-plugin", "plugin.json"));
|
|
3202
|
+
}
|
|
3203
|
+
async function installedOpenCodePluginVersion(targetRoot) {
|
|
3204
|
+
return await readPackageVersion(join(targetRoot, "node_modules", "embed-labs", "package.json"));
|
|
3205
|
+
}
|
|
3206
|
+
async function readPackageVersion(filePath) {
|
|
3207
|
+
try {
|
|
3208
|
+
const parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
3209
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : undefined;
|
|
3210
|
+
}
|
|
3211
|
+
catch {
|
|
3212
|
+
return undefined;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
2011
3215
|
async function localPluginSourcesAvailable() {
|
|
2012
3216
|
return await pathExists(sourceCheckoutPath("platform_plugins", "codex_plugin", "plugins", "embed-labs", ".codex-plugin", "plugin.json"))
|
|
2013
3217
|
&& await pathExists(sourceCheckoutPath("platform_plugins", "opencode_plugin", "package.json"));
|
|
@@ -2066,19 +3270,65 @@ async function parseErrorResponse(response) {
|
|
|
2066
3270
|
return undefined;
|
|
2067
3271
|
}
|
|
2068
3272
|
async function cloudAuthToken() {
|
|
3273
|
+
return (await cloudAuthConfig()).token;
|
|
3274
|
+
}
|
|
3275
|
+
async function cloudAuthConfig() {
|
|
2069
3276
|
const envToken = process.env.EMBED_API_TOKEN?.trim();
|
|
2070
3277
|
if (envToken) {
|
|
2071
|
-
|
|
3278
|
+
const fileConfig = await readLocalAuthFile();
|
|
3279
|
+
return {
|
|
3280
|
+
...fileConfig,
|
|
3281
|
+
token: envToken,
|
|
3282
|
+
profile: process.env.EMBED_AUTH_PROFILE ?? fileConfig.profile ?? "default",
|
|
3283
|
+
source: "env"
|
|
3284
|
+
};
|
|
2072
3285
|
}
|
|
3286
|
+
const fileConfig = await readLocalAuthFile();
|
|
3287
|
+
return {
|
|
3288
|
+
...fileConfig,
|
|
3289
|
+
token: fileConfig.token?.trim() || undefined,
|
|
3290
|
+
profile: fileConfig.profile ?? "default",
|
|
3291
|
+
source: fileConfig.token ? "file" : undefined
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
async function readLocalAuthFile() {
|
|
2073
3295
|
try {
|
|
2074
3296
|
const parsed = JSON.parse(await readFile(DEFAULT_AUTH_FILE, "utf8"));
|
|
2075
|
-
|
|
2076
|
-
return fileToken || undefined;
|
|
3297
|
+
return normalizeLocalAuthFile(parsed);
|
|
2077
3298
|
}
|
|
2078
3299
|
catch {
|
|
2079
|
-
return
|
|
3300
|
+
return {};
|
|
2080
3301
|
}
|
|
2081
3302
|
}
|
|
3303
|
+
function normalizeLocalAuthFile(parsed) {
|
|
3304
|
+
const device = isJsonObject(parsed.device) ? parsed.device : undefined;
|
|
3305
|
+
const normalizedDevice = device && typeof device.device_id === "string" && typeof device.fingerprint_hash === "string" && typeof device.private_key_pem === "string"
|
|
3306
|
+
? {
|
|
3307
|
+
device_id: device.device_id,
|
|
3308
|
+
fingerprint_hash: device.fingerprint_hash,
|
|
3309
|
+
private_key_pem: device.private_key_pem,
|
|
3310
|
+
public_key_pem: typeof device.public_key_pem === "string" ? device.public_key_pem : undefined,
|
|
3311
|
+
label: typeof device.label === "string" ? device.label : undefined,
|
|
3312
|
+
platform: typeof device.platform === "string" ? device.platform : undefined,
|
|
3313
|
+
arch: typeof device.arch === "string" ? device.arch : undefined,
|
|
3314
|
+
hostname_hash: typeof device.hostname_hash === "string" ? device.hostname_hash : undefined,
|
|
3315
|
+
registered_at: typeof device.registered_at === "string" ? device.registered_at : undefined
|
|
3316
|
+
}
|
|
3317
|
+
: undefined;
|
|
3318
|
+
return {
|
|
3319
|
+
profile: typeof parsed.profile === "string" ? parsed.profile : undefined,
|
|
3320
|
+
token: typeof parsed.token === "string" ? parsed.token.trim() : undefined,
|
|
3321
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined,
|
|
3322
|
+
account_id: typeof parsed.account_id === "string" ? parsed.account_id : undefined,
|
|
3323
|
+
api_key_id: typeof parsed.api_key_id === "string" ? parsed.api_key_id : undefined,
|
|
3324
|
+
device: normalizedDevice
|
|
3325
|
+
};
|
|
3326
|
+
}
|
|
3327
|
+
async function writeLocalAuthFile(config) {
|
|
3328
|
+
await mkdir(dirname(DEFAULT_AUTH_FILE), { recursive: true });
|
|
3329
|
+
await writeFile(DEFAULT_AUTH_FILE, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
3330
|
+
await chmod(DEFAULT_AUTH_FILE, 0o600).catch(() => undefined);
|
|
3331
|
+
}
|
|
2082
3332
|
function serviceBaseUrl(url) {
|
|
2083
3333
|
return url.replace(/\/+$/, "");
|
|
2084
3334
|
}
|
|
@@ -3422,6 +4672,47 @@ function boardKnowledgeFileRequest(parsed) {
|
|
|
3422
4672
|
outputPath: outputPath.value
|
|
3423
4673
|
};
|
|
3424
4674
|
}
|
|
4675
|
+
function boardKnowledgeSearchRequest(parsed) {
|
|
4676
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "query", "source", "limit"]);
|
|
4677
|
+
if (unknownFlag) {
|
|
4678
|
+
return `Unknown flag --${unknownFlag}. ${BOARD_KNOWLEDGE_SEARCH_USAGE}`;
|
|
4679
|
+
}
|
|
4680
|
+
const templateId = parsed.command[3]?.trim();
|
|
4681
|
+
if (!templateId) {
|
|
4682
|
+
return BOARD_KNOWLEDGE_SEARCH_USAGE;
|
|
4683
|
+
}
|
|
4684
|
+
const extra = parsed.command.slice(4);
|
|
4685
|
+
if (extra.length > 0) {
|
|
4686
|
+
return `Unexpected argument: ${extra[0]}. ${BOARD_KNOWLEDGE_SEARCH_USAGE}`;
|
|
4687
|
+
}
|
|
4688
|
+
const query = optionalTrimmedStringFlag(parsed, "query");
|
|
4689
|
+
if (query.error) {
|
|
4690
|
+
return query.error;
|
|
4691
|
+
}
|
|
4692
|
+
if (!query.value) {
|
|
4693
|
+
return BOARD_KNOWLEDGE_SEARCH_USAGE;
|
|
4694
|
+
}
|
|
4695
|
+
const source = optionalTrimmedStringFlag(parsed, "source");
|
|
4696
|
+
if (source.error) {
|
|
4697
|
+
return source.error;
|
|
4698
|
+
}
|
|
4699
|
+
if (source.value && !["board_pack", "build_template", "registry"].includes(source.value)) {
|
|
4700
|
+
return BOARD_KNOWLEDGE_SEARCH_USAGE;
|
|
4701
|
+
}
|
|
4702
|
+
const limit = optionalPositiveIntegerFlag(parsed, "limit");
|
|
4703
|
+
if (limit.error) {
|
|
4704
|
+
return limit.error;
|
|
4705
|
+
}
|
|
4706
|
+
if (limit.value !== undefined && limit.value > 10) {
|
|
4707
|
+
return "--limit must be between 1 and 10.";
|
|
4708
|
+
}
|
|
4709
|
+
return {
|
|
4710
|
+
templateId,
|
|
4711
|
+
query: query.value,
|
|
4712
|
+
source: source.value,
|
|
4713
|
+
limit: limit.value ?? 5
|
|
4714
|
+
};
|
|
4715
|
+
}
|
|
3425
4716
|
function toolCallRequest(parsed) {
|
|
3426
4717
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "input-json", "approve"]);
|
|
3427
4718
|
if (unknownFlag) {
|
|
@@ -3537,30 +4828,272 @@ async function authLogin(parsed) {
|
|
|
3537
4828
|
return fail("invalid_args", "Usage: embed auth login --token <token> [--profile default] [--json]");
|
|
3538
4829
|
}
|
|
3539
4830
|
const updatedAt = new Date().toISOString();
|
|
3540
|
-
|
|
3541
|
-
await
|
|
3542
|
-
|
|
4831
|
+
const current = await readLocalAuthFile();
|
|
4832
|
+
const localDevice = await buildLocalDeviceAuth(parsed, current.device);
|
|
4833
|
+
const registration = await registerLocalDevice(token.trim(), localDevice.registration);
|
|
4834
|
+
if (!registration.ok) {
|
|
4835
|
+
return fail(registration.error.code, registration.error.message, {
|
|
4836
|
+
remediation: registration.error.remediation,
|
|
4837
|
+
details: registration.error.details
|
|
4838
|
+
});
|
|
4839
|
+
}
|
|
4840
|
+
const device = {
|
|
4841
|
+
device_id: registration.data.device.device_id,
|
|
4842
|
+
fingerprint_hash: localDevice.device.fingerprint_hash,
|
|
4843
|
+
private_key_pem: localDevice.device.private_key_pem,
|
|
4844
|
+
public_key_pem: localDevice.device.public_key_pem,
|
|
4845
|
+
label: registration.data.device.label ?? localDevice.device.label,
|
|
4846
|
+
platform: registration.data.device.platform ?? localDevice.device.platform,
|
|
4847
|
+
arch: registration.data.device.arch ?? localDevice.device.arch,
|
|
4848
|
+
hostname_hash: registration.data.device.hostname_hash ?? localDevice.device.hostname_hash,
|
|
4849
|
+
registered_at: registration.data.device.first_seen_at
|
|
4850
|
+
};
|
|
4851
|
+
await writeLocalAuthFile({
|
|
4852
|
+
profile,
|
|
4853
|
+
token: token.trim(),
|
|
4854
|
+
updated_at: updatedAt,
|
|
4855
|
+
account_id: registration.data.device.account_id,
|
|
4856
|
+
api_key_id: registration.data.device.api_key_id,
|
|
4857
|
+
device
|
|
4858
|
+
});
|
|
4859
|
+
return ok({
|
|
4860
|
+
authenticated: true,
|
|
4861
|
+
profile,
|
|
4862
|
+
source: "file",
|
|
4863
|
+
updated_at: updatedAt,
|
|
4864
|
+
account_id: registration.data.device.account_id,
|
|
4865
|
+
api_key_id: registration.data.device.api_key_id,
|
|
4866
|
+
device_id: device.device_id,
|
|
4867
|
+
device_fingerprint_hash: device.fingerprint_hash,
|
|
4868
|
+
device_label: device.label,
|
|
4869
|
+
device_registered_at: device.registered_at,
|
|
4870
|
+
device_private_key_configured: true
|
|
4871
|
+
});
|
|
3543
4872
|
}
|
|
3544
4873
|
async function authStatus() {
|
|
3545
|
-
|
|
4874
|
+
const envToken = process.env.EMBED_API_TOKEN?.trim();
|
|
4875
|
+
const file = await readLocalAuthFile();
|
|
4876
|
+
const deviceIntegrity = file.device
|
|
4877
|
+
? (await validateLocalDeviceIntegrity(file.device)).ok ? "ok" : "failed"
|
|
4878
|
+
: "unbound";
|
|
4879
|
+
if (envToken) {
|
|
3546
4880
|
return {
|
|
3547
4881
|
authenticated: true,
|
|
3548
4882
|
profile: process.env.EMBED_AUTH_PROFILE ?? "default",
|
|
3549
|
-
source: "env"
|
|
4883
|
+
source: "env",
|
|
4884
|
+
account_id: file.account_id,
|
|
4885
|
+
api_key_id: file.api_key_id,
|
|
4886
|
+
device_id: file.device?.device_id,
|
|
4887
|
+
device_fingerprint_hash: file.device?.fingerprint_hash,
|
|
4888
|
+
device_label: file.device?.label,
|
|
4889
|
+
device_registered_at: file.device?.registered_at,
|
|
4890
|
+
device_private_key_configured: Boolean(file.device?.private_key_pem),
|
|
4891
|
+
device_integrity: deviceIntegrity
|
|
3550
4892
|
};
|
|
3551
4893
|
}
|
|
4894
|
+
return {
|
|
4895
|
+
authenticated: Boolean(file.token?.trim()),
|
|
4896
|
+
profile: file.profile ?? "default",
|
|
4897
|
+
source: file.token ? "file" : undefined,
|
|
4898
|
+
updated_at: file.updated_at,
|
|
4899
|
+
account_id: file.account_id,
|
|
4900
|
+
api_key_id: file.api_key_id,
|
|
4901
|
+
device_id: file.device?.device_id,
|
|
4902
|
+
device_fingerprint_hash: file.device?.fingerprint_hash,
|
|
4903
|
+
device_label: file.device?.label,
|
|
4904
|
+
device_registered_at: file.device?.registered_at,
|
|
4905
|
+
device_private_key_configured: Boolean(file.device?.private_key_pem),
|
|
4906
|
+
device_integrity: deviceIntegrity
|
|
4907
|
+
};
|
|
4908
|
+
}
|
|
4909
|
+
async function authDeviceStatus(parsed) {
|
|
4910
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
4911
|
+
const unexpected = parsed.command.slice(3);
|
|
4912
|
+
if (unknownFlag || unexpected.length > 0) {
|
|
4913
|
+
return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_STATUS_USAGE}` : AUTH_DEVICE_STATUS_USAGE);
|
|
4914
|
+
}
|
|
4915
|
+
const local = await authStatus();
|
|
4916
|
+
const remote = local.authenticated ? await cloudGet("/v1/me/devices") : undefined;
|
|
4917
|
+
if (remote && !remote.ok) {
|
|
4918
|
+
return ok({ local });
|
|
4919
|
+
}
|
|
4920
|
+
return ok({ local, remote: remote?.data });
|
|
4921
|
+
}
|
|
4922
|
+
async function authDeviceList(parsed) {
|
|
4923
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
4924
|
+
const unexpected = parsed.command.slice(3);
|
|
4925
|
+
if (unknownFlag || unexpected.length > 0) {
|
|
4926
|
+
return fail("invalid_args", unknownFlag ? `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_LIST_USAGE}` : AUTH_DEVICE_LIST_USAGE);
|
|
4927
|
+
}
|
|
4928
|
+
return await cloudGet("/v1/me/devices");
|
|
4929
|
+
}
|
|
4930
|
+
async function authDeviceRevoke(parsed) {
|
|
4931
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json"]);
|
|
4932
|
+
if (unknownFlag) {
|
|
4933
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_REVOKE_USAGE}`);
|
|
4934
|
+
}
|
|
4935
|
+
const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_REVOKE_USAGE);
|
|
4936
|
+
if (!id.ok) {
|
|
4937
|
+
return fail("invalid_args", id.error);
|
|
4938
|
+
}
|
|
4939
|
+
return await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}/revoke`, {});
|
|
4940
|
+
}
|
|
4941
|
+
async function authDeviceRename(parsed) {
|
|
4942
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "label"]);
|
|
4943
|
+
if (unknownFlag) {
|
|
4944
|
+
return fail("invalid_args", `Unknown flag --${unknownFlag}. ${AUTH_DEVICE_RENAME_USAGE}`);
|
|
4945
|
+
}
|
|
4946
|
+
const id = commandId(parsed, 3, "device_id", AUTH_DEVICE_RENAME_USAGE);
|
|
4947
|
+
if (!id.ok) {
|
|
4948
|
+
return fail("invalid_args", id.error);
|
|
4949
|
+
}
|
|
4950
|
+
const label = stringFlag(parsed, "label");
|
|
4951
|
+
if (!label?.trim()) {
|
|
4952
|
+
return fail("invalid_args", AUTH_DEVICE_RENAME_USAGE);
|
|
4953
|
+
}
|
|
4954
|
+
const updated = await cloudPost(`/v1/me/devices/${encodeURIComponent(id.value)}`, { label: label.trim() });
|
|
4955
|
+
if (updated.ok) {
|
|
4956
|
+
const auth = await readLocalAuthFile();
|
|
4957
|
+
if (auth.device?.device_id === updated.data.device_id) {
|
|
4958
|
+
await writeLocalAuthFile({ ...auth, device: { ...auth.device, label: updated.data.label } });
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
return updated;
|
|
4962
|
+
}
|
|
4963
|
+
async function buildLocalDeviceAuth(parsed, existing) {
|
|
4964
|
+
const fingerprint = await localHardwareFingerprint();
|
|
4965
|
+
const keyPair = existing?.fingerprint_hash === fingerprint.fingerprint_hash && existing.private_key_pem && existing.public_key_pem
|
|
4966
|
+
? { privateKeyPem: existing.private_key_pem, publicKeyPem: existing.public_key_pem }
|
|
4967
|
+
: generateLocalDeviceKeyPair();
|
|
4968
|
+
const label = stringFlag(parsed, "label")?.trim()
|
|
4969
|
+
|| existing?.label
|
|
4970
|
+
|| `${fingerprint.platform} ${fingerprint.arch}`;
|
|
4971
|
+
const device = {
|
|
4972
|
+
device_id: existing?.fingerprint_hash === fingerprint.fingerprint_hash ? existing.device_id : "",
|
|
4973
|
+
fingerprint_hash: fingerprint.fingerprint_hash,
|
|
4974
|
+
private_key_pem: keyPair.privateKeyPem,
|
|
4975
|
+
public_key_pem: keyPair.publicKeyPem,
|
|
4976
|
+
label,
|
|
4977
|
+
platform: fingerprint.platform,
|
|
4978
|
+
arch: fingerprint.arch,
|
|
4979
|
+
hostname_hash: fingerprint.hostname_hash,
|
|
4980
|
+
registered_at: existing?.registered_at
|
|
4981
|
+
};
|
|
4982
|
+
return {
|
|
4983
|
+
device,
|
|
4984
|
+
registration: {
|
|
4985
|
+
fingerprint_hash: fingerprint.fingerprint_hash,
|
|
4986
|
+
public_key: keyPair.publicKeyPem,
|
|
4987
|
+
label,
|
|
4988
|
+
platform: fingerprint.platform,
|
|
4989
|
+
arch: fingerprint.arch,
|
|
4990
|
+
hostname_hash: fingerprint.hostname_hash,
|
|
4991
|
+
client_name: EMBED_CLIENT_NAME,
|
|
4992
|
+
client_version: EMBED_CLIENT_VERSION,
|
|
4993
|
+
metadata: {
|
|
4994
|
+
fingerprint_version: "v1",
|
|
4995
|
+
fingerprint_source: fingerprint.source
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
};
|
|
4999
|
+
}
|
|
5000
|
+
function generateLocalDeviceKeyPair() {
|
|
5001
|
+
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
|
|
5002
|
+
return {
|
|
5003
|
+
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
5004
|
+
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString()
|
|
5005
|
+
};
|
|
5006
|
+
}
|
|
5007
|
+
async function registerLocalDevice(token, body) {
|
|
3552
5008
|
try {
|
|
3553
|
-
const
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
source: "file",
|
|
3558
|
-
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : undefined
|
|
5009
|
+
const bodyText = JSON.stringify(body);
|
|
5010
|
+
const headers = {
|
|
5011
|
+
"content-type": "application/json",
|
|
5012
|
+
authorization: `Bearer ${token}`
|
|
3559
5013
|
};
|
|
5014
|
+
addCloudRequestSignature(headers, "POST", "/v1/me/devices/register", bodyText, token);
|
|
5015
|
+
const response = await fetch(`${serviceBaseUrl(DEFAULT_CLOUD_API_URL)}/v1/me/devices/register`, {
|
|
5016
|
+
method: "POST",
|
|
5017
|
+
headers,
|
|
5018
|
+
body: bodyText
|
|
5019
|
+
});
|
|
5020
|
+
const parsed = await response.json();
|
|
5021
|
+
return enrichCloudAuthFailure(parsed, true);
|
|
5022
|
+
}
|
|
5023
|
+
catch (error) {
|
|
5024
|
+
return fail("cloud_api_unreachable", error instanceof Error ? error.message : String(error), {
|
|
5025
|
+
remediation: `Check that embed cloud-api is running at ${DEFAULT_CLOUD_API_URL}. Start it with: npm run cloud-api`
|
|
5026
|
+
});
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
async function localHardwareFingerprint() {
|
|
5030
|
+
cachedLocalHardwareFingerprint ??= localHardwareFingerprintUncached();
|
|
5031
|
+
return await cachedLocalHardwareFingerprint;
|
|
5032
|
+
}
|
|
5033
|
+
async function localHardwareFingerprintUncached() {
|
|
5034
|
+
const platformName = platform();
|
|
5035
|
+
const archName = arch();
|
|
5036
|
+
const raw = await localHardwareId(platformName);
|
|
5037
|
+
const fingerprintHash = createHash("sha256")
|
|
5038
|
+
.update(`embed-labs:device:v1:${platformName}:${raw.value}`)
|
|
5039
|
+
.digest("hex");
|
|
5040
|
+
const hostnameHash = createHash("sha256")
|
|
5041
|
+
.update(`embed-labs:hostname:v1:${hostname()}`)
|
|
5042
|
+
.digest("hex");
|
|
5043
|
+
return {
|
|
5044
|
+
fingerprint_hash: fingerprintHash,
|
|
5045
|
+
platform: platformName,
|
|
5046
|
+
arch: archName,
|
|
5047
|
+
hostname_hash: hostnameHash,
|
|
5048
|
+
source: raw.source
|
|
5049
|
+
};
|
|
5050
|
+
}
|
|
5051
|
+
async function localHardwareId(platformName) {
|
|
5052
|
+
if (platformName === "darwin") {
|
|
5053
|
+
const result = await runLocalProcess("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
|
|
5054
|
+
const match = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(result.stdout);
|
|
5055
|
+
if (match?.[1]) {
|
|
5056
|
+
return { value: match[1], source: "macos_ioplatformuuid" };
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
if (platformName === "win32") {
|
|
5060
|
+
const result = await runLocalProcess("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
|
|
5061
|
+
const match = /MachineGuid\s+REG_\w+\s+([^\r\n]+)/.exec(result.stdout);
|
|
5062
|
+
if (match?.[1]?.trim()) {
|
|
5063
|
+
return { value: match[1].trim(), source: "windows_machineguid" };
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
if (platformName === "linux") {
|
|
5067
|
+
for (const pathValue of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
5068
|
+
try {
|
|
5069
|
+
const value = (await readFile(pathValue, "utf8")).trim();
|
|
5070
|
+
if (value) {
|
|
5071
|
+
return { value, source: `linux:${pathValue}` };
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
catch {
|
|
5075
|
+
// Try the next stable machine id location.
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
const generated = await localGeneratedInstallId();
|
|
5080
|
+
return { value: generated, source: "generated_install_id" };
|
|
5081
|
+
}
|
|
5082
|
+
async function localGeneratedInstallId() {
|
|
5083
|
+
try {
|
|
5084
|
+
const parsed = JSON.parse(await readFile(DEFAULT_DEVICE_FILE, "utf8"));
|
|
5085
|
+
if (typeof parsed.generated_install_id === "string" && parsed.generated_install_id.trim()) {
|
|
5086
|
+
return parsed.generated_install_id.trim();
|
|
5087
|
+
}
|
|
3560
5088
|
}
|
|
3561
5089
|
catch {
|
|
3562
|
-
|
|
5090
|
+
// Fall through and create a local-only fallback id.
|
|
3563
5091
|
}
|
|
5092
|
+
const generated = `install_${randomBytes(24).toString("base64url")}`;
|
|
5093
|
+
await mkdir(dirname(DEFAULT_DEVICE_FILE), { recursive: true });
|
|
5094
|
+
await writeFile(DEFAULT_DEVICE_FILE, `${JSON.stringify({ generated_install_id: generated, created_at: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
5095
|
+
await chmod(DEFAULT_DEVICE_FILE, 0o600).catch(() => undefined);
|
|
5096
|
+
return generated;
|
|
3564
5097
|
}
|
|
3565
5098
|
function accountCreateBody(parsed) {
|
|
3566
5099
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "email", "display-name"]);
|
|
@@ -3716,6 +5249,80 @@ function usageRecordBody(parsed) {
|
|
|
3716
5249
|
created_at: createdAtResult.value
|
|
3717
5250
|
});
|
|
3718
5251
|
}
|
|
5252
|
+
function mcpToolEventBody(parsed) {
|
|
5253
|
+
const unknownFlag = firstUnknownFlag(parsed, [
|
|
5254
|
+
"json",
|
|
5255
|
+
"account",
|
|
5256
|
+
"account-id",
|
|
5257
|
+
"tool",
|
|
5258
|
+
"client",
|
|
5259
|
+
"mode",
|
|
5260
|
+
"server-model-used",
|
|
5261
|
+
"success",
|
|
5262
|
+
"local-device-id",
|
|
5263
|
+
"local_device_id",
|
|
5264
|
+
"request-id",
|
|
5265
|
+
"duration-ms",
|
|
5266
|
+
"input-summary",
|
|
5267
|
+
"output-summary"
|
|
5268
|
+
]);
|
|
5269
|
+
if (unknownFlag) {
|
|
5270
|
+
return `Unknown flag --${unknownFlag}. ${MCP_TOOL_EVENT_USAGE}`;
|
|
5271
|
+
}
|
|
5272
|
+
const extra = parsed.command.slice(2);
|
|
5273
|
+
if (extra.length > 0) {
|
|
5274
|
+
return `Unexpected argument: ${extra[0]}. ${MCP_TOOL_EVENT_USAGE}`;
|
|
5275
|
+
}
|
|
5276
|
+
const toolResult = optionalTrimmedStringFlag(parsed, "tool");
|
|
5277
|
+
if (toolResult.error)
|
|
5278
|
+
return toolResult.error;
|
|
5279
|
+
if (!toolResult.value)
|
|
5280
|
+
return MCP_TOOL_EVENT_USAGE;
|
|
5281
|
+
const accountResult = optionalTrimmedStringAliasFlag(parsed, ["account", "account-id"], "account or account-id");
|
|
5282
|
+
if (accountResult.error)
|
|
5283
|
+
return accountResult.error;
|
|
5284
|
+
const clientResult = optionalTrimmedStringFlag(parsed, "client");
|
|
5285
|
+
if (clientResult.error)
|
|
5286
|
+
return clientResult.error;
|
|
5287
|
+
const modeResult = optionalTrimmedStringFlag(parsed, "mode");
|
|
5288
|
+
if (modeResult.error)
|
|
5289
|
+
return modeResult.error;
|
|
5290
|
+
const localDeviceResult = optionalTrimmedStringAliasFlag(parsed, ["local-device-id", "local_device_id"], "local-device-id");
|
|
5291
|
+
if (localDeviceResult.error)
|
|
5292
|
+
return localDeviceResult.error;
|
|
5293
|
+
const requestIdResult = optionalTrimmedStringFlag(parsed, "request-id");
|
|
5294
|
+
if (requestIdResult.error)
|
|
5295
|
+
return requestIdResult.error;
|
|
5296
|
+
const inputSummaryResult = optionalTrimmedStringFlag(parsed, "input-summary");
|
|
5297
|
+
if (inputSummaryResult.error)
|
|
5298
|
+
return inputSummaryResult.error;
|
|
5299
|
+
const outputSummaryResult = optionalTrimmedStringFlag(parsed, "output-summary");
|
|
5300
|
+
if (outputSummaryResult.error)
|
|
5301
|
+
return outputSummaryResult.error;
|
|
5302
|
+
const durationResult = optionalNonNegativeIntegerFlag(parsed, "duration-ms");
|
|
5303
|
+
if (durationResult.error)
|
|
5304
|
+
return durationResult.error;
|
|
5305
|
+
const serverModelUsed = optionalBooleanFlag(parsed, "server-model-used");
|
|
5306
|
+
if (typeof serverModelUsed === "string")
|
|
5307
|
+
return serverModelUsed;
|
|
5308
|
+
const success = optionalBooleanFlag(parsed, "success");
|
|
5309
|
+
if (typeof success === "string")
|
|
5310
|
+
return success;
|
|
5311
|
+
return compactBody({
|
|
5312
|
+
account_id: accountResult.value,
|
|
5313
|
+
tool_name: toolResult.value,
|
|
5314
|
+
client: clientResult.value,
|
|
5315
|
+
mode: modeResult.value,
|
|
5316
|
+
local_device_id: localDeviceResult.value,
|
|
5317
|
+
server_model_used: serverModelUsed,
|
|
5318
|
+
success,
|
|
5319
|
+
request_id: requestIdResult.value,
|
|
5320
|
+
duration_ms: durationResult.value,
|
|
5321
|
+
input_summary: inputSummaryResult.value,
|
|
5322
|
+
output_summary: outputSummaryResult.value,
|
|
5323
|
+
metadata: localDeviceResult.value ? { local_device_id: localDeviceResult.value } : undefined
|
|
5324
|
+
});
|
|
5325
|
+
}
|
|
3719
5326
|
function usageSummaryRequest(parsed) {
|
|
3720
5327
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "account", "account-id", "api-key-id", "from", "to"]);
|
|
3721
5328
|
if (unknownFlag) {
|
|
@@ -4051,6 +5658,29 @@ function billingSnapshotListRequest(parsed) {
|
|
|
4051
5658
|
}
|
|
4052
5659
|
return { path: `/v1/accounts/${encodeURIComponent(accountResult.value)}/billing/snapshots` };
|
|
4053
5660
|
}
|
|
5661
|
+
function localToolchainListRequest(parsed, usage = LOCAL_TOOLCHAIN_LIST_USAGE) {
|
|
5662
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "install-root"]);
|
|
5663
|
+
if (unknownFlag) {
|
|
5664
|
+
return `Unknown flag --${unknownFlag}. ${usage}`;
|
|
5665
|
+
}
|
|
5666
|
+
const extra = parsed.command.slice(3);
|
|
5667
|
+
if (extra.length > 0) {
|
|
5668
|
+
return `Unexpected argument: ${extra[0]}. ${usage}`;
|
|
5669
|
+
}
|
|
5670
|
+
const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
|
|
5671
|
+
if (board.error)
|
|
5672
|
+
return board.error;
|
|
5673
|
+
const channel = optionalTrimmedStringFlag(parsed, "channel");
|
|
5674
|
+
if (channel.error)
|
|
5675
|
+
return channel.error;
|
|
5676
|
+
const metadataRoot = optionalTrimmedStringFlag(parsed, "metadata-root");
|
|
5677
|
+
if (metadataRoot.error)
|
|
5678
|
+
return metadataRoot.error;
|
|
5679
|
+
const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
|
|
5680
|
+
if (installRoot.error)
|
|
5681
|
+
return installRoot.error;
|
|
5682
|
+
return { boardId: board.value, channel: channel.value, metadataRoot: metadataRoot.value, installRoot: installRoot.value };
|
|
5683
|
+
}
|
|
4054
5684
|
function localToolchainLatestRequest(parsed) {
|
|
4055
5685
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root"]);
|
|
4056
5686
|
if (unknownFlag) {
|
|
@@ -4086,7 +5716,7 @@ function localToolchainCurrentRequest(parsed) {
|
|
|
4086
5716
|
return { installRoot: installRoot.value };
|
|
4087
5717
|
}
|
|
4088
5718
|
function localToolchainInstallRequest(parsed) {
|
|
4089
|
-
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "force"]);
|
|
5719
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "channel", "metadata-root", "source-url", "source-release-root", "install-root", "mode", "force"]);
|
|
4090
5720
|
if (unknownFlag) {
|
|
4091
5721
|
return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_INSTALL_USAGE}`;
|
|
4092
5722
|
}
|
|
@@ -4115,6 +5745,9 @@ function localToolchainInstallRequest(parsed) {
|
|
|
4115
5745
|
const installRoot = optionalTrimmedStringFlag(parsed, "install-root");
|
|
4116
5746
|
if (installRoot.error)
|
|
4117
5747
|
return installRoot.error;
|
|
5748
|
+
const mode = optionalTrimmedStringFlag(parsed, "mode");
|
|
5749
|
+
if (mode.error)
|
|
5750
|
+
return mode.error;
|
|
4118
5751
|
return {
|
|
4119
5752
|
boardId: board.value,
|
|
4120
5753
|
channel: channel.value,
|
|
@@ -4122,11 +5755,12 @@ function localToolchainInstallRequest(parsed) {
|
|
|
4122
5755
|
sourceUrl: sourceUrl.value,
|
|
4123
5756
|
sourceReleaseRoot: sourceReleaseRoot.value,
|
|
4124
5757
|
installRoot: installRoot.value,
|
|
5758
|
+
mode: mode.value,
|
|
4125
5759
|
force: booleanFlag(parsed, "force")
|
|
4126
5760
|
};
|
|
4127
5761
|
}
|
|
4128
5762
|
function localToolchainValidateRequest(parsed) {
|
|
4129
|
-
const unknownFlag = firstUnknownFlag(parsed, ["json", "release-root"]);
|
|
5763
|
+
const unknownFlag = firstUnknownFlag(parsed, ["json", "board", "board-id", "release-root", "mode"]);
|
|
4130
5764
|
if (unknownFlag) {
|
|
4131
5765
|
return `Unknown flag --${unknownFlag}. ${LOCAL_TOOLCHAIN_VALIDATE_USAGE}`;
|
|
4132
5766
|
}
|
|
@@ -4138,7 +5772,15 @@ function localToolchainValidateRequest(parsed) {
|
|
|
4138
5772
|
if (releaseRoot.error) {
|
|
4139
5773
|
return releaseRoot.error;
|
|
4140
5774
|
}
|
|
4141
|
-
|
|
5775
|
+
const board = optionalTrimmedStringAliasFlag(parsed, ["board", "board-id"], "board or board-id");
|
|
5776
|
+
if (board.error) {
|
|
5777
|
+
return board.error;
|
|
5778
|
+
}
|
|
5779
|
+
const mode = optionalTrimmedStringFlag(parsed, "mode");
|
|
5780
|
+
if (mode.error) {
|
|
5781
|
+
return mode.error;
|
|
5782
|
+
}
|
|
5783
|
+
return { releaseRoot: releaseRoot.value, mode: mode.value, boardId: board.value };
|
|
4142
5784
|
}
|
|
4143
5785
|
function localCompileTaishanPiRequest(parsed, auth) {
|
|
4144
5786
|
const unknownFlag = firstUnknownFlag(parsed, ["json", "source", "output", "release-root", "account", "account-id"]);
|
|
@@ -4223,7 +5865,9 @@ function localToolchainAuthContext(auth, accountId) {
|
|
|
4223
5865
|
authenticated: auth.authenticated,
|
|
4224
5866
|
profile: auth.profile,
|
|
4225
5867
|
source: auth.source,
|
|
4226
|
-
account_id: accountId
|
|
5868
|
+
account_id: accountId,
|
|
5869
|
+
api_key_id: auth.api_key_id,
|
|
5870
|
+
device_id: auth.device_id
|
|
4227
5871
|
};
|
|
4228
5872
|
}
|
|
4229
5873
|
function usageEventsRequest(parsed) {
|
|
@@ -4633,6 +6277,35 @@ function renderPluginList(result) {
|
|
|
4633
6277
|
`install="${plugin.install_command}"`
|
|
4634
6278
|
].filter(Boolean).join(" ")).join("\n");
|
|
4635
6279
|
}
|
|
6280
|
+
function renderPluginUpdateCheck(result) {
|
|
6281
|
+
const lines = [
|
|
6282
|
+
`release_url=${result.release_url}`,
|
|
6283
|
+
result.latest_version ? `latest_version=${result.latest_version}` : ""
|
|
6284
|
+
].filter(Boolean);
|
|
6285
|
+
if (result.release_notes.length > 0) {
|
|
6286
|
+
lines.push("release_notes:");
|
|
6287
|
+
for (const note of result.release_notes) {
|
|
6288
|
+
lines.push(` - ${note}`);
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
for (const plugin of result.plugins) {
|
|
6292
|
+
lines.push("");
|
|
6293
|
+
lines.push(`${plugin.display_name} (${plugin.id})`);
|
|
6294
|
+
lines.push(` installed=${plugin.installed}`);
|
|
6295
|
+
lines.push(` installed_version=${plugin.installed_version ?? "none"}`);
|
|
6296
|
+
lines.push(` latest_version=${plugin.latest_version ?? "unknown"}`);
|
|
6297
|
+
lines.push(` update_available=${plugin.update_available}`);
|
|
6298
|
+
lines.push(` target=${plugin.target_path}`);
|
|
6299
|
+
lines.push(` update_command=${plugin.update_command}`);
|
|
6300
|
+
if (plugin.release_file) {
|
|
6301
|
+
lines.push(` release_file=${plugin.release_file}`);
|
|
6302
|
+
}
|
|
6303
|
+
for (const note of plugin.notes) {
|
|
6304
|
+
lines.push(` note=${note}`);
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
return lines.join("\n");
|
|
6308
|
+
}
|
|
4636
6309
|
function renderPluginInstall(result) {
|
|
4637
6310
|
const lines = ["Installed plugins:"];
|
|
4638
6311
|
for (const item of result.installed) {
|
|
@@ -4654,6 +6327,15 @@ function renderPluginInstall(result) {
|
|
|
4654
6327
|
if (item.mcp_warning) {
|
|
4655
6328
|
lines.push(` warning=${item.mcp_warning}`);
|
|
4656
6329
|
}
|
|
6330
|
+
if (item.marketplace_registered !== undefined) {
|
|
6331
|
+
lines.push(` codex_marketplace_registered=${item.marketplace_registered}`);
|
|
6332
|
+
}
|
|
6333
|
+
if (item.marketplace_path) {
|
|
6334
|
+
lines.push(` codex_marketplace=${item.marketplace_path}`);
|
|
6335
|
+
}
|
|
6336
|
+
if (item.marketplace_warning) {
|
|
6337
|
+
lines.push(` warning=${item.marketplace_warning}`);
|
|
6338
|
+
}
|
|
4657
6339
|
}
|
|
4658
6340
|
return lines.join("\n");
|
|
4659
6341
|
}
|
|
@@ -4685,9 +6367,53 @@ function renderAgentRunResult(result) {
|
|
|
4685
6367
|
return lines.join("\n");
|
|
4686
6368
|
}
|
|
4687
6369
|
function renderAuthStatus(status) {
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
6370
|
+
if (!status.authenticated) {
|
|
6371
|
+
return `Not authenticated profile=${status.profile}`;
|
|
6372
|
+
}
|
|
6373
|
+
return [
|
|
6374
|
+
`Authenticated profile=${status.profile}${status.source ? ` source=${status.source}` : ""}`,
|
|
6375
|
+
status.account_id ? `account=${status.account_id}` : "",
|
|
6376
|
+
status.api_key_id ? `api_key=${status.api_key_id}` : "",
|
|
6377
|
+
status.device_id ? `device=${status.device_id}` : "device=not_registered",
|
|
6378
|
+
status.device_label ? `device_label=${status.device_label}` : "",
|
|
6379
|
+
status.device_integrity ? `device_integrity=${status.device_integrity}` : "",
|
|
6380
|
+
status.device_private_key_configured === false ? "device_private_key=missing" : ""
|
|
6381
|
+
].filter(Boolean).join("\n");
|
|
6382
|
+
}
|
|
6383
|
+
function renderAuthDeviceStatus(status) {
|
|
6384
|
+
const lines = [renderAuthStatus(status.local)];
|
|
6385
|
+
if (status.remote) {
|
|
6386
|
+
const activeCount = status.remote.devices.filter((device) => device.status === "active").length;
|
|
6387
|
+
lines.push(`remote_devices=${activeCount}/${status.remote.device_limit}`);
|
|
6388
|
+
const localDevice = status.local.device_id
|
|
6389
|
+
? status.remote.devices.find((device) => device.device_id === status.local.device_id)
|
|
6390
|
+
: undefined;
|
|
6391
|
+
if (localDevice) {
|
|
6392
|
+
lines.push(`remote_current=${renderAuthDevice(localDevice)}`);
|
|
6393
|
+
}
|
|
6394
|
+
}
|
|
6395
|
+
return lines.join("\n");
|
|
6396
|
+
}
|
|
6397
|
+
function renderAuthDeviceList(result) {
|
|
6398
|
+
if (result.devices.length === 0) {
|
|
6399
|
+
return `No registered devices. device_limit=${result.device_limit}`;
|
|
6400
|
+
}
|
|
6401
|
+
return [
|
|
6402
|
+
`device_limit=${result.device_limit}`,
|
|
6403
|
+
...result.devices.map(renderAuthDevice)
|
|
6404
|
+
].join("\n");
|
|
6405
|
+
}
|
|
6406
|
+
function renderAuthDevice(device) {
|
|
6407
|
+
return [
|
|
6408
|
+
`${device.device_id} account=${device.account_id}`,
|
|
6409
|
+
device.api_key_id ? `api_key=${device.api_key_id}` : "",
|
|
6410
|
+
`status=${device.status}`,
|
|
6411
|
+
device.label ? `label=${device.label}` : "",
|
|
6412
|
+
device.platform ? `platform=${device.platform}` : "",
|
|
6413
|
+
device.arch ? `arch=${device.arch}` : "",
|
|
6414
|
+
`last_seen_at=${device.last_seen_at}`,
|
|
6415
|
+
device.revoked_at ? `revoked_at=${device.revoked_at}` : ""
|
|
6416
|
+
].filter(Boolean).join(" ");
|
|
4691
6417
|
}
|
|
4692
6418
|
function renderAccount(account) {
|
|
4693
6419
|
return [
|
|
@@ -4737,6 +6463,20 @@ function renderUsageRecord(record) {
|
|
|
4737
6463
|
`created_at=${record.created_at}`
|
|
4738
6464
|
].filter(Boolean).join(" ");
|
|
4739
6465
|
}
|
|
6466
|
+
function renderMcpToolEvent(event) {
|
|
6467
|
+
return [
|
|
6468
|
+
`${event.event_id} tool=${event.tool_name}`,
|
|
6469
|
+
event.account_id ? `account=${event.account_id}` : "",
|
|
6470
|
+
event.api_key_id ? `api_key=${event.api_key_id}` : "",
|
|
6471
|
+
`client=${event.client}`,
|
|
6472
|
+
`mode=${event.mode}`,
|
|
6473
|
+
`server_model_used=${event.server_model_used}`,
|
|
6474
|
+
`success=${event.success}`,
|
|
6475
|
+
event.request_id ? `request=${event.request_id}` : "",
|
|
6476
|
+
event.duration_ms !== undefined ? `duration_ms=${event.duration_ms}` : "",
|
|
6477
|
+
`created_at=${event.created_at}`
|
|
6478
|
+
].filter(Boolean).join(" ");
|
|
6479
|
+
}
|
|
4740
6480
|
function renderUsageSummary(summary) {
|
|
4741
6481
|
const lines = [
|
|
4742
6482
|
summary.account_id ? `account=${summary.account_id}` : "",
|
|
@@ -5048,6 +6788,20 @@ function renderBoardKnowledge(data) {
|
|
|
5048
6788
|
`title=${file.title}`
|
|
5049
6789
|
].join(" ")).join("\n");
|
|
5050
6790
|
}
|
|
6791
|
+
function renderBoardKnowledgeSearch(data) {
|
|
6792
|
+
const result = data;
|
|
6793
|
+
const matches = Array.isArray(result.matches) ? result.matches : [];
|
|
6794
|
+
if (matches.length === 0) {
|
|
6795
|
+
return "No matching board knowledge snippets.";
|
|
6796
|
+
}
|
|
6797
|
+
return matches.map((match, index) => [
|
|
6798
|
+
`#${index + 1}`,
|
|
6799
|
+
`${match.source}:${match.path}`,
|
|
6800
|
+
`score=${match.score}`,
|
|
6801
|
+
`title=${match.title}`,
|
|
6802
|
+
`excerpt=${match.excerpt.replace(/\s+/g, " ").trim()}`
|
|
6803
|
+
].join(" ")).join("\n");
|
|
6804
|
+
}
|
|
5051
6805
|
function renderBoardKnowledgeFile(file) {
|
|
5052
6806
|
return [
|
|
5053
6807
|
`template=${file.template_id}`,
|
|
@@ -5260,14 +7014,150 @@ function renderBuildWorkspaceSourcePatch(result) {
|
|
|
5260
7014
|
}
|
|
5261
7015
|
return lines.join("\n");
|
|
5262
7016
|
}
|
|
7017
|
+
function renderLocalToolchainList(result) {
|
|
7018
|
+
const installedCount = result.environments.filter((environment) => !!environment.installed).length;
|
|
7019
|
+
const availableCount = result.environments.filter((environment) => environment.status === "available").length;
|
|
7020
|
+
const updateCount = result.environments.filter((environment) => environment.status === "update_available").length;
|
|
7021
|
+
const lines = [
|
|
7022
|
+
`Local development environments: ${result.environments.length}`,
|
|
7023
|
+
`installed=${installedCount} available=${availableCount} updates=${updateCount}`,
|
|
7024
|
+
`host=${result.host}`,
|
|
7025
|
+
`channel=${result.channel}`,
|
|
7026
|
+
result.metadata_source === "local_override" ? `metadata_override=${result.metadata_root}` : "metadata=production/built-in",
|
|
7027
|
+
`install_root=${result.install_root}`,
|
|
7028
|
+
`registry=${result.registry_path}`
|
|
7029
|
+
];
|
|
7030
|
+
for (const environment of result.environments) {
|
|
7031
|
+
lines.push("");
|
|
7032
|
+
lines.push(`${environment.display_name} (${environment.board_id})`);
|
|
7033
|
+
lines.push(` status=${localToolchainStatusLabel(environment.status)}`);
|
|
7034
|
+
lines.push(` latest=${environment.latest.version}`);
|
|
7035
|
+
if (environment.installed) {
|
|
7036
|
+
lines.push(` installed=${environment.installed.version ?? "unknown"} mode=${environment.installed.mode ?? "unknown"}`);
|
|
7037
|
+
if (environment.installed.release_root) {
|
|
7038
|
+
lines.push(` release_root=${environment.installed.release_root}`);
|
|
7039
|
+
}
|
|
7040
|
+
}
|
|
7041
|
+
lines.push(` install_modes=${environment.install_modes.join(",")}`);
|
|
7042
|
+
lines.push(` install_command=${environment.install_command}`);
|
|
7043
|
+
if (environment.status === "update_available") {
|
|
7044
|
+
lines.push(` update_command=${environment.update_command}`);
|
|
7045
|
+
}
|
|
7046
|
+
if (environment.components?.length) {
|
|
7047
|
+
lines.push(` component_catalog=${localToolchainComponentSummary(environment.components)}`);
|
|
7048
|
+
if (environment.latest.default_mode) {
|
|
7049
|
+
lines.push(` default_mode_download=${environment.latest.default_mode}: ${localToolchainComponentSummaryForMode(environment.components, environment.latest.default_mode)}`);
|
|
7050
|
+
}
|
|
7051
|
+
if (environment.installed?.mode && environment.installed.mode !== environment.latest.default_mode) {
|
|
7052
|
+
lines.push(` installed_mode_components=${environment.installed.mode}: ${localToolchainComponentSummaryForMode(environment.components, environment.installed.mode)}`);
|
|
7053
|
+
}
|
|
7054
|
+
lines.push(` mode_downloads=${localToolchainModeSummaries(environment.install_modes, environment.components).join("; ")}`);
|
|
7055
|
+
lines.push(` package_groups=${localToolchainComponentGroups(environment.components).join(", ")}`);
|
|
7056
|
+
lines.push(` detail_command=embedlabs local toolchain latest --board ${environment.board_id}`);
|
|
7057
|
+
}
|
|
7058
|
+
if (environment.notes.length > 0) {
|
|
7059
|
+
for (const note of environment.notes) {
|
|
7060
|
+
lines.push(` note=${note}`);
|
|
7061
|
+
}
|
|
7062
|
+
}
|
|
7063
|
+
}
|
|
7064
|
+
return lines.join("\n");
|
|
7065
|
+
}
|
|
7066
|
+
function localToolchainStatusLabel(status) {
|
|
7067
|
+
if (status === "installed")
|
|
7068
|
+
return "installed/已安装";
|
|
7069
|
+
if (status === "available")
|
|
7070
|
+
return "available/可安装";
|
|
7071
|
+
if (status === "update_available")
|
|
7072
|
+
return "update_available/可更新";
|
|
7073
|
+
if (status === "unsupported_host")
|
|
7074
|
+
return "unsupported_host/当前系统暂不支持";
|
|
7075
|
+
return status;
|
|
7076
|
+
}
|
|
7077
|
+
function localToolchainComponentSummary(components) {
|
|
7078
|
+
const totalBytes = components.reduce((total, component) => total + component.size_bytes, 0);
|
|
7079
|
+
return `${components.length} components, ${formatByteSize(totalBytes)}`;
|
|
7080
|
+
}
|
|
7081
|
+
function localToolchainComponentSummaryForMode(components, mode) {
|
|
7082
|
+
return localToolchainComponentSummary(localToolchainComponentsForMode(components, mode));
|
|
7083
|
+
}
|
|
7084
|
+
function localToolchainModeSummaries(modes, components) {
|
|
7085
|
+
return modes.map((mode) => `${mode}=${localToolchainComponentSummaryForMode(components, mode)}`);
|
|
7086
|
+
}
|
|
7087
|
+
function localToolchainComponentsForMode(components, mode) {
|
|
7088
|
+
return components.filter((component) => {
|
|
7089
|
+
if (!component.install_modes?.length) {
|
|
7090
|
+
return true;
|
|
7091
|
+
}
|
|
7092
|
+
return component.install_modes.includes(mode);
|
|
7093
|
+
});
|
|
7094
|
+
}
|
|
7095
|
+
function localToolchainComponentGroups(components) {
|
|
7096
|
+
const groups = new Set();
|
|
7097
|
+
for (const component of components) {
|
|
7098
|
+
const text = `${component.id} ${component.role ?? ""}`.toLowerCase();
|
|
7099
|
+
if (text.includes("arm-none-eabi") || text.includes("bare-metal") || text.includes("compiler"))
|
|
7100
|
+
groups.add("compiler/ARM 裸机编译器");
|
|
7101
|
+
if (text.includes("pico-sdk") || text.includes("sdk-core"))
|
|
7102
|
+
groups.add("pico-sdk/C/C++ SDK");
|
|
7103
|
+
if (text.includes("sysroot") || text.includes("cross"))
|
|
7104
|
+
groups.add("sysroot/交叉运行库");
|
|
7105
|
+
if (text.includes("qt"))
|
|
7106
|
+
groups.add("qt/Qt 应用支持");
|
|
7107
|
+
if (text.includes("rockchip") || text.includes("boot") || text.includes("resource"))
|
|
7108
|
+
groups.add("boot-flash/启动与烧写工具");
|
|
7109
|
+
if (text.includes("image") || text.includes("rootfs"))
|
|
7110
|
+
groups.add("images/镜像资源");
|
|
7111
|
+
if (text.includes("initial-firmware"))
|
|
7112
|
+
groups.add("initial-firmware/初始化镜像");
|
|
7113
|
+
if (text.includes("rp2350-monitor"))
|
|
7114
|
+
groups.add("rp2350-monitor/可选硬件监控镜像");
|
|
7115
|
+
if (text.includes("meta"))
|
|
7116
|
+
groups.add("metadata/知识与脚本元数据");
|
|
7117
|
+
}
|
|
7118
|
+
return groups.size > 0 ? [...groups] : ["runtime/运行时工具"];
|
|
7119
|
+
}
|
|
7120
|
+
function formatByteSize(bytes) {
|
|
7121
|
+
if (!Number.isFinite(bytes) || bytes < 0) {
|
|
7122
|
+
return "unknown size";
|
|
7123
|
+
}
|
|
7124
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
7125
|
+
let value = bytes;
|
|
7126
|
+
let unit = 0;
|
|
7127
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
7128
|
+
value /= 1024;
|
|
7129
|
+
unit += 1;
|
|
7130
|
+
}
|
|
7131
|
+
const fixed = unit === 0 || value >= 10 ? value.toFixed(0) : value.toFixed(1);
|
|
7132
|
+
return `${fixed} ${units[unit]}`;
|
|
7133
|
+
}
|
|
5263
7134
|
function renderLocalToolchainLatest(result) {
|
|
5264
7135
|
const lines = [
|
|
5265
7136
|
`board=${result.board_id}`,
|
|
5266
7137
|
`channel=${result.channel}`,
|
|
5267
7138
|
`version=${result.version}`,
|
|
5268
7139
|
`host=${result.host}`,
|
|
5269
|
-
result.metadata_root ? `metadata_root=${result.metadata_root}` : "metadata=built-in"
|
|
5270
|
-
|
|
7140
|
+
result.metadata_root ? `metadata_root=${result.metadata_root}` : "metadata=built-in",
|
|
7141
|
+
result.download?.source_url ? `download=${result.download.mirror_kind}:${result.download.source_url}` : "",
|
|
7142
|
+
result.download?.archive ? `archive_sha256=${result.download.archive.sha256}` : "",
|
|
7143
|
+
result.download?.archive ? `archive_size_bytes=${result.download.archive.size_bytes}` : "",
|
|
7144
|
+
result.download?.components?.length ? `components=${result.download.components.length}` : "",
|
|
7145
|
+
result.download?.default_mode ? `default_mode=${result.download.default_mode}` : "",
|
|
7146
|
+
result.download_error ? `download_error=${result.download_error}` : ""
|
|
7147
|
+
].filter(Boolean);
|
|
7148
|
+
if (result.download?.components?.length) {
|
|
7149
|
+
const modes = [...new Set(result.download.components.flatMap((component) => component.install_modes ?? ["all"]))].filter((mode) => mode !== "all");
|
|
7150
|
+
if (result.download.default_mode) {
|
|
7151
|
+
lines.push(`default_mode_download=${result.download.default_mode}: ${localToolchainDownloadComponentSummaryForMode(result.download.components, result.download.default_mode)}`);
|
|
7152
|
+
}
|
|
7153
|
+
if (modes.length > 0) {
|
|
7154
|
+
lines.push(`mode_downloads=${modes.map((mode) => `${mode}=${localToolchainDownloadComponentSummaryForMode(result.download?.components ?? [], mode)}`).join("; ")}`);
|
|
7155
|
+
}
|
|
7156
|
+
lines.push("download_components:");
|
|
7157
|
+
for (const component of result.download.components) {
|
|
7158
|
+
lines.push(` ${component.id}@${component.version} modes=${component.install_modes?.join(",") || "all"} size=${formatByteSize(component.archive.size_bytes)} file=${component.archive.file}`);
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
5271
7161
|
if (result.packages.length > 0) {
|
|
5272
7162
|
lines.push("packages:");
|
|
5273
7163
|
for (const pkg of result.packages) {
|
|
@@ -5276,6 +7166,11 @@ function renderLocalToolchainLatest(result) {
|
|
|
5276
7166
|
}
|
|
5277
7167
|
return lines.join("\n");
|
|
5278
7168
|
}
|
|
7169
|
+
function localToolchainDownloadComponentSummaryForMode(components, mode) {
|
|
7170
|
+
const selected = localToolchainComponentsForMode(components ?? [], mode);
|
|
7171
|
+
const totalBytes = selected.reduce((total, component) => total + component.archive.size_bytes, 0);
|
|
7172
|
+
return `${selected.length} components, ${formatByteSize(totalBytes)}`;
|
|
7173
|
+
}
|
|
5279
7174
|
function renderLocalToolchainCurrent(result) {
|
|
5280
7175
|
if (!result.installed) {
|
|
5281
7176
|
return [
|
|
@@ -5290,6 +7185,7 @@ function renderLocalToolchainCurrent(result) {
|
|
|
5290
7185
|
`board=${result.board_id}`,
|
|
5291
7186
|
result.version ? `version=${result.version}` : "",
|
|
5292
7187
|
result.channel ? `channel=${result.channel}` : "",
|
|
7188
|
+
result.mode ? `mode=${result.mode}` : "",
|
|
5293
7189
|
result.release_root ? `release_root=${result.release_root}` : "",
|
|
5294
7190
|
`install_root=${result.install_root}`,
|
|
5295
7191
|
`registry=${result.registry_path}`
|
|
@@ -5302,13 +7198,21 @@ function renderLocalToolchainInstall(result) {
|
|
|
5302
7198
|
`version=${result.version}`,
|
|
5303
7199
|
`channel=${result.channel}`,
|
|
5304
7200
|
`host=${result.host}`,
|
|
7201
|
+
`mode=${result.mode}`,
|
|
5305
7202
|
`install_root=${result.install_root}`,
|
|
5306
7203
|
`release_root=${result.release_root}`,
|
|
5307
7204
|
`registry=${result.registry_path}`,
|
|
5308
7205
|
`source=${result.source.kind}:${result.source.value}`,
|
|
5309
7206
|
result.source.downloaded_path ? `downloaded=${result.source.downloaded_path}` : "",
|
|
7207
|
+
result.source.components?.length ? `components=${result.source.components.length}` : "",
|
|
5310
7208
|
`validation=${result.validation.ok ? "ok" : "failed"}`
|
|
5311
7209
|
].filter(Boolean);
|
|
7210
|
+
if (result.source.components?.length) {
|
|
7211
|
+
lines.push("installed_components:");
|
|
7212
|
+
for (const component of result.source.components) {
|
|
7213
|
+
lines.push(` ${component.id}@${component.version} ${component.mirror_kind || ""} bytes=${component.size_bytes}`);
|
|
7214
|
+
}
|
|
7215
|
+
}
|
|
5312
7216
|
if (result.installed_paths.length > 0) {
|
|
5313
7217
|
lines.push("installed_paths:");
|
|
5314
7218
|
for (const installedPath of result.installed_paths) {
|
|
@@ -5327,12 +7231,26 @@ function renderLocalToolchainValidation(result) {
|
|
|
5327
7231
|
const lines = [
|
|
5328
7232
|
result.ok ? "Local toolchain ready." : "Local toolchain not ready.",
|
|
5329
7233
|
`board=${result.board_id}`,
|
|
7234
|
+
`mode=${result.mode}`,
|
|
5330
7235
|
`host=${result.host.platform}/${result.host.arch}`,
|
|
5331
|
-
`release_root=${result.release_root}
|
|
7236
|
+
`release_root=${result.release_root}`,
|
|
7237
|
+
`summary=${result.summary_for_user}`
|
|
5332
7238
|
];
|
|
7239
|
+
if (!result.ok && result.missing_groups.length > 0) {
|
|
7240
|
+
lines.push(`missing_groups=${result.missing_groups.join(", ")}`);
|
|
7241
|
+
}
|
|
7242
|
+
if (result.repair_command) {
|
|
7243
|
+
lines.push(`repair_command=${result.repair_command}`);
|
|
7244
|
+
}
|
|
5333
7245
|
for (const check of result.checked_paths) {
|
|
5334
7246
|
lines.push(`${check.exists ? "ok" : "missing"} ${check.label}: ${check.path}`);
|
|
5335
7247
|
}
|
|
7248
|
+
if (result.path_leaks.length > 0) {
|
|
7249
|
+
lines.push("path_leaks:");
|
|
7250
|
+
for (const leak of result.path_leaks) {
|
|
7251
|
+
lines.push(` ${leak.label}: ${leak.path} contains ${leak.forbidden}`);
|
|
7252
|
+
}
|
|
7253
|
+
}
|
|
5336
7254
|
if (result.notes.length > 0) {
|
|
5337
7255
|
lines.push("notes:");
|
|
5338
7256
|
for (const note of result.notes) {
|
|
@@ -5781,6 +7699,22 @@ function stringFlag(parsed, name) {
|
|
|
5781
7699
|
function booleanFlag(parsed, name) {
|
|
5782
7700
|
return parsed.flags[name] === true;
|
|
5783
7701
|
}
|
|
7702
|
+
function optionalBooleanFlag(parsed, name) {
|
|
7703
|
+
const values = flagValues(parsed, name);
|
|
7704
|
+
if (values.length === 0)
|
|
7705
|
+
return undefined;
|
|
7706
|
+
const value = values[values.length - 1];
|
|
7707
|
+
if (value === true)
|
|
7708
|
+
return true;
|
|
7709
|
+
if (typeof value !== "string")
|
|
7710
|
+
return `--${name} must be true or false.`;
|
|
7711
|
+
const normalized = value.trim().toLowerCase();
|
|
7712
|
+
if (["1", "true", "yes", "y", "on"].includes(normalized))
|
|
7713
|
+
return true;
|
|
7714
|
+
if (["0", "false", "no", "n", "off"].includes(normalized))
|
|
7715
|
+
return false;
|
|
7716
|
+
return `--${name} must be true or false.`;
|
|
7717
|
+
}
|
|
5784
7718
|
function switchFlag(parsed, name) {
|
|
5785
7719
|
const values = flagValues(parsed, name);
|
|
5786
7720
|
for (const value of values) {
|
|
@@ -6079,14 +8013,17 @@ Main workflow:
|
|
|
6079
8013
|
3. Inspect server model routing:
|
|
6080
8014
|
embed plugin install codex
|
|
6081
8015
|
embed plugin install opencode
|
|
8016
|
+
embed plugin update check
|
|
6082
8017
|
embed service modes
|
|
6083
8018
|
embed model list
|
|
6084
8019
|
embed model default
|
|
6085
8020
|
4. Run a natural-language local tool loop:
|
|
6086
8021
|
embed agent run --prompt "验证开发板状态"
|
|
6087
8022
|
5. Validate or use the local TaishanPi toolchain:
|
|
8023
|
+
embed local toolchain list
|
|
8024
|
+
embed local toolchain installed
|
|
6088
8025
|
embed local toolchain latest
|
|
6089
|
-
embed local toolchain install
|
|
8026
|
+
embed local toolchain install
|
|
6090
8027
|
embed local toolchain validate
|
|
6091
8028
|
embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
|
|
6092
8029
|
embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
|
|
@@ -6094,6 +8031,7 @@ Main workflow:
|
|
|
6094
8031
|
embed board registry list
|
|
6095
8032
|
embed board methods taishanpi-1m-rk3566
|
|
6096
8033
|
embed board knowledge taishanpi-1m-rk3566
|
|
8034
|
+
embed board knowledge search taishanpi-1m-rk3566 --query "UART pinout"
|
|
6097
8035
|
embed build template list
|
|
6098
8036
|
embed build template show <template_id>
|
|
6099
8037
|
7. Provision and populate a build workspace:
|
|
@@ -6129,12 +8067,15 @@ Local hardware:
|
|
|
6129
8067
|
embed tool list
|
|
6130
8068
|
embed tool call device.probe --input-json '{"host":"198.19.77.2","ports":[22,15301]}'
|
|
6131
8069
|
embed tool call wifi.scan --input-json '{"host":"198.19.77.2","user":"root"}'
|
|
8070
|
+
embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve
|
|
6132
8071
|
embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}'
|
|
6133
8072
|
embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}'
|
|
6134
8073
|
embed device list
|
|
8074
|
+
embed local toolchain list
|
|
8075
|
+
embed local toolchain installed
|
|
6135
8076
|
embed local toolchain latest
|
|
6136
8077
|
embed local toolchain current
|
|
6137
|
-
embed local toolchain install
|
|
8078
|
+
embed local toolchain install
|
|
6138
8079
|
embed local toolchain validate
|
|
6139
8080
|
embed local compile taishanpi --source ./main.c --output ./.embed-labs/build/main
|
|
6140
8081
|
embed local build qt-smoke --build-dir ./.embed-labs/build/qt-smoke
|
|
@@ -6148,7 +8089,7 @@ Help:
|
|
|
6148
8089
|
|
|
6149
8090
|
Environment:
|
|
6150
8091
|
EMBED_BRIDGE_URL=http://127.0.0.1:18083
|
|
6151
|
-
EMBED_CLOUD_API_URL=
|
|
8092
|
+
EMBED_CLOUD_API_URL=https://api.embedboard.com
|
|
6152
8093
|
EMBED_API_TOKEN=<token>
|
|
6153
8094
|
CODEX_HOME=~/.codex
|
|
6154
8095
|
|
|
@@ -6203,6 +8144,8 @@ Install local AI client plugins explicitly:
|
|
|
6203
8144
|
embed plugin list
|
|
6204
8145
|
embed plugin install codex
|
|
6205
8146
|
embed plugin install opencode
|
|
8147
|
+
embed plugin update check
|
|
8148
|
+
embed plugin update all
|
|
6206
8149
|
|
|
6207
8150
|
Cloud build path:
|
|
6208
8151
|
|
|
@@ -6217,6 +8160,7 @@ Cloud build path:
|
|
|
6217
8160
|
embed board registry show taishanpi-1m-rk3566
|
|
6218
8161
|
embed board methods taishanpi-1m-rk3566
|
|
6219
8162
|
embed board knowledge taishanpi-1m-rk3566
|
|
8163
|
+
embed board knowledge search taishanpi-1m-rk3566 --query "UART pinout"
|
|
6220
8164
|
embed build template list
|
|
6221
8165
|
embed build template show <template_id>
|
|
6222
8166
|
embed build workspace provision --account <account_id> --project <project_id> --template <template_id>
|
|
@@ -6281,6 +8225,8 @@ Usage:
|
|
|
6281
8225
|
embed auth logout [--json]
|
|
6282
8226
|
embed plugin list [--release-dir <dir>] [--release-url <url>] [--json]
|
|
6283
8227
|
embed plugin install <codex|opencode|all> [--release-dir <dir>] [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--force] [--json]
|
|
8228
|
+
embed plugin update check [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
|
|
8229
|
+
embed plugin update <codex|opencode|all> [--release-url <url>] [--target <dir>] [--codex-target <dir>] [--opencode-target <dir>] [--json]
|
|
6284
8230
|
embed service modes [--json]
|
|
6285
8231
|
embed model list [--json]
|
|
6286
8232
|
embed model default [--json]
|
|
@@ -6288,6 +8234,7 @@ Usage:
|
|
|
6288
8234
|
embed board registry show <template_id> [--json]
|
|
6289
8235
|
embed board methods <template_id> [--json]
|
|
6290
8236
|
embed board knowledge <template_id> [--json]
|
|
8237
|
+
embed board knowledge search <template_id> --query <text> [--source board_pack|build_template|registry] [--limit 5] [--json]
|
|
6291
8238
|
embed board knowledge file <template_id> --source board_pack|build_template|registry --path <relative_path> [--output <local_path>] [--json]
|
|
6292
8239
|
embed agent run --prompt <request> [--account <account_id>] [--workspace <workspace_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--max-tool-calls 6] [--host <ip>] [--ports 22,15301] [--artifact <local_file>|--artifact-id <artifact_id>|--artifact-task <task_id>] [--artifact-output <path>] [--remote-path <path>] [--run] [--approve] [--json]
|
|
6293
8240
|
embed run <natural language request> [--provider stub|openai|bai|cc|claude-code] [--approve] [--json]
|
|
@@ -6336,10 +8283,13 @@ Usage:
|
|
|
6336
8283
|
embed build image generate --workspace <workspace_id> --prompt <request> [--account <account_id>] [--image-profile <profile_id>] [--provider stub|openai|bai|cc|claude-code] [--model <model>] [--execution-mode cloud_worker|dry_run] [--worker-pool <pool>] [--json]
|
|
6337
8284
|
embed build image boot-logo --logo <local_image> [--account <account_id>] [--project <project_id>] [--board taishanpi] [--variant 1M-RK3566] [--kernel-logo <local_image>] [--rotate -90] [--scale 100] [--output <package.json>] [--json]
|
|
6338
8285
|
embed image boot-logo compose --package <boot-logo-package.json> --base-image <boot.img|image.img> --output <image> [--manifest <manifest.json>] [--force] [--json]
|
|
6339
|
-
embed local toolchain
|
|
8286
|
+
embed local toolchain list [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
|
|
8287
|
+
embed local toolchain installed [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--install-root <path>] [--json]
|
|
8288
|
+
embed local toolchain latest [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--channel stable] [--metadata-root <path>] [--json]
|
|
6340
8289
|
embed local toolchain current [--install-root <path>] [--json]
|
|
6341
|
-
embed local toolchain install [--board taishanpi-1m-rk3566] [--channel stable] [--metadata-root <path>] [--source-url <tar.gz-url>|--source-release-root <path>] [--install-root <path>] [--force] [--json]
|
|
6342
|
-
|
|
8290
|
+
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]
|
|
8291
|
+
Defaults to the production download channel at download.embedboard.com.
|
|
8292
|
+
embed local toolchain validate [--board taishanpi-1m-rk3566|pico2w-rp2350-monitor|coloreasypico2-rp2350-monitor] [--release-root <path>] [--mode minimal|runtime|compile|qt|firmware|full|images] [--json]
|
|
6343
8293
|
embed local compile taishanpi --source <main.c|main.cpp> --output <artifact> [--release-root <path>] [--account <account_id>] [--json]
|
|
6344
8294
|
embed local build qt-smoke --build-dir <dir> [--source <qt-smoke-dir>] [--release-root <path>] [--account <account_id>] [--json]
|
|
6345
8295
|
embed debug tools [--json]
|
|
@@ -6350,6 +8300,18 @@ Usage:
|
|
|
6350
8300
|
embed tool call chip.temperature --input-json '{"host":"198.19.77.2","user":"root"}' [--json]
|
|
6351
8301
|
embed tool call qml.runtime.status --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
|
|
6352
8302
|
embed tool call qml.runtime.start --input-json '{"host":"198.19.77.2","user":"root","port":18130}' [--json]
|
|
8303
|
+
embed tool call rp2350.monitor.capabilities [--json]
|
|
8304
|
+
embed tool call rp2350.monitor.status [--json]
|
|
8305
|
+
embed tool call rp2350.monitor.gpio.read --input-json '{"pins":[16,17]}' --approve [--json]
|
|
8306
|
+
embed tool call rp2350.monitor.gpio.write --input-json '{"pin":16,"level":true}' --approve [--json]
|
|
8307
|
+
embed tool call rp2350.monitor.uart.write --input-json '{"baud":115200,"text":"hello","line_ending":"lf"}' --approve [--json]
|
|
8308
|
+
embed tool call rp2350.monitor.i2c.transfer --input-json '{"address":"0x50","write":"00","read_len":4}' --approve [--json]
|
|
8309
|
+
embed tool call rp2350.monitor.spi.transfer --input-json '{"hex":"a55a3cc3"}' --approve [--json]
|
|
8310
|
+
embed tool call rp2350.monitor.logic.capture --input-json '{"pin_base":16,"pin_count":4,"sample_rate":1000000,"samples":4096}' --approve [--json]
|
|
8311
|
+
embed tool call rp2350.monitor.logic.decode --input-json '{"input_path":".embed-labs/rp2350-monitor/captures/logic.jsonl","decoder":"summary"}' [--json]
|
|
8312
|
+
embed tool call rp2350.monitor.wifi.manage --input-json '{"action":"scan"}' --approve [--json]
|
|
8313
|
+
embed tool call rp2350.monitor.probe.debug --input-json '{"action":"status"}' --approve [--json]
|
|
8314
|
+
embed tool call rp2350.monitor.operation --input-json '{"action":"logic.stop","params":{}}' --approve [--json]
|
|
6353
8315
|
embed deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
|
|
6354
8316
|
embed board deploy taishanpi --host <ip> --artifact <local_file> --approve [--remote-path /userdata/embed-labs/apps/app] [--run] [--json]
|
|
6355
8317
|
embed device list [--json]
|
|
@@ -6383,7 +8345,7 @@ Usage:
|
|
|
6383
8345
|
|
|
6384
8346
|
Environment:
|
|
6385
8347
|
EMBED_BRIDGE_URL=http://127.0.0.1:18083
|
|
6386
|
-
EMBED_CLOUD_API_URL=
|
|
8348
|
+
EMBED_CLOUD_API_URL=https://api.embedboard.com
|
|
6387
8349
|
EMBED_API_TOKEN=<token>
|
|
6388
8350
|
CODEX_HOME=~/.codex
|
|
6389
8351
|
`);
|