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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3139 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { createHash } from "node:crypto";
4
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
9
+ const REPO_ROOT = path.resolve(SCRIPT_DIR, "../../../../..");
10
+ const DEFAULT_TEMPLATE_ID = "taishanpi-1m-rk3566";
11
+ const DEFAULT_HOST = "198.19.77.2";
12
+ const DEFAULT_PORTS = "22,15301";
13
+ const DEFAULT_STATUS_PORTS = "22";
14
+ const TOOL_INTEGRITY_RELOGIN_MESSAGE = "工具完整性校验失败,请在当前电脑重新执行 embedlabs auth login --token <key>";
15
+ const MCP_SERVER_VERSION = "0.2.40";
16
+ const MCP_ICON_FILES = [
17
+ { fileName: "embed-labs-icon-dark.png", theme: "dark" },
18
+ { fileName: "embed-labs-icon-light.png", theme: "light" }
19
+ ];
20
+
21
+ function iconDataUri(fileName) {
22
+ const candidates = [
23
+ path.join(SCRIPT_DIR, "..", "assets", fileName),
24
+ path.join(SCRIPT_DIR, "assets", fileName)
25
+ ];
26
+ for (const candidate of candidates) {
27
+ if (!existsSync(candidate)) continue;
28
+ const data = readFileSync(candidate).toString("base64");
29
+ return `data:image/png;base64,${data}`;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function mcpServerInfo() {
35
+ const icons = MCP_ICON_FILES
36
+ .map(({ fileName, theme }) => {
37
+ const src = iconDataUri(fileName);
38
+ return src ? { src, mimeType: "image/png", sizes: ["100x100"], theme } : null;
39
+ })
40
+ .filter(Boolean);
41
+ return {
42
+ name: "embed-labs",
43
+ title: "Embed Labs",
44
+ version: MCP_SERVER_VERSION,
45
+ description: "Embed Labs MCP service for local-first embedded development boards, board knowledge, toolchain metadata, Local Bridge hardware access, and account/device-bound service logging.",
46
+ websiteUrl: "https://embedboard.com/",
47
+ ...(icons.length > 0 ? { icons } : {})
48
+ };
49
+ }
50
+
51
+ const tools = [
52
+ {
53
+ name: "dbt_auth_status",
54
+ description: "Check whether the local Embed Labs CLI is signed in and bound to this computer. If not ready, return the exact login command and dashboard URL the user should use before any board/cloud tool call.",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {}
58
+ }
59
+ },
60
+ {
61
+ name: "dbt_current_board_status",
62
+ description: "Get the latest local Embed Labs Bridge inventory snapshot for supported development boards, without server-side AI planning. Use this MCP tool directly for requests like 当前开发板状态, 开发板状态, board status, USB ECM, and SSH checks; it is not a shell command. Do not report optional control ports or unrelated local serial paths unless the user explicitly asks.",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ reason: { type: "string" },
67
+ local_device_id: { type: "string" },
68
+ host: { type: "string" },
69
+ ports: { type: "string" }
70
+ }
71
+ }
72
+ },
73
+ {
74
+ name: "dbt_initial_image_flash",
75
+ description: "Single high-level entrypoint for initialization-image flashing. Use this directly for requests like 刷写初始化镜像, 将泰山派1M RK3566的初始化镜像刷写到开发板, or RP2350/Pico initial firmware. The tool infers or accepts the board, validates installed SDK resources, calls Local Bridge flash plan/run, waits for completion when approved, and returns a compact user-facing result. Do not inspect plugin scripts, do not list tools, do not use dbt_board_debug, and do not print raw CLI help.",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: {
79
+ board_id: { type: "string", description: "Optional board/template id such as taishanpi-1m-rk3566, pico2w-rp2350-monitor, or coloreasypico2-rp2350-monitor." },
80
+ board: { type: "string" },
81
+ local_device_id: { type: "string" },
82
+ device_id: { type: "string" },
83
+ image_dir: { type: "string" },
84
+ artifact_path: { type: "string" },
85
+ approve: { type: "boolean" },
86
+ approved: { type: "boolean" },
87
+ wait: { type: "boolean", description: "Defaults to true when approved=true." },
88
+ timeout_ms: { type: "number" },
89
+ include_raw: { type: "boolean" }
90
+ }
91
+ }
92
+ },
93
+ {
94
+ name: "dbt_rp2350_monitor_status",
95
+ description: "Read RP2350-Monitor status through Embed Labs Local Bridge on 18083, including board status, logic-analyzer caps, probe caps, pins, channels, and the browser UI URL. Report ui_url/browser_url for the web interface; bridge_url/monitor_bridge_url is the internal upstream and must not be opened as /rp2350-monitor/.",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ transport: { type: "string" },
100
+ local_device_id: { type: "string" },
101
+ monitor_bridge_url: { type: "string" },
102
+ serial_path: { type: "string" },
103
+ host: { type: "string" },
104
+ port: { type: "number" },
105
+ include_caps: { type: "boolean" },
106
+ timeout_ms: { type: "number" },
107
+ monitor_root: { type: "string" }
108
+ }
109
+ }
110
+ },
111
+ {
112
+ name: "dbt_rp2350_capabilities",
113
+ description: "Return the RP2350/Pico2 monitor capability catalog: supported operations, parameters, defaults, firmware commands, and preferred MCP tools. Call this before using unfamiliar RP2350 hardware protocols.",
114
+ inputSchema: { type: "object", properties: {} }
115
+ },
116
+ {
117
+ name: "dbt_rp2350_operation",
118
+ description: "Run a supported RP2350 Monitor /api/operation action through the shared Local Bridge. Prefer specific tools for GPIO/UART/I2C/SPI/Wi-Fi/probe; use this for advanced catalog actions.",
119
+ inputSchema: {
120
+ type: "object",
121
+ required: ["action"],
122
+ properties: {
123
+ local_device_id: { type: "string" },
124
+ monitor_bridge_url: { type: "string" },
125
+ action: { type: "string" },
126
+ params: { type: "object", additionalProperties: true },
127
+ approve: { type: "boolean" },
128
+ approved: { type: "boolean" },
129
+ timeout_ms: { type: "number" }
130
+ }
131
+ }
132
+ },
133
+ {
134
+ name: "dbt_rp2350_gpio_read",
135
+ description: "Read one or more RP2350/Pico GPIO levels through the shared monitor bridge. This temporarily configures pins, so use approved=true after user approval.",
136
+ inputSchema: { type: "object", properties: { pins: { type: "array", items: { type: "number" } }, pull: { type: "string" }, approved: { type: "boolean" }, approve: { type: "boolean" }, local_device_id: { type: "string" }, timeout_ms: { type: "number" } } }
137
+ },
138
+ {
139
+ name: "dbt_rp2350_gpio_write",
140
+ description: "Drive an RP2350/Pico GPIO pin high or low through the shared monitor bridge. Requires approved=true.",
141
+ inputSchema: { type: "object", properties: { pin: { type: "number" }, gpio: { type: "number" }, id: { type: "number" }, level: { type: "boolean" }, approved: { type: "boolean" }, approve: { type: "boolean" }, local_device_id: { type: "string" }, timeout_ms: { type: "number" } } }
142
+ },
143
+ {
144
+ name: "dbt_rp2350_uart_write",
145
+ description: "Send text or HEX data over the current/default RP2350 UART channel, with optional tx/rx/baud override. Requires approved=true.",
146
+ inputSchema: { type: "object", properties: { id: { type: "number" }, instance: { type: "number" }, tx: { type: "number" }, rx: { type: "number" }, baud: { type: "number" }, text: { type: "string" }, hex: { type: "string" }, data_hex: { type: "string" }, line_ending: { type: "string" }, approved: { type: "boolean" }, approve: { type: "boolean" }, local_device_id: { type: "string" }, timeout_ms: { type: "number" } } }
147
+ },
148
+ {
149
+ name: "dbt_rp2350_i2c_transfer",
150
+ description: "Run RP2350 I2C write/read/write-read transaction with address, write payload, and read length. Requires approved=true.",
151
+ inputSchema: { type: "object", properties: { id: { type: "number" }, instance: { type: "number" }, sda: { type: "number" }, scl: { type: "number" }, baud: { type: "number" }, addr: { type: "string" }, address: { type: "string" }, write: { type: "string" }, write_hex: { type: "string" }, hex: { type: "string" }, read_len: { type: "number" }, approved: { type: "boolean" }, approve: { type: "boolean" }, local_device_id: { type: "string" }, timeout_ms: { type: "number" } } }
152
+ },
153
+ {
154
+ name: "dbt_rp2350_logic_capture",
155
+ description: "Capture a compact RP2350-Monitor logic-analyzer JSONL file from selected GPIO pins for AI-assisted data collection and later decode. Requires approved=true because it arms local hardware. Logic analyzer pins are configurable as a contiguous exposed GPIO range; use dbt_rp2350_capabilities/status and the user's wiring before choosing pin_base/pin_count.",
156
+ inputSchema: {
157
+ type: "object",
158
+ required: ["pin_base", "pin_count"],
159
+ properties: {
160
+ transport: { type: "string" },
161
+ local_device_id: { type: "string" },
162
+ monitor_bridge_url: { type: "string" },
163
+ serial_path: { type: "string" },
164
+ host: { type: "string" },
165
+ port: { type: "number" },
166
+ pin_base: { type: "number" },
167
+ pin_count: { type: "number" },
168
+ sample_rate: { type: "number" },
169
+ samples: { type: "number" },
170
+ output_path: { type: "string" },
171
+ pull: { type: "string" },
172
+ release: { type: "boolean" },
173
+ wait_timeout_ms: { type: "number" },
174
+ read_timeout_ms: { type: "number" },
175
+ pre_samples: { type: "number" },
176
+ post_samples: { type: "number" },
177
+ search_samples: { type: "number" },
178
+ burst_count: { type: "number" },
179
+ trigger_pin: { type: "number" },
180
+ trigger_mode: { type: "string" },
181
+ trigger_level: { type: "boolean" },
182
+ trigger_mask: { type: "number" },
183
+ trigger_value: { type: "number" },
184
+ channel_names: { type: "object", additionalProperties: { type: "string" } },
185
+ pin_pulls: { type: "object", additionalProperties: { type: "string" } },
186
+ stimulus: { type: "object", additionalProperties: true, description: "Optional closed-loop stimulus. For SPI, use action/protocol spi.transfer. If SPI pins are omitted, the bridge uses the documented SPI0 example SCK GP2, MOSI GP3, MISO GP0, CS GP1; this is only an example profile, not a global wiring rule." },
187
+ approve: { type: "boolean" },
188
+ approved: { type: "boolean" },
189
+ timeout_ms: { type: "number" },
190
+ monitor_root: { type: "string" }
191
+ }
192
+ }
193
+ },
194
+ {
195
+ name: "dbt_rp2350_logic_decode",
196
+ description: "Decode a local RP2350-Monitor logic JSONL capture without touching hardware. Supported decoders: summary, bursts, edges, spi, and structure/auto/unknown. The result includes summary_for_user, decoded_preview, protocol_data, and analysis so the AI can report actual sampled data; use structure/unknown when the protocol is not known.",
197
+ inputSchema: {
198
+ type: "object",
199
+ required: ["input_path"],
200
+ properties: {
201
+ local_device_id: { type: "string" },
202
+ input_path: { type: "string" },
203
+ decoder: { type: "string", description: "summary, bursts, edges, spi, structure, auto, or unknown." },
204
+ output_path: { type: "string" },
205
+ preview_limit: { type: "number" },
206
+ pins: { type: "array", items: { type: "number" } },
207
+ rx_pin: { type: "number" },
208
+ baud: { type: "number" },
209
+ sck_pin: { type: "number" },
210
+ mosi_pin: { type: "number" },
211
+ miso_pin: { type: "number" },
212
+ cs_pin: { type: "number" },
213
+ sda_pin: { type: "number" },
214
+ scl_pin: { type: "number" },
215
+ monitor_root: { type: "string" }
216
+ }
217
+ }
218
+ },
219
+ {
220
+ name: "dbt_rp2350_spi_transfer",
221
+ description: "Send test bytes through a configurable RP2350 SPI channel via Embed Labs Local Bridge. Pin choices must match the firmware pin map for the selected SPI instance; if pins are omitted, the bridge uses the documented SPI0 example SCK GP2, MOSI GP3, MISO GP0, CS GP1. Use this directly for requests like 通过默认SPI接口发送测试数据; mutating bus output requires approve=true.",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ transport: { type: "string" },
226
+ local_device_id: { type: "string" },
227
+ monitor_bridge_url: { type: "string" },
228
+ serial_path: { type: "string" },
229
+ host: { type: "string" },
230
+ port: { type: "number" },
231
+ id: { type: "number" },
232
+ instance: { type: "number" },
233
+ sck: { type: "number", default: 2, description: "SPI SCK GPIO. Default is the documented SPI0 example GP2; other valid pins depend on the selected SPI instance." },
234
+ mosi: { type: "number", default: 3, description: "SPI MOSI/TX GPIO. Default is the documented SPI0 example GP3; other valid pins depend on the selected SPI instance." },
235
+ miso: { type: "number", default: 0, description: "SPI MISO/RX GPIO. Default is the documented SPI0 example GP0; other valid pins depend on the selected SPI instance." },
236
+ cs: { type: "number", default: 1, description: "Manual CS GPIO. Default is the documented SPI0 example GP1; it may be another free exposed GPIO that does not overlap SCK/MOSI/MISO." },
237
+ baud: { type: "number", default: 1000000 },
238
+ hex: { type: "string" },
239
+ data_hex: { type: "string" },
240
+ write_hex: { type: "string" },
241
+ read_len: { type: "number" },
242
+ use_current_channel: { type: "boolean", description: "Advanced: reuse active SPI channel from monitor snapshot instead of the documented SPI0 example pins." },
243
+ approve: { type: "boolean" },
244
+ approved: { type: "boolean" },
245
+ timeout_ms: { type: "number" },
246
+ monitor_root: { type: "string" }
247
+ }
248
+ }
249
+ },
250
+ {
251
+ name: "dbt_rp2350_wifi_manage",
252
+ description: "Manage RP2350 Monitor Wi-Fi actions: scan, save, save-connect, connect, clear, or AP mode. Requires approved=true because it can change board Wi-Fi state.",
253
+ inputSchema: { type: "object", properties: { action: { type: "string" }, wifi_action: { type: "string" }, slot: { type: "number" }, ssid: { type: "string" }, password: { type: "string" }, save: { type: "boolean" }, approved: { type: "boolean" }, approve: { type: "boolean" }, local_device_id: { type: "string" }, timeout_ms: { type: "number" } } }
254
+ },
255
+ {
256
+ name: "dbt_rp2350_probe_debug",
257
+ description: "Run RP2350 Monitor debug-probe actions: caps, status, config, release, reset, or raw DAP packet. Mutating actions require approved=true.",
258
+ inputSchema: { type: "object", required: ["action"], properties: { action: { type: "string" }, swclk: { type: "number" }, swdio: { type: "number" }, reset: { type: "number" }, swclk_khz: { type: "number" }, reset_action: { type: "string" }, pulse_ms: { type: "number" }, packet_hex: { type: "string" }, approved: { type: "boolean" }, approve: { type: "boolean" }, local_device_id: { type: "string" }, timeout_ms: { type: "number" } } }
259
+ },
260
+ {
261
+ name: "dbt_rp2350_monitor_command",
262
+ description: "Run an allowlisted RP2350-Monitor command through Embed Labs Local Bridge. Mutating commands require approve=true.",
263
+ inputSchema: {
264
+ type: "object",
265
+ required: ["command"],
266
+ properties: {
267
+ transport: { type: "string" },
268
+ local_device_id: { type: "string" },
269
+ monitor_bridge_url: { type: "string" },
270
+ serial_path: { type: "string" },
271
+ host: { type: "string" },
272
+ port: { type: "number" },
273
+ command: { type: "string" },
274
+ args: { type: "array", items: { type: "string" } },
275
+ approve: { type: "boolean" },
276
+ approved: { type: "boolean" },
277
+ timeout_ms: { type: "number" },
278
+ monitor_root: { type: "string" }
279
+ }
280
+ }
281
+ },
282
+ {
283
+ name: "dbt_cloud_status",
284
+ description: "Discover Embed Labs Cloud supported boards, knowledge file index, plugin/service metadata, and local toolchain download metadata. Server-side user workspaces, quota/billing tools, and Docker builds are disabled by default.",
285
+ inputSchema: {
286
+ type: "object",
287
+ properties: {
288
+ template_id: { type: "string" }
289
+ }
290
+ }
291
+ },
292
+ {
293
+ name: "dbt_board_knowledge_search",
294
+ description: "Search bounded Embed Labs server-side board knowledge snippets for pinout, UART/serial, GPIO, build, flash, SDK, and workflow questions. For supported boards, use this instead of web search; if no snippet matches, report the board-pack gap.",
295
+ inputSchema: {
296
+ type: "object",
297
+ required: ["query"],
298
+ properties: {
299
+ template_id: { type: "string" },
300
+ query: { type: "string" },
301
+ source: { type: "string", enum: ["board_pack", "build_template", "registry"] },
302
+ limit: { type: "number" }
303
+ }
304
+ }
305
+ },
306
+ {
307
+ name: "dbt_supported_boards",
308
+ description: "List currently supported Embed Labs development boards across Cloud board registry and local toolchain environments. Use this directly for 当前支持哪些开发板 / supported boards questions.",
309
+ inputSchema: {
310
+ type: "object",
311
+ properties: {}
312
+ }
313
+ },
314
+ {
315
+ name: "dbt_task_status",
316
+ description: "Read an Embed Labs Cloud task status, including output_tail and artifact metadata.",
317
+ inputSchema: {
318
+ type: "object",
319
+ required: ["task_id"],
320
+ properties: {
321
+ task_id: { type: "string" }
322
+ }
323
+ }
324
+ },
325
+ {
326
+ name: "dbt_task_log_download",
327
+ description: "Download the first log artifact from a Cloud task.",
328
+ inputSchema: {
329
+ type: "object",
330
+ required: ["task_id"],
331
+ properties: {
332
+ task_id: { type: "string" },
333
+ output_path: { type: "string" }
334
+ }
335
+ }
336
+ },
337
+ {
338
+ name: "dbt_artifact_download",
339
+ description: "Download an Embed Labs Cloud artifact.",
340
+ inputSchema: {
341
+ type: "object",
342
+ required: ["artifact_id", "output_path"],
343
+ properties: {
344
+ artifact_id: { type: "string" },
345
+ output_path: { type: "string" }
346
+ }
347
+ }
348
+ },
349
+ {
350
+ name: "dbt_board_debug",
351
+ description: "Run managed-model local-board debug/deploy planning through Cloud API plus Local Bridge. Source edits and compilation stay in the user's local environment. Do not use this for initialization image flashing; use dbt_initial_image_flash instead.",
352
+ inputSchema: {
353
+ type: "object",
354
+ required: ["prompt"],
355
+ properties: {
356
+ account_id: { type: "string" },
357
+ workspace_id: { type: "string" },
358
+ prompt: { type: "string" },
359
+ provider: { type: "string" },
360
+ model: { type: "string" },
361
+ host: { type: "string" },
362
+ ports: { type: "string" },
363
+ artifact_path: { type: "string" },
364
+ artifact_id: { type: "string" },
365
+ artifact_task_id: { type: "string" },
366
+ artifact_output: { type: "string" },
367
+ remote_path: { type: "string" },
368
+ run_command: { type: "string" },
369
+ approve: { type: "boolean" },
370
+ run: { type: "boolean" }
371
+ }
372
+ }
373
+ },
374
+ {
375
+ name: "dbt_boot_logo_update",
376
+ description: "Single high-level local boot-logo action. Use this directly for 启动logo修改/刷写启动Logo/logo替换. Pass only the image path and optional approval; the tool validates the local SDK, converts/resizes the image locally, repacks the local Rockchip resource image, patches the local boot.img, and returns a compact result. Do not call dbt_update_logo and dbt_compose_boot_logo manually unless this tool reports a product gap.",
377
+ inputSchema: {
378
+ type: "object",
379
+ required: ["logo_path"],
380
+ properties: {
381
+ logo_path: { type: "string" },
382
+ logo: { type: "string" },
383
+ kernel_logo_path: { type: "string" },
384
+ board_id: { type: "string" },
385
+ board: { type: "string" },
386
+ variant_id: { type: "string" },
387
+ variant: { type: "string" },
388
+ output_dir: { type: "string" },
389
+ package_path: { type: "string" },
390
+ base_image_path: { type: "string" },
391
+ output_image_path: { type: "string" },
392
+ manifest_path: { type: "string" },
393
+ approve: { type: "boolean" },
394
+ approved: { type: "boolean" },
395
+ force: { type: "boolean" },
396
+ auto_repair: { type: "boolean" },
397
+ include_raw: { type: "boolean" }
398
+ }
399
+ }
400
+ },
401
+ {
402
+ name: "dbt_update_logo",
403
+ description: "Ask the server to generate a small free boot-logo package for later local image merge and approved flashing. This does not allocate a paid server workspace.",
404
+ inputSchema: {
405
+ type: "object",
406
+ required: ["logo_path"],
407
+ properties: {
408
+ account_id: { type: "string" },
409
+ account: { type: "string" },
410
+ project_id: { type: "string" },
411
+ project: { type: "string" },
412
+ board_id: { type: "string" },
413
+ board: { type: "string" },
414
+ variant_id: { type: "string" },
415
+ variant: { type: "string" },
416
+ logo_path: { type: "string" },
417
+ kernel_logo_path: { type: "string" },
418
+ output_path: { type: "string" },
419
+ output: { type: "string" },
420
+ rotate: { type: "string" },
421
+ scale: { type: "string" }
422
+ }
423
+ }
424
+ },
425
+ {
426
+ name: "dbt_compose_boot_logo",
427
+ description: "Compose a downloaded server boot-logo package with a local base image using the cross-platform Embed CLI local composer. The result reports whether it is ready for approved flashing.",
428
+ inputSchema: {
429
+ type: "object",
430
+ required: ["package_path", "base_image_path", "output_path"],
431
+ properties: {
432
+ package_path: { type: "string" },
433
+ package: { type: "string" },
434
+ base_image_path: { type: "string" },
435
+ base_image: { type: "string" },
436
+ output_path: { type: "string" },
437
+ output: { type: "string" },
438
+ manifest_path: { type: "string" },
439
+ manifest: { type: "string" },
440
+ force: { type: "boolean" }
441
+ }
442
+ }
443
+ },
444
+ {
445
+ name: "dbt_local_toolchain_list",
446
+ description: "List supported local development-board environments for this host. Summarize board names, installed/available/update status, package groups, and the recommended install command in plain language.",
447
+ inputSchema: {
448
+ type: "object",
449
+ properties: {
450
+ board_id: { type: "string" },
451
+ board: { type: "string" },
452
+ channel: { type: "string" },
453
+ metadata_root: { type: "string" },
454
+ metadataRoot: { type: "string" },
455
+ install_root: { type: "string" },
456
+ installRoot: { type: "string" }
457
+ }
458
+ }
459
+ },
460
+ {
461
+ name: "dbt_local_toolchain_installed",
462
+ description: "Show only development-board environments already installed on this computer, including the release root and installed mode. Use this when the user asks what board packages or SDKs are currently installed.",
463
+ inputSchema: {
464
+ type: "object",
465
+ properties: {
466
+ board_id: { type: "string" },
467
+ board: { type: "string" },
468
+ channel: { type: "string" },
469
+ metadata_root: { type: "string" },
470
+ metadataRoot: { type: "string" },
471
+ install_root: { type: "string" },
472
+ installRoot: { type: "string" }
473
+ }
474
+ }
475
+ },
476
+ {
477
+ name: "dbt_local_toolchain_latest",
478
+ description: "Show the latest available local development-board environment package metadata for this host and board.",
479
+ inputSchema: {
480
+ type: "object",
481
+ properties: {
482
+ board_id: { type: "string" },
483
+ board: { type: "string" },
484
+ channel: { type: "string" },
485
+ metadata_root: { type: "string" },
486
+ metadataRoot: { type: "string" }
487
+ }
488
+ }
489
+ },
490
+ {
491
+ name: "dbt_local_toolchain_current",
492
+ description: "Show the currently selected local development-board environment in the Embed Labs CLI registry.",
493
+ inputSchema: {
494
+ type: "object",
495
+ properties: {
496
+ install_root: { type: "string" },
497
+ installRoot: { type: "string" }
498
+ }
499
+ }
500
+ },
501
+ {
502
+ name: "dbt_local_toolchain_install",
503
+ description: "Download, install, or repair a local development-board environment, such as TaishanPi LLVM/Qt support or Pico2W/RP2350 Monitor runtime support. If validation reports missing components for the user's requested workflow, call this with the matching board_id and mode to overlay the missing packages.",
504
+ inputSchema: {
505
+ type: "object",
506
+ properties: {
507
+ board_id: { type: "string" },
508
+ board: { type: "string" },
509
+ channel: { type: "string" },
510
+ metadata_root: { type: "string" },
511
+ metadataRoot: { type: "string" },
512
+ source_url: { type: "string" },
513
+ sourceUrl: { type: "string" },
514
+ source_release_root: { type: "string" },
515
+ sourceReleaseRoot: { type: "string" },
516
+ install_root: { type: "string" },
517
+ installRoot: { type: "string" },
518
+ mode: { type: "string", enum: ["minimal", "runtime", "compile", "qt", "firmware", "full", "images"] },
519
+ force: { type: "boolean" }
520
+ }
521
+ }
522
+ },
523
+ {
524
+ name: "dbt_local_toolchain_uninstall",
525
+ description: "Uninstall a selected local development-board SDK/tool package from this computer and remove its CLI registry entry. Use this when the user asks to remove, uninstall, or clean up a board environment.",
526
+ inputSchema: {
527
+ type: "object",
528
+ required: ["board_id"],
529
+ properties: {
530
+ board_id: { type: "string" },
531
+ board: { type: "string" },
532
+ install_root: { type: "string" },
533
+ installRoot: { type: "string" }
534
+ }
535
+ }
536
+ },
537
+ {
538
+ name: "dbt_local_toolchain_validate",
539
+ description: "Validate a local development-board environment release, such as TaishanPi LLVM/Qt support or Pico2W/RP2350 Monitor runtime support. The response includes missing_groups, summary_for_user, and repair_command; for TaishanPi Qt development, an ok=false result should be repaired with dbt_local_toolchain_install mode=qt before one-click cross compile or deploy.",
540
+ inputSchema: {
541
+ type: "object",
542
+ properties: {
543
+ release_root: { type: "string" },
544
+ releaseRoot: { type: "string" },
545
+ board_id: { type: "string" },
546
+ board: { type: "string" },
547
+ mode: { type: "string", enum: ["minimal", "runtime", "compile", "qt", "firmware", "full", "images"] }
548
+ }
549
+ }
550
+ },
551
+ {
552
+ name: "dbt_taishanpi_initial_image_flash",
553
+ description: "Install/repair the TaishanPi 1M-RK3566 images SDK, resolve images/current automatically, plan initialization image flashing through Embed Labs Local Bridge, and optionally run it when approved=true. Use this directly for TaishanPi requests like 烧写初始化镜像, 刷写1M RK3566初始化镜像, Loader/Maskrom 初始化镜像刷写. Do not use dbt_board_debug for this flow and do not ask the user to provide image_dir when the SDK is installed.",
554
+ inputSchema: {
555
+ type: "object",
556
+ properties: {
557
+ board_id: { type: "string", description: "Defaults to taishanpi-1m-rk3566 for SDK install and taishanpi for flash." },
558
+ board: { type: "string" },
559
+ local_device_id: { type: "string" },
560
+ device_id: { type: "string" },
561
+ image_dir: { type: "string" },
562
+ install_root: { type: "string" },
563
+ installRoot: { type: "string" },
564
+ channel: { type: "string" },
565
+ metadata_root: { type: "string" },
566
+ metadataRoot: { type: "string" },
567
+ force: { type: "boolean" },
568
+ approve: { type: "boolean" },
569
+ approved: { type: "boolean" }
570
+ }
571
+ }
572
+ },
573
+ {
574
+ name: "dbt_rp2350_monitor_firmware_flash",
575
+ description: "Download/install the optional RP2350 Monitor firmware package, locate rp2350_monitor.uf2, plan UF2 flashing through Embed Labs Local Bridge, and optionally flash it when approved=true. Use this only when the user explicitly asks for RP2350 Monitor, logic-analyzer, hardware-control, or Monitor firmware. For 初始化镜像/initial firmware requests use dbt_rp2350_initial_firmware_flash instead. Initialized boards use the Local Bridge inventory and picotool runtime handoff when available.",
576
+ inputSchema: {
577
+ type: "object",
578
+ properties: {
579
+ board_id: { type: "string", description: "pico2w-rp2350-monitor or coloreasypico2-rp2350-monitor. Defaults to pico2w-rp2350-monitor." },
580
+ board: { type: "string" },
581
+ install_root: { type: "string" },
582
+ installRoot: { type: "string" },
583
+ channel: { type: "string" },
584
+ metadata_root: { type: "string" },
585
+ metadataRoot: { type: "string" },
586
+ artifact_path: { type: "string", description: "Optional UF2 path. If omitted, the tool installs firmware mode and uses toolkit-runtime/rp2350-monitor/firmware/rp2350_monitor.uf2." },
587
+ artifact: { type: "string" },
588
+ force: { type: "boolean" },
589
+ approve: { type: "boolean" },
590
+ approved: { type: "boolean" }
591
+ }
592
+ }
593
+ },
594
+ {
595
+ name: "dbt_rp2350_initial_firmware_flash",
596
+ description: "Download/install the RP2350 board initialization firmware package, locate the board-specific initial.uf2, plan UF2 flashing through Embed Labs Local Bridge, and optionally flash it when approved=true. Use this directly for user requests like 烧写初始化镜像, 刷初始化镜像, initial firmware, or initialization UF2. Do not ask the user to choose Monitor vs initial firmware for those requests. If Local Bridge inventory already shows an initialized RP2350 runtime, the flash plan should use the picotool runtime handoff after approval.",
597
+ inputSchema: {
598
+ type: "object",
599
+ properties: {
600
+ board_id: { type: "string", description: "pico2w-rp2350-monitor or coloreasypico2-rp2350-monitor. Defaults to pico2w-rp2350-monitor." },
601
+ board: { type: "string" },
602
+ install_root: { type: "string" },
603
+ installRoot: { type: "string" },
604
+ channel: { type: "string" },
605
+ metadata_root: { type: "string" },
606
+ metadataRoot: { type: "string" },
607
+ artifact_path: { type: "string", description: "Optional UF2 path. If omitted, the tool installs firmware mode and uses rp2350-initial-firmware/<board>/initial.uf2." },
608
+ artifact: { type: "string" },
609
+ force: { type: "boolean" },
610
+ approve: { type: "boolean" },
611
+ approved: { type: "boolean" }
612
+ }
613
+ }
614
+ },
615
+ {
616
+ name: "dbt_plugin_update_check",
617
+ description: "Check the published Embed Labs Codex/OpenCode plugin release and report whether the local plugins should be updated.",
618
+ inputSchema: {
619
+ type: "object",
620
+ properties: {
621
+ release_url: { type: "string" },
622
+ releaseUrl: { type: "string" },
623
+ target: { type: "string" },
624
+ codex_target: { type: "string" },
625
+ codexTarget: { type: "string" },
626
+ opencode_target: { type: "string" },
627
+ opencodeTarget: { type: "string" }
628
+ }
629
+ }
630
+ },
631
+ {
632
+ name: "dbt_plugin_update",
633
+ description: "Update the local Embed Labs Codex/OpenCode plugin installation from the published release. Restart Codex/OpenCode after this finishes.",
634
+ inputSchema: {
635
+ type: "object",
636
+ properties: {
637
+ target_plugin: { type: "string", enum: ["codex", "opencode", "all"] },
638
+ plugin: { type: "string", enum: ["codex", "opencode", "all"] },
639
+ release_url: { type: "string" },
640
+ releaseUrl: { type: "string" },
641
+ target: { type: "string" },
642
+ codex_target: { type: "string" },
643
+ codexTarget: { type: "string" },
644
+ opencode_target: { type: "string" },
645
+ opencodeTarget: { type: "string" }
646
+ }
647
+ }
648
+ },
649
+ {
650
+ name: "dbt_local_compile",
651
+ description: "Compile a local TaishanPi source file with the local Embed Labs LLVM toolchain. The user must be signed in with an Embed Labs token for account attribution.",
652
+ inputSchema: {
653
+ type: "object",
654
+ required: ["source_path", "output_path"],
655
+ properties: {
656
+ account_id: { type: "string" },
657
+ source_path: { type: "string" },
658
+ output_path: { type: "string" },
659
+ release_root: { type: "string" }
660
+ }
661
+ }
662
+ },
663
+ {
664
+ name: "dbt_local_qt_smoke",
665
+ description: "Build a local TaishanPi Qt/CMake application with the local Embed Labs LLVM/Qt toolchain. Defaults to the built-in smoke app, but for real demos pass source_path and target_name. Use official board-pack examples such as taishanpi/examples/modbus-loop-demo instead of ad hoc projects. The user must be signed in with an Embed Labs token for account attribution.",
666
+ inputSchema: {
667
+ type: "object",
668
+ required: ["build_dir"],
669
+ properties: {
670
+ account_id: { type: "string" },
671
+ source_path: { type: "string" },
672
+ build_dir: { type: "string" },
673
+ target_name: { type: "string" },
674
+ release_root: { type: "string" }
675
+ }
676
+ }
677
+ },
678
+ {
679
+ name: "dbt_taishanpi_qt_app_workflow",
680
+ description: "Single high-level TaishanPi Qt application workflow. Use this directly for Qt/QtQuick app development, Modbus/UART demos, cross compilation, deployment, and runtime checks. The tool validates/repairs the Qt SDK, selects a board-pack template when source_path is omitted, builds with the local LLVM/Qt toolchain, and can deploy/run after approval. Do not search scripts, print CLI help, or create ad hoc serial backends.",
681
+ inputSchema: {
682
+ type: "object",
683
+ properties: {
684
+ request: { type: "string" },
685
+ source_path: { type: "string" },
686
+ project_dir: { type: "string" },
687
+ build_dir: { type: "string" },
688
+ target_name: { type: "string" },
689
+ local_device_id: { type: "string" },
690
+ host: { type: "string" },
691
+ remote_path: { type: "string" },
692
+ deploy: { type: "boolean" },
693
+ run: { type: "boolean" },
694
+ approve: { type: "boolean" },
695
+ approved: { type: "boolean" },
696
+ include_raw: { type: "boolean" }
697
+ }
698
+ }
699
+ },
700
+ {
701
+ name: "dbttool",
702
+ description: "Dispatcher for supported-boards, status, RP2350 hardware tools, debug, high-level boot-logo-update, TaishanPi Qt workflow, local toolchain install/validate, local-compile, task-status, task-log-download, or artifact-download. Server workspace and Docker build actions are disabled in the local-first product model.",
703
+ inputSchema: {
704
+ type: "object",
705
+ required: ["action"],
706
+ properties: {
707
+ action: { type: "string" },
708
+ request: { type: "string" },
709
+ arguments_json: { type: "string" }
710
+ }
711
+ }
712
+ }
713
+ ];
714
+
715
+ function asString(value) {
716
+ return typeof value === "string" ? value.trim() : "";
717
+ }
718
+
719
+ function asCliValue(value) {
720
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
721
+ return asString(value);
722
+ }
723
+
724
+ function boolValue(value) {
725
+ return value === true || ["1", "true", "yes", "on"].includes(asString(value).toLowerCase());
726
+ }
727
+
728
+ function compactObject(value) {
729
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== ""));
730
+ }
731
+
732
+ function objectValue(value) {
733
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
734
+ const text = asString(value);
735
+ if (!text) return undefined;
736
+ try {
737
+ const parsed = JSON.parse(text);
738
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
739
+ } catch {
740
+ // Fall through to key=value parsing for compact CLI-oriented prompts.
741
+ }
742
+ const entries = text
743
+ .split(/[,\s]+/g)
744
+ .map((item) => item.trim())
745
+ .filter(Boolean)
746
+ .map((item) => item.split("=", 2))
747
+ .filter((item) => item.length === 2 && item[0]);
748
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
749
+ }
750
+
751
+ function unwrapRp2350Snapshot(value) {
752
+ const root = objectValue(value) || {};
753
+ return objectValue(root.snapshot) || root;
754
+ }
755
+
756
+ function resolveEmbedCommand() {
757
+ if (process.env.EMBED_CLI_BIN) {
758
+ const cliBin = process.env.EMBED_CLI_BIN;
759
+ if (cliBin.endsWith(".js") || cliBin.endsWith(".mjs") || cliBin.endsWith(".cjs")) {
760
+ return { file: process.execPath, args: [cliBin] };
761
+ }
762
+ return { file: cliBin, args: [] };
763
+ }
764
+ const localDist = path.join(REPO_ROOT, "packages", "cli", "dist", "index.js");
765
+ if (existsSync(localDist)) return { file: process.execPath, args: [localDist] };
766
+ return { file: "embedlabs", args: [] };
767
+ }
768
+
769
+ function ensureJson(args) {
770
+ return args.includes("--json") ? args : args.concat("--json");
771
+ }
772
+
773
+ function pushOptional(args, name, value) {
774
+ const text = asCliValue(value);
775
+ if (text) args.push(`--${name}`, text);
776
+ }
777
+
778
+ async function runEmbed(args) {
779
+ const command = resolveEmbedCommand();
780
+ const fullArgs = command.args.concat(ensureJson(args));
781
+ return await new Promise((resolve, reject) => {
782
+ const child = spawn(command.file, fullArgs, {
783
+ cwd: process.cwd(),
784
+ env: process.env,
785
+ stdio: ["ignore", "pipe", "pipe"]
786
+ });
787
+ let stdout = "";
788
+ let stderr = "";
789
+ child.stdout.setEncoding("utf8");
790
+ child.stderr.setEncoding("utf8");
791
+ child.stdout.on("data", (chunk) => {
792
+ stdout += chunk;
793
+ });
794
+ child.stderr.on("data", (chunk) => {
795
+ stderr += chunk;
796
+ });
797
+ child.on("error", reject);
798
+ child.on("close", (code) => {
799
+ const output = stdout.trim();
800
+ const parsed = parseEmbedJson(output);
801
+ if (code !== 0) {
802
+ reject(new Error(JSON.stringify(summarizeEmbedFailure(code, parsed, stderr), null, 2)));
803
+ return;
804
+ }
805
+ resolve({ ok: true, response: parsed });
806
+ });
807
+ });
808
+ }
809
+
810
+ async function runProcess(command, args, options = {}) {
811
+ return await new Promise((resolve) => {
812
+ const child = spawn(command, args, {
813
+ cwd: asString(options.cwd) || process.cwd(),
814
+ env: options.env || process.env,
815
+ stdio: ["ignore", "pipe", "pipe"]
816
+ });
817
+ let stdout = "";
818
+ let stderr = "";
819
+ child.stdout.setEncoding("utf8");
820
+ child.stderr.setEncoding("utf8");
821
+ child.stdout.on("data", (chunk) => {
822
+ stdout += chunk;
823
+ });
824
+ child.stderr.on("data", (chunk) => {
825
+ stderr += chunk;
826
+ });
827
+ child.on("error", (error) => {
828
+ resolve({ code: 127, stdout, stderr: stderr || error.message, error });
829
+ });
830
+ child.on("close", (code) => {
831
+ resolve({ code: code ?? 1, stdout, stderr });
832
+ });
833
+ });
834
+ }
835
+
836
+ function parseEmbedJson(output) {
837
+ if (!output) {
838
+ return { ok: false, error: { code: "empty_cli_output", message: "Embed Labs CLI returned an empty response." } };
839
+ }
840
+ try {
841
+ return JSON.parse(output);
842
+ } catch {
843
+ return { ok: false, error: { code: "non_json_cli_output", message: "Embed Labs CLI returned non-JSON output." } };
844
+ }
845
+ }
846
+
847
+ function summarizeEmbedFailure(exitCode, parsed, stderr) {
848
+ const body = parsed && typeof parsed === "object" ? parsed : {};
849
+ const error = body.error && typeof body.error === "object" ? body.error : {};
850
+ const message = asString(error.message) || asString(error.code) || summarizePublicText(stderr) || "Embed Labs CLI command failed.";
851
+ const code = asString(error.code) || "embed_cli_failed";
852
+ const summarizedError = {
853
+ code,
854
+ message: message.slice(0, 500)
855
+ };
856
+ const remediation = asString(error.remediation);
857
+ if (remediation && isAuthSetupError(code)) {
858
+ summarizedError.remediation = remediation.slice(0, 1200);
859
+ summarizedError.details = authSetupDetails(error.details);
860
+ }
861
+ return {
862
+ ok: false,
863
+ exit_code: exitCode,
864
+ error: summarizedError,
865
+ stderr_summary: summarizePublicText(stderr)
866
+ };
867
+ }
868
+
869
+ function isAuthSetupError(code) {
870
+ return code === "auth_token_missing"
871
+ || code === "auth_token_rejected"
872
+ || code === "auth_not_ready"
873
+ || code === "tool_integrity_check_failed"
874
+ || code === "device_signature_required";
875
+ }
876
+
877
+ function authSetupDetails(details) {
878
+ if (!details || typeof details !== "object" || Array.isArray(details)) return undefined;
879
+ const allowed = {};
880
+ for (const key of ["dashboard_url", "login_command", "env_var", "auth_status_command", "device_status_command", "auth_file"]) {
881
+ const value = details[key];
882
+ if (typeof value === "string" && value.trim()) {
883
+ allowed[key] = value.slice(0, 500);
884
+ }
885
+ }
886
+ return Object.keys(allowed).length ? allowed : undefined;
887
+ }
888
+
889
+ function summarizePublicText(value) {
890
+ const text = asString(value).trim();
891
+ if (!text) return undefined;
892
+ return text
893
+ .replace(/(sk-[A-Za-z0-9_-]{8,})/g, "[redacted]")
894
+ .replace(/(npm_[A-Za-z0-9_-]{8,})/g, "[redacted]")
895
+ .replace(/(re_[A-Za-z0-9_-]{8,})/g, "[redacted]")
896
+ .slice(0, 500);
897
+ }
898
+
899
+ function responseData(response) {
900
+ const body = response?.response ?? response;
901
+ if (body && typeof body === "object" && "data" in body) return body.data;
902
+ return body;
903
+ }
904
+
905
+ async function pluginAuthStatus() {
906
+ let response;
907
+ try {
908
+ response = await runEmbed(["auth", "status"]);
909
+ } catch (error) {
910
+ return authNotReadyResult({
911
+ authenticated: false,
912
+ source_error: error instanceof Error ? error.message : String(error)
913
+ });
914
+ }
915
+ const status = responseData(response) || {};
916
+ if (status.authenticated === true && status.device_id && status.device_integrity === "ok") {
917
+ return {
918
+ ok: true,
919
+ authenticated: true,
920
+ device_ready: true,
921
+ profile: status.profile,
922
+ source: status.source,
923
+ account_id: status.account_id,
924
+ api_key_id: status.api_key_id,
925
+ device_id: status.device_id,
926
+ device_integrity: status.device_integrity,
927
+ message: "Embed Labs token and device binding are ready."
928
+ };
929
+ }
930
+ return authNotReadyResult(status);
931
+ }
932
+
933
+ function formatCurrencyCents(value, currency = "USD") {
934
+ const amount = Number(value);
935
+ if (!Number.isFinite(amount)) return undefined;
936
+ return `${currency || "USD"} ${(amount / 100).toFixed(2)}`;
937
+ }
938
+
939
+ function authNotReadyResult(status) {
940
+ const authenticated = status?.authenticated === true;
941
+ const integrity = asString(status?.device_integrity) || (status?.device_id ? "unknown" : "unbound");
942
+ const code = !authenticated ? "auth_token_missing" : integrity === "failed" ? "tool_integrity_check_failed" : "auth_not_ready";
943
+ const message = code === "tool_integrity_check_failed"
944
+ ? TOOL_INTEGRITY_RELOGIN_MESSAGE
945
+ : !authenticated
946
+ ? "Embed Labs API Token is not configured for this plugin."
947
+ : "Embed Labs API Token 已配置,但还没有完成本机设备绑定。";
948
+ return {
949
+ ok: false,
950
+ error: {
951
+ code,
952
+ message,
953
+ remediation: [
954
+ "1. Open https://api.embedboard.com/dashboard and sign in or register.",
955
+ "2. Create or copy your Embed Labs API Token.",
956
+ "3. Update the local CLI, then run: embedlabs auth login --token <your_token>",
957
+ "4. Verify device binding with: embedlabs auth device status",
958
+ "5. Restart Codex/OpenCode so the plugin picks up the refreshed CLI and auth file."
959
+ ].join("\n"),
960
+ details: {
961
+ dashboard_url: "https://api.embedboard.com/dashboard",
962
+ login_command: "embedlabs auth login --token <your_token>",
963
+ auth_status_command: "embedlabs auth status",
964
+ device_status_command: "embedlabs auth device status",
965
+ auth_file: ".embed-labs/auth.json",
966
+ current_status: {
967
+ authenticated,
968
+ source: status?.source,
969
+ profile: status?.profile,
970
+ device_id: status?.device_id,
971
+ device_integrity: integrity
972
+ }
973
+ }
974
+ }
975
+ };
976
+ }
977
+
978
+ function pickArtifact(response, preferredKind) {
979
+ const data = responseData(response);
980
+ const artifacts = Array.isArray(data?.artifacts) ? data.artifacts : [];
981
+ const candidates = artifacts.filter((item) => !["manifest", "log", "evidence", "report"].includes(String(item.kind || "")));
982
+ return (
983
+ candidates.find((item) => item.kind === preferredKind) ||
984
+ candidates.find((item) => /\.sh$|\.img$|\.bin$|\.zip$/i.test(String(item.name || ""))) ||
985
+ candidates[0] ||
986
+ artifacts[0] ||
987
+ null
988
+ );
989
+ }
990
+
991
+ async function cloudStatus(args) {
992
+ const template = asString(args.template_id || args.template);
993
+ const [modelDefault, serviceModes] = await Promise.all([
994
+ runEmbed(["model", "default"]),
995
+ runEmbed(["service", "modes"])
996
+ ]);
997
+ if (!template) {
998
+ const boards = await supportedBoards(args);
999
+ return {
1000
+ model_default: modelDefault,
1001
+ service_modes: serviceModes,
1002
+ ...boards,
1003
+ selected_template_id: null,
1004
+ board_methods: undefined,
1005
+ note: "No template_id was supplied, so this response lists supported boards only. Board methods are template-specific; call dbt_cloud_status with template_id for one selected board."
1006
+ };
1007
+ }
1008
+ const [board, methods, knowledge] = await Promise.all([
1009
+ runEmbed(["board", "registry", "show", template]),
1010
+ runEmbed(["board", "methods", template]),
1011
+ runEmbed(["board", "knowledge", template])
1012
+ ]);
1013
+ return { model_default: modelDefault, service_modes: serviceModes, board, methods, knowledge };
1014
+ }
1015
+
1016
+ async function boardKnowledgeSearch(args) {
1017
+ const template = asString(args.template_id || args.template) || DEFAULT_TEMPLATE_ID;
1018
+ const query = asString(args.query || args.q || args.prompt);
1019
+ if (!query) {
1020
+ throw new Error("query is required");
1021
+ }
1022
+ const command = ["board", "knowledge", "search", template, "--query", query];
1023
+ pushOptional(command, "source", args.source);
1024
+ pushOptional(command, "limit", args.limit);
1025
+ return await runEmbed(command);
1026
+ }
1027
+
1028
+ async function supportedBoards() {
1029
+ const [registrySettled, localSettled] = await Promise.allSettled([
1030
+ runEmbed(["board", "registry", "list"]),
1031
+ runEmbed(["local", "toolchain", "list"])
1032
+ ]);
1033
+ const registry = settledRunValue(registrySettled);
1034
+ const local = settledRunValue(localSettled);
1035
+ const profiles = Array.isArray(responseData(registry)?.profiles) ? responseData(registry).profiles : [];
1036
+ const environments = Array.isArray(responseData(local)?.environments) ? responseData(local).environments : [];
1037
+ const localByBoard = new Map(environments.map((item) => [asString(item.board_id), item]));
1038
+ const supported = profiles.map((profile) => {
1039
+ const localEnv = localByBoard.get(asString(profile.template_id));
1040
+ const methods = Array.isArray(profile.methods) ? profile.methods : [];
1041
+ const runtimes = [...new Set(methods.map((method) => asString(method.runtime)).filter(Boolean))];
1042
+ return compactObject({
1043
+ template_id: profile.template_id,
1044
+ board_id: profile.board_id,
1045
+ variant_id: profile.variant_id,
1046
+ display_name: normalizeSupportedBoardDisplayName(profile.display_name || profile.variant_id || profile.template_id),
1047
+ support_level: profile.support_level,
1048
+ cloud_method_count: methods.length,
1049
+ cloud_method_runtimes: runtimes,
1050
+ local_toolchain_status: localEnv?.status,
1051
+ local_install_modes: Array.isArray(localEnv?.install_modes) ? localEnv.install_modes : undefined,
1052
+ local_installed: localEnv?.installed,
1053
+ local_installed_version: localEnv?.installed?.version,
1054
+ server_latest_version: localEnv?.latest?.version,
1055
+ local_install_command: localEnv?.install_command,
1056
+ local_update_command: localEnv?.update_command,
1057
+ local_uninstall_command: localEnv ? `embedlabs local toolchain uninstall --board ${profile.template_id}` : undefined
1058
+ });
1059
+ });
1060
+ for (const env of environments) {
1061
+ const boardId = asString(env.board_id);
1062
+ if (!boardId || supported.some((item) => item.template_id === boardId)) continue;
1063
+ supported.push(compactObject({
1064
+ template_id: boardId,
1065
+ board_id: boardId,
1066
+ display_name: normalizeSupportedBoardDisplayName(env.display_name || boardId),
1067
+ support_level: "local_toolchain",
1068
+ cloud_method_count: 0,
1069
+ cloud_method_runtimes: [],
1070
+ local_toolchain_status: env.status,
1071
+ local_install_modes: Array.isArray(env.install_modes) ? env.install_modes : undefined,
1072
+ local_installed: env.installed,
1073
+ local_installed_version: env.installed?.version,
1074
+ server_latest_version: env.latest?.version,
1075
+ local_install_command: env.install_command,
1076
+ local_update_command: env.update_command,
1077
+ local_uninstall_command: `embedlabs local toolchain uninstall --board ${boardId}`
1078
+ }));
1079
+ }
1080
+ supported.sort((a, b) => asString(a.display_name).localeCompare(asString(b.display_name)));
1081
+ const table = supported.map((item) => ({
1082
+ board: item.display_name,
1083
+ template_id: item.template_id,
1084
+ support_level: item.support_level,
1085
+ install_status: item.local_toolchain_status || "unknown",
1086
+ local_version: item.local_installed_version || "not installed",
1087
+ server_version: item.server_latest_version || "unknown",
1088
+ modes: Array.isArray(item.local_install_modes) ? item.local_install_modes.join(",") : "",
1089
+ install: item.local_install_command,
1090
+ update: item.local_update_command,
1091
+ uninstall: item.local_uninstall_command
1092
+ }));
1093
+ return {
1094
+ supported_boards: supported,
1095
+ table,
1096
+ cloud_registry: {
1097
+ available: !!registry,
1098
+ count: profiles.length,
1099
+ error: settledRunError(registrySettled)
1100
+ },
1101
+ local_toolchains: {
1102
+ available: !!local,
1103
+ count: environments.length,
1104
+ error: settledRunError(localSettled)
1105
+ },
1106
+ guidance: "When the user asks which boards are supported, show the table fields as a Markdown table. Use install/update/uninstall commands through dbt_local_toolchain_install and dbt_local_toolchain_uninstall rather than shell rm.",
1107
+ observed_at: new Date().toISOString()
1108
+ };
1109
+ }
1110
+
1111
+ function settledRunValue(result) {
1112
+ return result.status === "fulfilled" ? result.value : undefined;
1113
+ }
1114
+
1115
+ function settledRunError(result) {
1116
+ if (result.status !== "rejected") return undefined;
1117
+ return compactEmbedError(result.reason);
1118
+ }
1119
+
1120
+ function compactEmbedError(error) {
1121
+ const message = error instanceof Error ? error.message : String(error);
1122
+ try {
1123
+ const parsed = JSON.parse(message);
1124
+ return parsed?.response?.error || parsed?.error || { message: redactSensitive(message) };
1125
+ } catch {
1126
+ return { message: redactSensitive(message) };
1127
+ }
1128
+ }
1129
+
1130
+ function redactSensitive(value) {
1131
+ return asString(value)
1132
+ .replace(/(sk-[A-Za-z0-9_-]{8,})/g, "[redacted]")
1133
+ .replace(/(npm_[A-Za-z0-9_-]{8,})/g, "[redacted]")
1134
+ .replace(/(re_[A-Za-z0-9_-]{8,})/g, "[redacted]")
1135
+ .slice(0, 500);
1136
+ }
1137
+
1138
+ function normalizeSupportedBoardDisplayName(value) {
1139
+ const text = asString(value);
1140
+ if (!text) return "";
1141
+ if (/^TaishanPi 1M RK3566 server build template$/i.test(text)) return "TaishanPi 1M-RK3566";
1142
+ return text;
1143
+ }
1144
+
1145
+ async function boardDebug(args) {
1146
+ const prompt = asString(args.prompt || args.request || args.reason) || "验证开发板状态";
1147
+ const command = ["agent", "run", "--prompt", prompt];
1148
+ pushOptional(command, "account", args.account_id || args.account);
1149
+ pushOptional(command, "workspace", args.workspace_id || args.workspace);
1150
+ pushOptional(command, "provider", args.provider);
1151
+ pushOptional(command, "model", args.model);
1152
+ pushOptional(command, "host", args.host || DEFAULT_HOST);
1153
+ pushOptional(command, "ports", args.ports || DEFAULT_PORTS);
1154
+ pushOptional(command, "artifact", args.artifact_path || args.artifact);
1155
+ pushOptional(command, "artifact-id", args.artifact_id);
1156
+ pushOptional(command, "artifact-task", args.artifact_task_id || args.task_id);
1157
+ pushOptional(command, "artifact-output", args.artifact_output);
1158
+ pushOptional(command, "remote-path", args.remote_path);
1159
+ pushOptional(command, "run-command", args.run_command || args.runCommand);
1160
+ if (boolValue(args.approve)) command.push("--approve");
1161
+ if (boolValue(args.run)) command.push("--run");
1162
+ return await runEmbed(command);
1163
+ }
1164
+
1165
+ async function currentBoardStatus(args, authStatus = undefined) {
1166
+ const startedAt = Date.now();
1167
+ const explicitHost = asString(args.host);
1168
+ const ports = parsePorts(args.ports || DEFAULT_STATUS_PORTS);
1169
+ const requestedLocalDeviceId = localDeviceIdForTool(args, authStatus);
1170
+ const scan = await runEmbed(["device", "list"]);
1171
+ const taishanHost = taishanStatusHostFromScan(scan);
1172
+ const host = explicitHost || taishanHost;
1173
+ const probe = host
1174
+ ? await runEmbed(["tool", "call", "device.probe", "--input-json", JSON.stringify({ host, ports })])
1175
+ : emptyBoardStatusProbe();
1176
+ const summary = summarizeBoardStatus(scan, probe, ports);
1177
+ const localDeviceId = requestedLocalDeviceId || (summary.local_device_ids?.length === 1 ? summary.local_device_ids[0] : "");
1178
+ if (authStatus?.ok) {
1179
+ summary.device_binding = {
1180
+ authenticated: true,
1181
+ account_id: authStatus.account_id,
1182
+ api_key_id: authStatus.api_key_id,
1183
+ device_id: authStatus.device_id,
1184
+ device_integrity: authStatus.device_integrity
1185
+ };
1186
+ }
1187
+ if (localDeviceId) {
1188
+ summary.local_device_id = localDeviceId;
1189
+ }
1190
+ const logResult = await recordMcpToolEvent("dbt_current_board_status", {
1191
+ client: "codex",
1192
+ mode: "local_ai",
1193
+ server_model_used: false,
1194
+ success: summary.query_ok === true,
1195
+ duration_ms: Date.now() - startedAt,
1196
+ local_device_id: localDeviceId,
1197
+ input_summary: host ? `host=${host} ports=${ports.join(",")}` : "inventory_only",
1198
+ output_summary: summary.status_text
1199
+ });
1200
+ summary.usage.mcp_service_log = logResult;
1201
+ return summary;
1202
+ }
1203
+
1204
+ function taishanStatusHostFromScan(scanResult) {
1205
+ const scan = responseData(scanResult) || {};
1206
+ const devices = Array.isArray(scan.development_boards) ? scan.development_boards : (Array.isArray(scan.devices) ? scan.devices : []);
1207
+ const taishan = devices.find((item) => item?.board_id === "taishanpi" || String(item?.device_id || "").includes("taishanpi"));
1208
+ if (!taishan) return "";
1209
+ const locatorHost = Array.isArray(taishan.locators)
1210
+ ? taishan.locators.map((locator) => asString(locator.host)).find(Boolean)
1211
+ : "";
1212
+ return asString(taishan.host || taishan.link_local?.host || locatorHost);
1213
+ }
1214
+
1215
+ function emptyBoardStatusProbe() {
1216
+ return {
1217
+ ok: true,
1218
+ response: {
1219
+ ok: true,
1220
+ data: {
1221
+ result: {
1222
+ tcp: [],
1223
+ observed_at: new Date().toISOString()
1224
+ }
1225
+ }
1226
+ }
1227
+ };
1228
+ }
1229
+
1230
+ async function rp2350MonitorTool(toolName, capabilityId, args) {
1231
+ const startedAt = Date.now();
1232
+ const input = normalizeRp2350Input(args);
1233
+ const requestedLocalDeviceId = asString(input.local_device_id || input.localDeviceId);
1234
+ if (requestedLocalDeviceId) input.local_device_id = requestedLocalDeviceId;
1235
+ delete input.localDeviceId;
1236
+ delete input.approve;
1237
+ delete input.approved;
1238
+ const command = ["tool", "call", capabilityId, "--input-json", JSON.stringify(input)];
1239
+ if (boolValue(args.approve) || boolValue(args.approved)) command.push("--approve");
1240
+ const response = await runEmbed(command);
1241
+ const data = responseData(response);
1242
+ const localDeviceId = requestedLocalDeviceId || inferLocalDeviceIdFromToolResponse(data);
1243
+ const summary = data?.result?.summary_for_user || data?.summary_for_user || `${capabilityId} completed.`;
1244
+ const logResult = await recordMcpToolEvent(toolName, {
1245
+ client: "codex",
1246
+ mode: "local_ai",
1247
+ server_model_used: false,
1248
+ success: response?.response?.ok === true,
1249
+ duration_ms: Date.now() - startedAt,
1250
+ local_device_id: localDeviceId,
1251
+ input_summary: `${capabilityId} ${JSON.stringify(input).slice(0, 180)}`,
1252
+ output_summary: asString(summary).slice(0, 240)
1253
+ });
1254
+ return { ...response, usage: { mode: "local_ai_mcp_tool", server_model_used: false, input_tokens: 0, output_tokens: 0, local_device_id: localDeviceId || undefined, mcp_service_log: logResult } };
1255
+ }
1256
+
1257
+ function normalizeRp2350Input(args) {
1258
+ const input = compactObject({ ...args });
1259
+ for (const key of [
1260
+ "port", "baud", "timeout_ms", "pin_base", "pin_count", "sample_rate", "samples",
1261
+ "wait_timeout_ms", "read_timeout_ms", "pre_samples", "post_samples", "search_samples",
1262
+ "burst_count", "trigger_pin", "trigger_mask", "trigger_value", "capture_id",
1263
+ "start_sample", "end_sample", "preview_limit", "rx_pin", "data_bits", "sck_pin", "mosi_pin",
1264
+ "miso_pin", "cs_pin", "mode", "word_bits", "sda_pin", "scl_pin",
1265
+ "id", "instance", "sck", "mosi", "miso", "cs", "read_len", "tx", "rx",
1266
+ "sda", "scl", "pin", "gpio", "slot", "swclk", "swdio", "reset",
1267
+ "swclk_khz", "pulse_ms"
1268
+ ]) {
1269
+ const value = input[key];
1270
+ if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) {
1271
+ input[key] = Number(value);
1272
+ }
1273
+ }
1274
+ for (const key of ["include_caps", "release", "trigger_level", "invert", "cs_active_high", "level", "save", "use_current_channel"]) {
1275
+ if (typeof input[key] === "string") {
1276
+ input[key] = boolValue(input[key]);
1277
+ }
1278
+ }
1279
+ if (typeof input.pins === "string") {
1280
+ input.pins = input.pins.split(/[,\s]+/g).map((item) => Number(item)).filter((item) => Number.isFinite(item));
1281
+ }
1282
+ for (const key of ["channel_names", "pin_pulls", "stimulus", "params"]) {
1283
+ const parsed = objectValue(input[key]);
1284
+ if (parsed) input[key] = parsed;
1285
+ else delete input[key];
1286
+ }
1287
+ return input;
1288
+ }
1289
+
1290
+ async function rp2350MonitorStatus(args) {
1291
+ return await rp2350MonitorTool("dbt_rp2350_monitor_status", "rp2350.monitor.status", args);
1292
+ }
1293
+
1294
+ async function rp2350Capabilities(args) {
1295
+ return await rp2350MonitorTool("dbt_rp2350_capabilities", "rp2350.monitor.capabilities", args);
1296
+ }
1297
+
1298
+ async function rp2350Operation(args) {
1299
+ return await rp2350MonitorTool("dbt_rp2350_operation", "rp2350.monitor.operation", args);
1300
+ }
1301
+
1302
+ async function rp2350GpioRead(args) {
1303
+ return await rp2350MonitorTool("dbt_rp2350_gpio_read", "rp2350.monitor.gpio.read", args);
1304
+ }
1305
+
1306
+ async function rp2350GpioWrite(args) {
1307
+ return await rp2350MonitorTool("dbt_rp2350_gpio_write", "rp2350.monitor.gpio.write", args);
1308
+ }
1309
+
1310
+ async function rp2350UartWrite(args) {
1311
+ return await rp2350MonitorTool("dbt_rp2350_uart_write", "rp2350.monitor.uart.write", args);
1312
+ }
1313
+
1314
+ async function rp2350I2cTransfer(args) {
1315
+ return await rp2350MonitorTool("dbt_rp2350_i2c_transfer", "rp2350.monitor.i2c.transfer", args);
1316
+ }
1317
+
1318
+ async function rp2350LogicCapture(args) {
1319
+ return await rp2350MonitorTool("dbt_rp2350_logic_capture", "rp2350.monitor.logic.capture", args);
1320
+ }
1321
+
1322
+ async function rp2350LogicDecode(args) {
1323
+ return await rp2350MonitorTool("dbt_rp2350_logic_decode", "rp2350.monitor.logic.decode", args);
1324
+ }
1325
+
1326
+ async function rp2350SpiTransfer(args) {
1327
+ return await rp2350MonitorTool("dbt_rp2350_spi_transfer", "rp2350.monitor.spi.transfer", args);
1328
+ }
1329
+
1330
+ async function rp2350WifiManage(args) {
1331
+ return await rp2350MonitorTool("dbt_rp2350_wifi_manage", "rp2350.monitor.wifi.manage", args);
1332
+ }
1333
+
1334
+ async function rp2350ProbeDebug(args) {
1335
+ return await rp2350MonitorTool("dbt_rp2350_probe_debug", "rp2350.monitor.probe.debug", args);
1336
+ }
1337
+
1338
+ async function rp2350MonitorCommand(args) {
1339
+ return await rp2350MonitorTool("dbt_rp2350_monitor_command", "rp2350.monitor.command", args);
1340
+ }
1341
+
1342
+ function parsePorts(value) {
1343
+ const text = asString(value) || DEFAULT_PORTS;
1344
+ const ports = text
1345
+ .split(/[,\s]+/g)
1346
+ .map((item) => Number.parseInt(item, 10))
1347
+ .filter((item) => Number.isInteger(item) && item > 0 && item <= 65535);
1348
+ return ports.length > 0 ? ports : DEFAULT_PORTS.split(",").map((item) => Number.parseInt(item, 10));
1349
+ }
1350
+
1351
+ async function recordMcpToolEvent(toolName, details) {
1352
+ try {
1353
+ const requestId = `mcp_${toolName}_${Date.now()}_${Math.random().toString(16).slice(2)}`;
1354
+ const command = [
1355
+ "mcp", "log",
1356
+ "--tool", toolName,
1357
+ "--client", details.client || "codex",
1358
+ "--mode", details.mode || "local_ai",
1359
+ "--server-model-used", details.server_model_used ? "true" : "false",
1360
+ "--success", details.success ? "true" : "false",
1361
+ "--request-id", requestId
1362
+ ];
1363
+ if (Number.isFinite(details.duration_ms)) command.push("--duration-ms", String(Math.max(0, Math.round(details.duration_ms))));
1364
+ if (asString(details.local_device_id)) command.push("--local-device-id", asString(details.local_device_id));
1365
+ if (asString(details.input_summary)) command.push("--input-summary", asString(details.input_summary));
1366
+ if (asString(details.output_summary)) command.push("--output-summary", asString(details.output_summary));
1367
+ const response = await runEmbed(command);
1368
+ return { recorded: response?.response?.ok === true, event_id: response?.response?.data?.event_id };
1369
+ } catch (error) {
1370
+ return compactMcpLogError(error);
1371
+ }
1372
+ }
1373
+
1374
+ function compactMcpLogError(error) {
1375
+ const message = error instanceof Error ? error.message : String(error);
1376
+ try {
1377
+ const parsed = JSON.parse(message);
1378
+ const cloudError = parsed?.stdout?.error || parsed?.response?.error || parsed?.error;
1379
+ if (cloudError?.code) {
1380
+ return {
1381
+ recorded: false,
1382
+ error_code: cloudError.code,
1383
+ message: cloudError.message || "MCP service log was not recorded."
1384
+ };
1385
+ }
1386
+ } catch {
1387
+ // Fall through to a concise text message.
1388
+ }
1389
+ return { recorded: false, message: message.slice(0, 240) };
1390
+ }
1391
+
1392
+ function localDeviceIdForTool(args) {
1393
+ return asString(args.local_device_id || args.localDeviceId);
1394
+ }
1395
+
1396
+ function inferLocalDeviceIdFromToolResponse(data) {
1397
+ const result = data?.result || data || {};
1398
+ const existing = asString(result.local_device_id || result.device_id);
1399
+ if (existing) return existing;
1400
+ const bridgeUrl = asString(result.bridge_url);
1401
+ if (bridgeUrl) return stableLocalDeviceId("rp2350-monitor", bridgeUrl);
1402
+ const host = asString(result.host);
1403
+ if (host) return stableLocalDeviceId("taishanpi", host);
1404
+ return "";
1405
+ }
1406
+
1407
+ function stableLocalDeviceId(kind, locator) {
1408
+ const hash = createHash("sha256").update(`${kind}:${locator}`).digest("hex").slice(0, 12);
1409
+ return `${kind}_${hash}`;
1410
+ }
1411
+
1412
+ function summarizeBoardStatus(scanResult, probeResult, requestedPorts = []) {
1413
+ const scan = responseData(scanResult) || {};
1414
+ const probeEnvelope = responseData(probeResult) || {};
1415
+ const probe = probeEnvelope.result || probeEnvelope || {};
1416
+ const devices = Array.isArray(scan.development_boards) ? scan.development_boards : (Array.isArray(scan.devices) ? scan.devices : []);
1417
+ const bridgeBoardStatus = objectValue(scan.board_status) || {};
1418
+ const visiblePorts = new Set((Array.isArray(requestedPorts) ? requestedPorts : []).map((port) => Number(port)).filter((port) => Number.isFinite(port)));
1419
+ const boardLocalDeviceIds = devices
1420
+ .map((item) => asString(item.local_device_id || item.device_id))
1421
+ .filter(Boolean);
1422
+ const boards = devices
1423
+ .map((item) => summarizeBoardDevice(item, visiblePorts));
1424
+ const tcp = (Array.isArray(probe.tcp) ? probe.tcp : []).map((item) => ({
1425
+ host: item.host,
1426
+ port: item.port,
1427
+ reachable: item.reachable,
1428
+ duration_ms: item.duration_ms
1429
+ }));
1430
+ const result = {
1431
+ board_connected: boards.some((item) => item.connected === true || item.status === "connected"),
1432
+ boards,
1433
+ local_device_ids: boardLocalDeviceIds,
1434
+ tcp_ports: tcp,
1435
+ status_text: asString(bridgeBoardStatus.status_text) || (boards.length > 0
1436
+ ? boards.map((board) => `${board.display_name || board.board_id || "Board"} is ${board.status || "detected"}${board.host ? ` at ${board.host}` : ""}.`).join(" ")
1437
+ : "No supported board was detected in the local scan."),
1438
+ bridge_board_status: bridgeBoardStatus,
1439
+ usage: {
1440
+ mode: "local_ai_mcp_tool",
1441
+ server_model_used: false,
1442
+ input_tokens: 0,
1443
+ output_tokens: 0
1444
+ },
1445
+ observed_at: probe.observed_at || scan.observed_at,
1446
+ raw_result_omitted: true,
1447
+ model_answer_omitted: true
1448
+ };
1449
+ Object.defineProperty(result, "query_ok", {
1450
+ value: scanResult?.ok === true && scanResult?.response?.ok === true && probeResult?.ok === true && probeResult?.response?.ok === true,
1451
+ enumerable: false
1452
+ });
1453
+ return result;
1454
+ }
1455
+
1456
+ function summarizeBoardDevice(item, visiblePorts) {
1457
+ const isRp2350 = isRp2350Device(item);
1458
+ const monitor = isRp2350 ? rp2350MonitorSummary(item) : undefined;
1459
+ const displayName = isRp2350
1460
+ ? `${rp2350BoardDisplayName(item) || item.display_name || item.name || "RP2350"}${monitor?.running ? " (Monitor)" : ""}`
1461
+ : item.display_name || item.name || item.variant_id;
1462
+ const host = isRp2350 ? asString(item.host || item.link_local?.host) : item.host || item.link_local?.host || item.bridge_url;
1463
+ return {
1464
+ device_id: item.device_id,
1465
+ local_device_id: item.local_device_id,
1466
+ board_id: item.board_id,
1467
+ display_name: displayName,
1468
+ status: item.status || (item.connected === true ? "connected" : item.reachable === true ? "reachable" : "detected"),
1469
+ connected: item.connected === true || item.status === "connected",
1470
+ host: host || undefined,
1471
+ ui_url: isRp2350 ? asString(item.ui_url || item.browser_url) || undefined : undefined,
1472
+ browser_url: isRp2350 ? asString(item.browser_url || item.ui_url) || undefined : undefined,
1473
+ public_bridge_url: isRp2350 ? asString(item.public_bridge_url) || undefined : undefined,
1474
+ reachable: item.reachable,
1475
+ link_local: item.link_local,
1476
+ locators: isRp2350 ? filteredRp2350Locators(item.locators) : item.locators,
1477
+ monitor,
1478
+ tcp: Array.isArray(item.tcp) ? item.tcp
1479
+ .filter((entry) => visiblePorts.size === 0 || visiblePorts.has(Number(entry.port)))
1480
+ .map((entry) => ({
1481
+ host: entry.host,
1482
+ port: entry.port,
1483
+ reachable: entry.reachable,
1484
+ error: entry.error
1485
+ })) : undefined,
1486
+ identity: item.identity ? {
1487
+ attempted: item.identity.attempted,
1488
+ exit_code: item.identity.exit_code,
1489
+ stdout: item.identity.stdout,
1490
+ timed_out: item.identity.timed_out,
1491
+ board: isRp2350 ? rp2350BoardDisplayName(item) : undefined
1492
+ } : undefined,
1493
+ summary_for_user: item.summary_for_user
1494
+ };
1495
+ }
1496
+
1497
+ function isRp2350Device(item) {
1498
+ const boardId = asString(item?.board_id).toLowerCase();
1499
+ const kind = asString(item?.kind).toLowerCase();
1500
+ const id = asString(item?.local_device_id || item?.device_id).toLowerCase();
1501
+ return boardId === "rp2350" || boardId.includes("rp2350") || kind.includes("rp2350") || id.includes("rp2350");
1502
+ }
1503
+
1504
+ function rp2350BoardDisplayName(item) {
1505
+ const identity = objectValue(item?.identity) || {};
1506
+ const monitorDevice = objectValue(identity.monitor_device) || {};
1507
+ const details = objectValue(item?.details) || {};
1508
+ const snapshot = unwrapRp2350Snapshot(details.snapshot);
1509
+ const snapshotDevice = objectValue(snapshot.device) || {};
1510
+ for (const value of [
1511
+ item?.board_display_name,
1512
+ item?.variant_id,
1513
+ item?.board_model,
1514
+ monitorDevice.board,
1515
+ snapshotDevice.board,
1516
+ item?.board,
1517
+ item?.display_name,
1518
+ item?.name
1519
+ ]) {
1520
+ const normalized = normalizeRp2350BoardName(value);
1521
+ if (normalized) return normalized;
1522
+ }
1523
+ return "";
1524
+ }
1525
+
1526
+ function normalizeRp2350BoardName(value) {
1527
+ const text = asString(value);
1528
+ if (!text) return "";
1529
+ const compact = text.toLowerCase().replace(/[\s_/-]+/g, "");
1530
+ if (compact.includes("coloreasypico2") || compact.includes("colorpico2")) return "ColorEasyPICO2";
1531
+ if (compact.includes("pico2w") || compact.includes("pico2wireless")) return "Pico 2 W";
1532
+ if (compact === "rp2350" || compact === "rp2350monitor") return "";
1533
+ return text;
1534
+ }
1535
+
1536
+ function rp2350MonitorSummary(item) {
1537
+ const identity = objectValue(item?.identity) || {};
1538
+ const monitorDevice = objectValue(identity.monitor_device) || {};
1539
+ const details = objectValue(item?.details) || {};
1540
+ const snapshot = unwrapRp2350Snapshot(details.snapshot);
1541
+ const snapshotDevice = objectValue(snapshot.device) || {};
1542
+ const running = item?.monitor_running === true || item?.monitor_status === "running";
1543
+ return compactObject({
1544
+ running,
1545
+ status: item?.monitor_status || (running ? "running" : "not_running"),
1546
+ feature_state: item?.feature_state,
1547
+ ui_url: asString(item?.ui_url || item?.browser_url),
1548
+ browser_url: asString(item?.browser_url || item?.ui_url),
1549
+ public_bridge_url: asString(item?.public_bridge_url),
1550
+ upstream_url: asString(item?.upstream_url || item?.monitor_bridge_url || item?.bridge_url),
1551
+ firmware: monitorDevice.firmware || snapshotDevice.firmware,
1552
+ transport: monitorDevice.transport || snapshotDevice.transport
1553
+ });
1554
+ }
1555
+
1556
+ function filteredRp2350Locators(locators) {
1557
+ if (!Array.isArray(locators)) return undefined;
1558
+ const filtered = locators.filter((locator) => asString(locator?.transport) !== "rp2350-monitor");
1559
+ return filtered.length > 0 ? filtered : undefined;
1560
+ }
1561
+
1562
+ async function updateLogo(args) {
1563
+ const logoPath = asString(args.logo_path || args.logo);
1564
+ if (!logoPath) throw new Error("logo_path is required");
1565
+ const command = ["build", "image", "boot-logo", "--logo", logoPath];
1566
+ pushOptional(command, "account", args.account_id || args.account);
1567
+ pushOptional(command, "project", args.project_id || args.project);
1568
+ pushOptional(command, "board", args.board_id || args.board || "taishanpi");
1569
+ pushOptional(command, "variant", args.variant_id || args.variant);
1570
+ pushOptional(command, "kernel-logo", args.kernel_logo_path || args.kernel_logo);
1571
+ pushOptional(command, "output", args.output_path || args.output);
1572
+ pushOptional(command, "rotate", args.rotate);
1573
+ pushOptional(command, "scale", args.scale);
1574
+ return await runEmbed(command);
1575
+ }
1576
+
1577
+ async function composeBootLogo(args) {
1578
+ const packagePath = asString(args.package_path || args.package);
1579
+ const baseImagePath = asString(args.base_image_path || args.base_image);
1580
+ const outputPath = asString(args.output_path || args.output);
1581
+ if (!packagePath) throw new Error("package_path is required");
1582
+ if (!baseImagePath) throw new Error("base_image_path is required");
1583
+ if (!outputPath) throw new Error("output_path is required");
1584
+ const command = [
1585
+ "image",
1586
+ "boot-logo",
1587
+ "compose",
1588
+ "--package",
1589
+ packagePath,
1590
+ "--base-image",
1591
+ baseImagePath,
1592
+ "--output",
1593
+ outputPath
1594
+ ];
1595
+ pushOptional(command, "manifest", args.manifest_path || args.manifest);
1596
+ if (boolValue(args.force)) command.push("--force");
1597
+ return await runEmbed(command);
1598
+ }
1599
+
1600
+ function defaultActionDir(kind, explicitDir) {
1601
+ const outputDir = asString(explicitDir) || path.join(process.cwd(), ".embed-labs", kind, `${kind}_${Date.now()}`);
1602
+ mkdirSync(outputDir, { recursive: true });
1603
+ return outputDir;
1604
+ }
1605
+
1606
+ function firstExistingPath(candidates) {
1607
+ return candidates.map(asString).find((candidate) => candidate && existsSync(candidate)) || "";
1608
+ }
1609
+
1610
+ async function ensureLocalToolchain(args, boardId, mode) {
1611
+ const sdkCheck = await localToolchainValidate({
1612
+ board_id: boardId,
1613
+ mode,
1614
+ release_root: args.release_root || args.releaseRoot
1615
+ });
1616
+ const checkData = responseData(sdkCheck) || {};
1617
+ if (checkData.ok) {
1618
+ return { sdkCheck, sdkInstall: null, releaseRoot: asString(checkData.release_root) };
1619
+ }
1620
+ const sdkInstall = await localToolchainInstall({
1621
+ board_id: boardId,
1622
+ channel: args.channel,
1623
+ metadata_root: args.metadata_root || args.metadataRoot,
1624
+ install_root: args.install_root || args.installRoot,
1625
+ mode,
1626
+ force: args.force
1627
+ });
1628
+ const installData = responseData(sdkInstall) || {};
1629
+ return { sdkCheck, sdkInstall, releaseRoot: asString(installData.release_root) };
1630
+ }
1631
+
1632
+ function bootLogoBaseImageCandidates(args, releaseRoot) {
1633
+ return [
1634
+ args.base_image_path || args.base_image,
1635
+ releaseRoot ? path.join(releaseRoot, "boot-workspace", "out", "resource.img") : "",
1636
+ releaseRoot ? path.join(releaseRoot, "boot-workspace", "out", "boot.img") : "",
1637
+ releaseRoot ? path.join(releaseRoot, "images", "current", "boot.img") : ""
1638
+ ].map(asString).filter(Boolean);
1639
+ }
1640
+
1641
+ function fileSha256Sync(filePath) {
1642
+ return createHash("sha256").update(readFileSync(filePath)).digest("hex");
1643
+ }
1644
+
1645
+ function expectedBaseShaFromPackage(packagePath) {
1646
+ try {
1647
+ const payload = JSON.parse(readFileSync(packagePath, "utf8"));
1648
+ return asString(payload?.local_merge?.base_image?.sha256).toLowerCase();
1649
+ } catch {
1650
+ return "";
1651
+ }
1652
+ }
1653
+
1654
+ function resolveBootLogoBaseImage(args, releaseRoot, packagePath) {
1655
+ const candidates = bootLogoBaseImageCandidates(args, releaseRoot).filter((candidate) => existsSync(candidate));
1656
+ const expectedSha = expectedBaseShaFromPackage(packagePath);
1657
+ if (expectedSha) {
1658
+ const matched = candidates.find((candidate) => fileSha256Sync(candidate) === expectedSha);
1659
+ if (matched) return matched;
1660
+ return "";
1661
+ }
1662
+ return candidates[0] || "";
1663
+ }
1664
+
1665
+ function compactBootLogoCompose(composeResult) {
1666
+ const data = responseData(composeResult) || {};
1667
+ return compactObject({
1668
+ ready_for_flash: data.ready_for_flash,
1669
+ replacement_applied: data.replacement_applied,
1670
+ operation_kind: data.operation_kind,
1671
+ output_path: data.output_path || data.output,
1672
+ manifest_path: data.manifest_path || data.manifest,
1673
+ local_merge_strategy: data.local_merge_strategy,
1674
+ requires_board_specific_patcher: data.requires_board_specific_patcher,
1675
+ summary_for_user: data.summary_for_user,
1676
+ warnings: compactWarnings(data.warnings)
1677
+ });
1678
+ }
1679
+
1680
+ function executableExists(filePath) {
1681
+ try {
1682
+ return statSync(filePath).isFile();
1683
+ } catch {
1684
+ return false;
1685
+ }
1686
+ }
1687
+
1688
+ function resolveExecutableFromPath(names) {
1689
+ const pathEntries = asString(process.env.PATH).split(path.delimiter).filter(Boolean);
1690
+ const extensions = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
1691
+ for (const name of names) {
1692
+ if (path.isAbsolute(name) && executableExists(name)) return name;
1693
+ for (const dir of pathEntries) {
1694
+ for (const ext of extensions) {
1695
+ const candidate = path.join(dir, `${name}${ext}`);
1696
+ if (executableExists(candidate)) return candidate;
1697
+ }
1698
+ }
1699
+ }
1700
+ return "";
1701
+ }
1702
+
1703
+ function resourceToolPath(releaseRoot) {
1704
+ const platformDirs = process.platform === "darwin"
1705
+ ? ["mac", "darwin"]
1706
+ : process.platform === "win32"
1707
+ ? ["windows", "win"]
1708
+ : ["linux"];
1709
+ const names = process.platform === "win32" ? ["resource_tool.exe", "resource_tool"] : ["resource_tool"];
1710
+ const candidates = [];
1711
+ for (const dir of platformDirs) {
1712
+ for (const name of names) {
1713
+ candidates.push(path.join(releaseRoot, "tools", dir, name));
1714
+ }
1715
+ }
1716
+ for (const name of names) {
1717
+ candidates.push(path.join(releaseRoot, "tools", name));
1718
+ }
1719
+ return firstExistingPath(candidates);
1720
+ }
1721
+
1722
+ function readBmpDimensions(filePath) {
1723
+ const bytes = readFileSync(filePath);
1724
+ if (bytes.length < 54 || bytes.toString("ascii", 0, 2) !== "BM") {
1725
+ throw new Error(`Not a Windows BMP file: ${filePath}`);
1726
+ }
1727
+ return {
1728
+ width: Math.abs(bytes.readInt32LE(18)),
1729
+ height: Math.abs(bytes.readInt32LE(22)),
1730
+ bits_per_pixel: bytes.readUInt16LE(28)
1731
+ };
1732
+ }
1733
+
1734
+ function normalizeBmpTopDown(filePath) {
1735
+ const bytes = Buffer.from(readFileSync(filePath));
1736
+ if (bytes.length < 54 || bytes.toString("ascii", 0, 2) !== "BM") return;
1737
+ const width = bytes.readInt32LE(18);
1738
+ const height = bytes.readInt32LE(22);
1739
+ const bitsPerPixel = bytes.readUInt16LE(28);
1740
+ if (height <= 0 || width <= 0 || bitsPerPixel <= 0) return;
1741
+ const pixelOffset = bytes.readUInt32LE(10);
1742
+ const rowSize = Math.floor((bitsPerPixel * width + 31) / 32) * 4;
1743
+ const pixelBytes = rowSize * height;
1744
+ if (pixelOffset + pixelBytes > bytes.length) return;
1745
+ const original = Buffer.from(bytes.subarray(pixelOffset, pixelOffset + pixelBytes));
1746
+ for (let row = 0; row < height; row += 1) {
1747
+ const sourceStart = (height - row - 1) * rowSize;
1748
+ original.copy(bytes, pixelOffset + row * rowSize, sourceStart, sourceStart + rowSize);
1749
+ }
1750
+ bytes.writeInt32LE(-height, 22);
1751
+ writeFileSync(filePath, bytes);
1752
+ }
1753
+
1754
+ async function convertImageToBootBmp(inputPath, outputPath, width, height) {
1755
+ const magick = resolveExecutableFromPath(["magick"]);
1756
+ if (magick) {
1757
+ const result = await runProcess(magick, [
1758
+ inputPath,
1759
+ "-auto-orient",
1760
+ "-resize", `${width}x${height}^`,
1761
+ "-gravity", "center",
1762
+ "-extent", `${width}x${height}`,
1763
+ "-type", "TrueColor",
1764
+ "-depth", "8",
1765
+ `BMP3:${outputPath}`
1766
+ ]);
1767
+ if (result.code === 0) {
1768
+ normalizeBmpTopDown(outputPath);
1769
+ return { converter: "magick", output_path: outputPath };
1770
+ }
1771
+ }
1772
+ const convert = resolveExecutableFromPath(["convert"]);
1773
+ if (convert) {
1774
+ const result = await runProcess(convert, [
1775
+ inputPath,
1776
+ "-auto-orient",
1777
+ "-resize", `${width}x${height}^`,
1778
+ "-gravity", "center",
1779
+ "-extent", `${width}x${height}`,
1780
+ "-type", "TrueColor",
1781
+ "-depth", "8",
1782
+ `BMP3:${outputPath}`
1783
+ ]);
1784
+ if (result.code === 0) {
1785
+ normalizeBmpTopDown(outputPath);
1786
+ return { converter: "convert", output_path: outputPath };
1787
+ }
1788
+ }
1789
+ const sips = resolveExecutableFromPath(["sips"]);
1790
+ if (sips) {
1791
+ const result = await runProcess(sips, [
1792
+ "-s", "format", "bmp",
1793
+ "-z", String(height), String(width),
1794
+ inputPath,
1795
+ "--out", outputPath
1796
+ ]);
1797
+ if (result.code === 0) {
1798
+ normalizeBmpTopDown(outputPath);
1799
+ return { converter: "sips", output_path: outputPath };
1800
+ }
1801
+ }
1802
+ const ffmpeg = resolveExecutableFromPath(["ffmpeg"]);
1803
+ if (ffmpeg) {
1804
+ const result = await runProcess(ffmpeg, [
1805
+ "-y",
1806
+ "-i", inputPath,
1807
+ "-vf", `scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height}`,
1808
+ outputPath
1809
+ ]);
1810
+ if (result.code === 0) {
1811
+ normalizeBmpTopDown(outputPath);
1812
+ return { converter: "ffmpeg", output_path: outputPath };
1813
+ }
1814
+ }
1815
+ throw new Error("No local image converter was found. Install ImageMagick, or use the SDK image converter package once published.");
1816
+ }
1817
+
1818
+ function readFdtTotalsize(bytes) {
1819
+ if (bytes.length < 8 || bytes.readUInt32BE(0) !== 0xd00dfeed) return 0;
1820
+ const totalSize = bytes.readUInt32BE(4);
1821
+ return totalSize > 0 && totalSize <= bytes.length ? totalSize : 0;
1822
+ }
1823
+
1824
+ function findUniqueSubBufferOffset(haystack, needle) {
1825
+ let found = -1;
1826
+ let searchFrom = 0;
1827
+ while (searchFrom < haystack.length) {
1828
+ const index = haystack.indexOf(needle, searchFrom);
1829
+ if (index < 0) break;
1830
+ if (found >= 0) return -1;
1831
+ found = index;
1832
+ searchFrom = index + 1;
1833
+ }
1834
+ return found;
1835
+ }
1836
+
1837
+ async function readFitIntegerProperty(imagePath, nodePath, property, workDir) {
1838
+ const fdtget = resolveExecutableFromPath(["fdtget"]);
1839
+ if (!fdtget) return undefined;
1840
+ const result = await runProcess(fdtget, ["-ti", imagePath, nodePath, property], { cwd: workDir });
1841
+ if (result.code !== 0) return undefined;
1842
+ const value = Number.parseInt(result.stdout.trim(), 10);
1843
+ return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
1844
+ }
1845
+
1846
+ async function readFitExternalDataInfo(imagePath, nodeName, workDir) {
1847
+ const nodePath = `/images/${nodeName}`;
1848
+ const offset = await readFitIntegerProperty(imagePath, nodePath, "data-position", workDir);
1849
+ const size = await readFitIntegerProperty(imagePath, nodePath, "data-size", workDir);
1850
+ if (offset === undefined || size === undefined) return undefined;
1851
+ return { offset, size };
1852
+ }
1853
+
1854
+ function sha256Buffer(bytes) {
1855
+ return createHash("sha256").update(bytes).digest("hex");
1856
+ }
1857
+
1858
+ function writeJsonFile(filePath, payload) {
1859
+ mkdirSync(path.dirname(filePath), { recursive: true });
1860
+ writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1861
+ }
1862
+
1863
+ async function localBootLogoCompose(args, sdk, logoPath, outputDir, outputImagePath, manifestPath) {
1864
+ const releaseRoot = sdk.releaseRoot;
1865
+ const toolPath = resourceToolPath(releaseRoot);
1866
+ if (!toolPath) {
1867
+ return {
1868
+ ok: false,
1869
+ error: {
1870
+ code: "resource_tool_missing",
1871
+ message: "本机 SDK 缺少 Rockchip resource_tool,无法在本地重打包启动 Logo 镜像。"
1872
+ }
1873
+ };
1874
+ }
1875
+
1876
+ const baseBootImage = firstExistingPath([
1877
+ args.base_image_path || args.base_image,
1878
+ path.join(releaseRoot, "images", "current", "boot.img"),
1879
+ path.join(releaseRoot, "boot-workspace", "out", "boot.img")
1880
+ ]);
1881
+ if (!baseBootImage) {
1882
+ return {
1883
+ ok: false,
1884
+ error: {
1885
+ code: "base_boot_image_missing",
1886
+ message: "本机 SDK 中没有找到基础 boot.img,无法生成新的启动 Logo 镜像。"
1887
+ }
1888
+ };
1889
+ }
1890
+
1891
+ const baseBootBytes = readFileSync(baseBootImage);
1892
+ const headerSize = readFdtTotalsize(baseBootBytes);
1893
+ if (!headerSize) {
1894
+ return {
1895
+ ok: false,
1896
+ error: {
1897
+ code: "unsupported_boot_image_format",
1898
+ message: "当前 boot.img 不是可直接本地补丁的 FIT 镜像格式。"
1899
+ }
1900
+ };
1901
+ }
1902
+ const fitResource = await readFitExternalDataInfo(baseBootImage, "resource", outputDir);
1903
+ if (!fitResource || fitResource.offset + fitResource.size > baseBootBytes.length) {
1904
+ return {
1905
+ ok: false,
1906
+ error: {
1907
+ code: "fit_resource_not_found",
1908
+ message: "无法从 boot.img 中定位 Rockchip resource 数据段。请确认 SDK 包包含 fdtget 或等效 FIT 工具。"
1909
+ }
1910
+ };
1911
+ }
1912
+
1913
+ const workDir = path.join(outputDir, "local-work");
1914
+ rmSync(workDir, { recursive: true, force: true });
1915
+ mkdirSync(workDir, { recursive: true });
1916
+ const baseResourcePath = path.join(workDir, "base-resource.img");
1917
+ const patchedResourcePath = asString(args.output_resource_path || args.outputResource)
1918
+ || path.join(outputDir, "boot-logo-resource.img");
1919
+ writeFileSync(baseResourcePath, baseBootBytes.subarray(fitResource.offset, fitResource.offset + fitResource.size));
1920
+
1921
+ const unpackResult = await runProcess(toolPath, ["--unpack", `--image=${baseResourcePath}`], { cwd: workDir });
1922
+ if (unpackResult.code !== 0) {
1923
+ return {
1924
+ ok: false,
1925
+ error: {
1926
+ code: "resource_unpack_failed",
1927
+ message: summarizePublicText(unpackResult.stderr || unpackResult.stdout) || "resource_tool unpack failed."
1928
+ }
1929
+ };
1930
+ }
1931
+ const unpackDir = path.join(workDir, "out");
1932
+ const logoBmpPath = path.join(unpackDir, "logo.bmp");
1933
+ const kernelLogoBmpPath = path.join(unpackDir, "logo_kernel.bmp");
1934
+ const dimensions = readBmpDimensions(logoBmpPath);
1935
+ const convertedLogoPath = path.join(outputDir, "logo.bmp");
1936
+ const convertedKernelLogoPath = path.join(outputDir, "logo_kernel.bmp");
1937
+ const converter = await convertImageToBootBmp(logoPath, convertedLogoPath, dimensions.width, dimensions.height);
1938
+ const kernelLogoSource = asString(args.kernel_logo_path || args.kernel_logo) || logoPath;
1939
+ const kernelConverter = await convertImageToBootBmp(kernelLogoSource, convertedKernelLogoPath, dimensions.width, dimensions.height);
1940
+ copyFileSync(convertedLogoPath, logoBmpPath);
1941
+ copyFileSync(convertedKernelLogoPath, kernelLogoBmpPath);
1942
+
1943
+ mkdirSync(path.dirname(patchedResourcePath), { recursive: true });
1944
+ const packResult = await runProcess(toolPath, [
1945
+ "--pack",
1946
+ `--image=${patchedResourcePath}`,
1947
+ "rk-kernel.dtb",
1948
+ "logo.bmp",
1949
+ "logo_kernel.bmp"
1950
+ ], { cwd: unpackDir });
1951
+ if (packResult.code !== 0) {
1952
+ return {
1953
+ ok: false,
1954
+ error: {
1955
+ code: "resource_pack_failed",
1956
+ message: summarizePublicText(packResult.stderr || packResult.stdout) || "resource_tool pack failed."
1957
+ }
1958
+ };
1959
+ }
1960
+
1961
+ let patchedResourceBytes = readFileSync(patchedResourcePath);
1962
+ if (patchedResourceBytes.length > fitResource.size) {
1963
+ const extra = patchedResourceBytes.subarray(fitResource.size);
1964
+ if (extra.every((byte) => byte === 0)) {
1965
+ patchedResourceBytes = patchedResourceBytes.subarray(0, fitResource.size);
1966
+ writeFileSync(patchedResourcePath, patchedResourceBytes);
1967
+ }
1968
+ }
1969
+ if (patchedResourceBytes.length < fitResource.size) {
1970
+ const padded = Buffer.alloc(fitResource.size);
1971
+ patchedResourceBytes.copy(padded, 0);
1972
+ patchedResourceBytes = padded;
1973
+ writeFileSync(patchedResourcePath, patchedResourceBytes);
1974
+ }
1975
+ if (patchedResourceBytes.length !== fitResource.size) {
1976
+ return {
1977
+ ok: false,
1978
+ error: {
1979
+ code: "resource_size_mismatch",
1980
+ message: `重打包后的 resource.img 大小为 ${patchedResourceBytes.length} 字节,但 boot.img 资源槽大小为 ${fitResource.size} 字节。`
1981
+ }
1982
+ };
1983
+ }
1984
+
1985
+ const baseResourceBytes = baseBootBytes.subarray(fitResource.offset, fitResource.offset + fitResource.size);
1986
+ const baseResourceHash = Buffer.from(sha256Buffer(baseResourceBytes), "hex");
1987
+ const patchedResourceHash = Buffer.from(sha256Buffer(patchedResourceBytes), "hex");
1988
+ const hashOffset = findUniqueSubBufferOffset(baseBootBytes.subarray(0, headerSize), baseResourceHash);
1989
+ if (hashOffset < 0) {
1990
+ return {
1991
+ ok: false,
1992
+ error: {
1993
+ code: "fit_resource_hash_not_found",
1994
+ message: "无法在 boot.img FIT 头中唯一定位 resource 哈希,已停止生成,避免输出不可刷写镜像。"
1995
+ }
1996
+ };
1997
+ }
1998
+
1999
+ const outputBootBytes = Buffer.from(baseBootBytes);
2000
+ patchedResourceHash.copy(outputBootBytes, hashOffset);
2001
+ patchedResourceBytes.copy(outputBootBytes, fitResource.offset);
2002
+ mkdirSync(path.dirname(outputImagePath), { recursive: true });
2003
+ writeFileSync(outputImagePath, outputBootBytes);
2004
+ const manifest = {
2005
+ schema_version: "1.0",
2006
+ operation_kind: "image.boot_logo.local_compose",
2007
+ board_id: "taishanpi-1m-rk3566",
2008
+ logo_path: logoPath,
2009
+ kernel_logo_path: kernelLogoSource,
2010
+ base_boot_image: baseBootImage,
2011
+ output_boot_image: outputImagePath,
2012
+ output_resource_image: patchedResourcePath,
2013
+ dimensions,
2014
+ fit_resource: fitResource,
2015
+ fit_resource_hash_offset: hashOffset,
2016
+ base_boot_sha256: sha256Buffer(baseBootBytes),
2017
+ output_boot_sha256: sha256Buffer(outputBootBytes),
2018
+ base_resource_sha256: sha256Buffer(baseResourceBytes),
2019
+ output_resource_sha256: sha256Buffer(patchedResourceBytes),
2020
+ converter: converter.converter,
2021
+ kernel_converter: kernelConverter.converter,
2022
+ cross_platform: true,
2023
+ platform: process.platform,
2024
+ arch: process.arch,
2025
+ observed_at: new Date().toISOString()
2026
+ };
2027
+ writeJsonFile(manifestPath, manifest);
2028
+ return {
2029
+ ok: true,
2030
+ ready_for_flash: true,
2031
+ local_merge_strategy: "local_resource_repack_and_fit_patch",
2032
+ output_image_path: outputImagePath,
2033
+ output_resource_path: patchedResourcePath,
2034
+ manifest_path: manifestPath,
2035
+ base_image_path: baseBootImage,
2036
+ summary_for_user: "启动 Logo 已在本机完成图片转换、resource.img 重打包和 boot.img 补丁生成。",
2037
+ manifest
2038
+ };
2039
+ }
2040
+
2041
+ async function bootLogoUpdate(args) {
2042
+ const logoPath = asString(args.logo_path || args.logo);
2043
+ if (!logoPath) throw new Error("logo_path is required");
2044
+ if (!existsSync(logoPath)) {
2045
+ return {
2046
+ ok: false,
2047
+ action: "boot_logo_update",
2048
+ error: {
2049
+ code: "logo_not_found",
2050
+ message: `Logo image does not exist: ${logoPath}`
2051
+ }
2052
+ };
2053
+ }
2054
+
2055
+ const boardId = asString(args.board_id || args.board) || "taishanpi-1m-rk3566";
2056
+ const outputDir = defaultActionDir("boot-logo", args.output_dir || args.outputDir);
2057
+ const outputImagePath = asString(args.output_image_path || args.outputImage || args.output)
2058
+ || path.join(outputDir, "boot-logo-boot.img");
2059
+ const manifestPath = asString(args.manifest_path || args.manifest) || path.join(outputDir, "boot-logo-compose-manifest.json");
2060
+ const sdk = await ensureLocalToolchain(args, boardId, "images");
2061
+ if (!sdk.releaseRoot) {
2062
+ return {
2063
+ ok: false,
2064
+ action: "boot_logo_update",
2065
+ error: {
2066
+ code: "local_sdk_missing",
2067
+ message: "本机没有可用的 TaishanPi images SDK,无法本地生成启动 Logo 镜像。"
2068
+ },
2069
+ sdk: compactSdkState(sdk.sdkCheck, sdk.sdkInstall)
2070
+ };
2071
+ }
2072
+ const composeSummary = await localBootLogoCompose(args, sdk, logoPath, outputDir, outputImagePath, manifestPath);
2073
+ if (!composeSummary.ok) {
2074
+ return {
2075
+ ok: false,
2076
+ action: "boot_logo_update",
2077
+ error: composeSummary.error,
2078
+ sdk: compactSdkState(sdk.sdkCheck, sdk.sdkInstall),
2079
+ output_dir: outputDir
2080
+ };
2081
+ }
2082
+ const approved = boolValue(args.approved) || boolValue(args.approve);
2083
+ const readyForFlash = composeSummary.ready_for_flash === true;
2084
+ const result = {
2085
+ ok: readyForFlash,
2086
+ action: "boot_logo_update",
2087
+ board: "TaishanPi 1M RK3566",
2088
+ status: readyForFlash ? (approved ? "composed_ready_for_flash" : "ready_for_approval") : "composed_not_flashable",
2089
+ approved,
2090
+ summary_for_user: readyForFlash
2091
+ ? "启动 Logo 已在本机完成格式转换、resource.img 重打包和 boot.img 镜像生成。需要真实写入开发板时,应继续走受控的本地刷写入口。"
2092
+ : "启动 Logo 本地生成未完成,不能直接作为刷写产物。",
2093
+ sdk: compactSdkState(sdk.sdkCheck, sdk.sdkInstall),
2094
+ artifacts: compactObject({
2095
+ logo_path: logoPath,
2096
+ base_image_path: composeSummary.base_image_path,
2097
+ output_image_path: outputImagePath,
2098
+ output_resource_path: composeSummary.output_resource_path,
2099
+ manifest_path: manifestPath
2100
+ }),
2101
+ compose: composeSummary,
2102
+ next_step: readyForFlash
2103
+ ? "如果用户明确批准写入开发板,后续应由同一高层工具调用 Local Bridge 的受控 boot 分区刷写能力,不能让模型手动拼命令。"
2104
+ : "需要修复本地 SDK 图像转换或 resource 重打包工具后再刷写。",
2105
+ raw_omitted: boolValue(args.include_raw) ? undefined : true
2106
+ };
2107
+ if (boolValue(args.include_raw)) {
2108
+ result.raw = compactObject({ compose_result: composeSummary });
2109
+ }
2110
+ return result;
2111
+ }
2112
+
2113
+ function shellQuote(value) {
2114
+ const text = asString(value);
2115
+ return `'${text.replaceAll("'", "'\\''")}'`;
2116
+ }
2117
+
2118
+ function defaultQtExampleForRequest(request) {
2119
+ const text = asString(request).toLowerCase();
2120
+ if (text.includes("modbus") || text.includes("uart") || text.includes("串口") || text.includes("协议")) {
2121
+ const relative = path.join("board-packs", "taishanpi", "examples", "modbus-loop-demo", "taishanpi");
2122
+ return {
2123
+ sourcePath: firstExistingPath([path.join(process.cwd(), relative), path.join(REPO_ROOT, relative)]),
2124
+ targetName: "modbus_loop_demo",
2125
+ template: "taishanpi_modbus_loop_demo"
2126
+ };
2127
+ }
2128
+ return {
2129
+ sourcePath: "",
2130
+ targetName: "qt_smoke",
2131
+ template: "qt_smoke"
2132
+ };
2133
+ }
2134
+
2135
+ function qtRuntimeCommand(remotePath) {
2136
+ const remote = shellQuote(remotePath);
2137
+ return [
2138
+ "export QT_ROOT=/userdata/rootfs/qt6-rk3566-llvm",
2139
+ "export LD_LIBRARY_PATH=$QT_ROOT/lib:$LD_LIBRARY_PATH",
2140
+ "export QT_PLUGIN_PATH=$QT_ROOT/plugins",
2141
+ "export QT_QPA_PLATFORM_PLUGIN_PATH=$QT_ROOT/plugins/platforms",
2142
+ "export QML2_IMPORT_PATH=$QT_ROOT/qml",
2143
+ "export QML_IMPORT_PATH=$QT_ROOT/qml",
2144
+ "export QT_QUICK_CONTROLS_STYLE=Basic",
2145
+ `chmod +x ${remote}`,
2146
+ remote
2147
+ ].join(" && ");
2148
+ }
2149
+
2150
+ function artifactPathFromBuild(buildResult) {
2151
+ const data = responseData(buildResult) || {};
2152
+ return asString(data.artifact_path || data.artifactPath || data.output_path || data.executable_path);
2153
+ }
2154
+
2155
+ function taishanpiHostFromDeviceList(scanResult) {
2156
+ const data = responseData(scanResult) || {};
2157
+ const devices = Array.isArray(data.development_boards) ? data.development_boards : [];
2158
+ for (const device of devices) {
2159
+ if (device?.board_id !== "taishanpi") continue;
2160
+ const host = asString(device.host || device.link_local?.host);
2161
+ if (host && device.status !== "bootloader") return host;
2162
+ }
2163
+ return "";
2164
+ }
2165
+
2166
+ async function taishanpiQtAppWorkflow(args) {
2167
+ const request = asString(args.request || args.prompt);
2168
+ const boardId = "taishanpi-1m-rk3566";
2169
+ const selected = defaultQtExampleForRequest(request);
2170
+ const outputDir = defaultActionDir("qt-app", args.output_dir || args.outputDir);
2171
+ const explicitSource = asString(args.source_path || args.source);
2172
+ let sourcePath = explicitSource || selected.sourcePath;
2173
+ const projectDir = asString(args.project_dir || args.projectDir);
2174
+ if (!explicitSource && projectDir && selected.sourcePath) {
2175
+ mkdirSync(path.dirname(projectDir), { recursive: true });
2176
+ cpSync(selected.sourcePath, projectDir, { recursive: true, force: true });
2177
+ sourcePath = projectDir;
2178
+ }
2179
+ if (!sourcePath && selected.template !== "qt_smoke") {
2180
+ return {
2181
+ ok: false,
2182
+ action: "taishanpi_qt_app_workflow",
2183
+ error: {
2184
+ code: "qt_template_not_found",
2185
+ message: "未找到匹配需求的官方 Qt 示例模板。",
2186
+ remediation: "请在包含 board-packs 的 Embed Labs 项目目录中运行,或传入 source_path 指向已有 Qt/CMake 工程。"
2187
+ },
2188
+ template: selected.template
2189
+ };
2190
+ }
2191
+ const targetName = asString(args.target_name || args.targetName) || selected.targetName;
2192
+ const buildDir = asString(args.build_dir || args.buildDir) || path.join(outputDir, targetName || "qt-app-build");
2193
+
2194
+ const sdk = await ensureLocalToolchain(args, boardId, "qt");
2195
+ const buildArgs = { build_dir: buildDir, target_name: targetName, release_root: sdk.releaseRoot };
2196
+ if (sourcePath) buildArgs.source_path = sourcePath;
2197
+ const buildResult = await localQtSmoke(buildArgs);
2198
+ const artifactPath = artifactPathFromBuild(buildResult);
2199
+
2200
+ let deployResult;
2201
+ const approved = boolValue(args.approved) || boolValue(args.approve);
2202
+ const shouldDeploy = boolValue(args.deploy) || boolValue(args.run);
2203
+ if (shouldDeploy && approved) {
2204
+ const scan = await runEmbed(["device", "list"]);
2205
+ const host = asString(args.host) || taishanpiHostFromDeviceList(scan);
2206
+ if (!host) {
2207
+ deployResult = {
2208
+ ok: false,
2209
+ error: {
2210
+ code: "runtime_host_not_found",
2211
+ message: "Qt 程序已完成交叉编译,但当前未检测到可通过 SSH 部署的泰山派运行态主机。",
2212
+ remediation: "确认泰山派已启动初始化系统并连接到当前电脑/网络后,再调用同一工具部署。"
2213
+ }
2214
+ };
2215
+ } else if (!artifactPath) {
2216
+ deployResult = {
2217
+ ok: false,
2218
+ error: {
2219
+ code: "artifact_not_found",
2220
+ message: "Qt 构建没有返回可部署的可执行文件路径。"
2221
+ }
2222
+ };
2223
+ } else {
2224
+ const remotePath = asString(args.remote_path || args.remotePath)
2225
+ || `/userdata/embed-labs/apps/${path.basename(artifactPath)}`;
2226
+ const command = ["deploy", "taishanpi", "--host", host, "--artifact", artifactPath, "--remote-path", remotePath, "--approve"];
2227
+ if (boolValue(args.run)) {
2228
+ command.push("--run", "--run-command", qtRuntimeCommand(remotePath));
2229
+ }
2230
+ deployResult = await runEmbed(command);
2231
+ }
2232
+ }
2233
+
2234
+ const buildData = responseData(buildResult) || {};
2235
+ const deployData = responseData(deployResult) || {};
2236
+ const result = {
2237
+ ok: buildResult?.response?.ok === true && (!deployResult || deployResult?.response?.ok === true || deployResult?.ok === false),
2238
+ action: "taishanpi_qt_app_workflow",
2239
+ board: "TaishanPi 1M RK3566",
2240
+ status: deployResult ? (deployResult?.response?.ok === true ? "built_and_deployed" : "built_deploy_pending") : "built",
2241
+ template: selected.template,
2242
+ request: request || undefined,
2243
+ sdk: compactSdkState(sdk.sdkCheck, sdk.sdkInstall),
2244
+ source_path: sourcePath || "built-in Qt smoke source",
2245
+ build: compactObject({
2246
+ build_dir: buildDir,
2247
+ target_name: targetName,
2248
+ artifact_path: artifactPath,
2249
+ artifact_size_bytes: buildData.artifact_size_bytes,
2250
+ artifact_sha256: buildData.artifact_sha256,
2251
+ summary_for_user: buildData.summary_for_user
2252
+ }),
2253
+ deploy: deployResult ? compactObject({
2254
+ ok: deployResult?.response?.ok ?? deployResult?.ok,
2255
+ host: deployData.host,
2256
+ remote_path: deployData.remote_path,
2257
+ ran: deployData.ran,
2258
+ error: deployResult?.error || deployResult?.response?.error
2259
+ }) : undefined,
2260
+ summary_for_user: deployResult?.response?.ok === true
2261
+ ? "Qt 程序已按泰山派 Qt 运行库路径完成构建、部署并启动。"
2262
+ : "Qt 程序已通过本机 LLVM/Qt 工具链完成交叉编译。",
2263
+ next_step: deployResult ? "查看部署/运行结果;如需协议采集,再调用 RP2350 Monitor 的对应高层工具。" : "如需上板运行,用户批准后用同一工具传入 deploy=true、run=true、approved=true。",
2264
+ raw_omitted: boolValue(args.include_raw) ? undefined : true
2265
+ };
2266
+ if (boolValue(args.include_raw)) {
2267
+ result.raw = compactObject({ build_result: buildResult, deploy_result: deployResult });
2268
+ }
2269
+ return result;
2270
+ }
2271
+
2272
+ async function localToolchainLatest(args) {
2273
+ const command = ["local", "toolchain", "latest"];
2274
+ pushOptional(command, "board", args.board_id || args.board);
2275
+ pushOptional(command, "channel", args.channel);
2276
+ pushOptional(command, "metadata-root", args.metadata_root || args.metadataRoot);
2277
+ return await runEmbed(command);
2278
+ }
2279
+
2280
+ async function localToolchainList(args) {
2281
+ const command = ["local", "toolchain", "list"];
2282
+ pushOptional(command, "board", args.board_id || args.board);
2283
+ pushOptional(command, "channel", args.channel);
2284
+ pushOptional(command, "metadata-root", args.metadata_root || args.metadataRoot);
2285
+ pushOptional(command, "install-root", args.install_root || args.installRoot);
2286
+ return await runEmbed(command);
2287
+ }
2288
+
2289
+ async function localToolchainInstalled(args) {
2290
+ const command = ["local", "toolchain", "installed"];
2291
+ pushOptional(command, "board", args.board_id || args.board);
2292
+ pushOptional(command, "channel", args.channel);
2293
+ pushOptional(command, "metadata-root", args.metadata_root || args.metadataRoot);
2294
+ pushOptional(command, "install-root", args.install_root || args.installRoot);
2295
+ return await runEmbed(command);
2296
+ }
2297
+
2298
+ async function localToolchainCurrent(args) {
2299
+ const command = ["local", "toolchain", "current"];
2300
+ pushOptional(command, "install-root", args.install_root || args.installRoot);
2301
+ return await runEmbed(command);
2302
+ }
2303
+
2304
+ async function localToolchainInstall(args) {
2305
+ const command = ["local", "toolchain", "install"];
2306
+ pushOptional(command, "board", args.board_id || args.board);
2307
+ pushOptional(command, "channel", args.channel);
2308
+ pushOptional(command, "metadata-root", args.metadata_root || args.metadataRoot);
2309
+ pushOptional(command, "source-url", args.source_url || args.sourceUrl);
2310
+ pushOptional(command, "source-release-root", args.source_release_root || args.sourceReleaseRoot);
2311
+ pushOptional(command, "install-root", args.install_root || args.installRoot);
2312
+ pushOptional(command, "mode", args.mode);
2313
+ if (boolValue(args.force)) command.push("--force");
2314
+ return await runEmbed(command);
2315
+ }
2316
+
2317
+ async function localToolchainUninstall(args) {
2318
+ const boardId = asString(args.board_id || args.board);
2319
+ if (!boardId) throw new Error("board_id is required");
2320
+ const command = ["local", "toolchain", "uninstall", "--board", boardId];
2321
+ pushOptional(command, "install-root", args.install_root || args.installRoot);
2322
+ return await runEmbed(command);
2323
+ }
2324
+
2325
+ async function localToolchainValidate(args) {
2326
+ const command = ["local", "toolchain", "validate"];
2327
+ pushOptional(command, "board", args.board_id || args.board);
2328
+ pushOptional(command, "release-root", args.release_root || args.releaseRoot);
2329
+ pushOptional(command, "mode", args.mode);
2330
+ return await runEmbed(command);
2331
+ }
2332
+
2333
+ function isTaishanpiInitialTarget(value) {
2334
+ const text = asString(value).toLowerCase();
2335
+ return text.includes("taishan") || text.includes("tspi") || text.includes("rk3566") || text.includes("1m");
2336
+ }
2337
+
2338
+ function isRp2350InitialTarget(value) {
2339
+ const text = asString(value).toLowerCase();
2340
+ return text.includes("rp2350") || text.includes("pico") || text.includes("coloreasy");
2341
+ }
2342
+
2343
+ async function initialImageFlash(args) {
2344
+ const requestedBoard = asString(args.board_id || args.board);
2345
+ if (isRp2350InitialTarget(requestedBoard)) {
2346
+ return await rp2350InitialFirmwareFlash(args);
2347
+ }
2348
+ if (isTaishanpiInitialTarget(requestedBoard)) {
2349
+ return await taishanpiInitialImageFlash(args);
2350
+ }
2351
+
2352
+ const scan = await runEmbed(["device", "list"]);
2353
+ const scanData = responseData(scan) || {};
2354
+ const devices = Array.isArray(scanData.development_boards) ? scanData.development_boards : [];
2355
+ const candidates = devices
2356
+ .filter((item) => item?.board_id === "taishanpi" || isRp2350Device(item))
2357
+ .map((item) => compactObject({
2358
+ local_device_id: item.local_device_id || item.device_id,
2359
+ board_id: item.board_id,
2360
+ variant_id: item.variant_id,
2361
+ display_name: item.display_name || item.name,
2362
+ status: item.status
2363
+ }));
2364
+
2365
+ if (candidates.length === 1) {
2366
+ const candidate = candidates[0];
2367
+ const nextArgs = { ...args, local_device_id: args.local_device_id || candidate.local_device_id };
2368
+ if (candidate.board_id === "taishanpi") return await taishanpiInitialImageFlash(nextArgs);
2369
+ return await rp2350InitialFirmwareFlash({ ...nextArgs, board_id: candidate.variant_id || candidate.board_id });
2370
+ }
2371
+
2372
+ return {
2373
+ ok: false,
2374
+ error: {
2375
+ code: candidates.length > 1 ? "ambiguous_board" : "target_not_found",
2376
+ message: candidates.length > 1
2377
+ ? "检测到多块可刷写开发板,请指定要刷写的开发板。"
2378
+ : "当前未检测到可刷写初始化镜像的支持开发板。",
2379
+ candidates,
2380
+ remediation: candidates.length > 1
2381
+ ? "重新调用 dbt_initial_image_flash,并传入对应 local_device_id。"
2382
+ : "请连接开发板,或让开发板进入 Loader/Maskrom/BOOTSEL 后重新查询开发板状态。"
2383
+ }
2384
+ };
2385
+ }
2386
+
2387
+ function compactSdkState(sdkCheck, sdkInstall) {
2388
+ const checkData = responseData(sdkCheck) || {};
2389
+ const installData = responseData(sdkInstall) || {};
2390
+ const validation = objectValue(installData.validation) || {};
2391
+ const repairedOk = typeof installData.ok === "boolean" ? installData.ok : validation.ok;
2392
+ return compactObject({
2393
+ checked: !!sdkCheck,
2394
+ installed_or_repaired: !!sdkInstall,
2395
+ ok: sdkInstall ? repairedOk : checkData.ok,
2396
+ release_root: installData.release_root || validation.release_root || checkData.release_root,
2397
+ mode: installData.mode || validation.mode || checkData.mode,
2398
+ summary_for_user: installData.summary_for_user || validation.summary_for_user || checkData.summary_for_user
2399
+ });
2400
+ }
2401
+
2402
+ function compactFlashPlan(plan, boardLabel, imagePath) {
2403
+ const data = responseData(plan) || {};
2404
+ const target = data.target_device || {};
2405
+ return compactObject({
2406
+ ready: data.ready === true,
2407
+ board: boardLabel || data.board_id,
2408
+ boot_mode: data.boot_mode,
2409
+ local_device_id: data.local_device_id || target.local_device_id || target.device_id,
2410
+ target: target.display_name || target.name,
2411
+ target_status: target.status,
2412
+ image_dir: data.image_dir,
2413
+ image_dir_source: data.image_dir_source,
2414
+ artifact_path: imagePath,
2415
+ approval_required: data.approval_required === true,
2416
+ destructive: data.destructive === true,
2417
+ step_count: Array.isArray(data.steps) ? data.steps.length : undefined,
2418
+ partitions: Array.isArray(data.partitions)
2419
+ ? data.partitions.map((item) => item.partition).filter(Boolean)
2420
+ : undefined,
2421
+ warnings: compactWarnings(data.warnings),
2422
+ errors: data.errors
2423
+ });
2424
+ }
2425
+
2426
+ function compactWarnings(warnings) {
2427
+ if (!Array.isArray(warnings) || warnings.length === 0) return undefined;
2428
+ return warnings.map((item) => compactObject({
2429
+ code: item.code,
2430
+ message: item.message,
2431
+ partitions: Array.isArray(item.partitions) ? item.partitions.map((part) => part.partition).filter(Boolean) : undefined
2432
+ }));
2433
+ }
2434
+
2435
+ function localJobIdFromRun(response) {
2436
+ const data = responseData(response) || {};
2437
+ return asString(data.local_job_id || data.job_id);
2438
+ }
2439
+
2440
+ async function waitForLocalJob(jobId, timeoutMs = 180000) {
2441
+ const deadline = Date.now() + Math.max(5000, timeoutMs);
2442
+ let lastStatus = null;
2443
+ while (Date.now() < deadline) {
2444
+ lastStatus = await runEmbed(["task", "status", jobId]);
2445
+ const data = responseData(lastStatus) || {};
2446
+ const status = asString(data.status);
2447
+ if (["completed", "failed", "cancelled"].includes(status)) {
2448
+ return lastStatus;
2449
+ }
2450
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2451
+ }
2452
+ return lastStatus || { ok: false, response: { ok: false, error: { code: "job_status_timeout", message: "Timed out waiting for local flash job." } } };
2453
+ }
2454
+
2455
+ function compactFlashRunResult(runResult, finalStatus) {
2456
+ const started = responseData(runResult) || {};
2457
+ const finalData = responseData(finalStatus) || {};
2458
+ const data = Object.keys(finalData).length > 0 ? finalData : started;
2459
+ return compactObject({
2460
+ local_job_id: data.local_job_id || started.local_job_id,
2461
+ status: data.status || started.status,
2462
+ started_at: data.started_at || started.started_at,
2463
+ updated_at: data.updated_at || started.updated_at,
2464
+ completed_at: data.completed_at,
2465
+ error: data.error,
2466
+ logs: Array.isArray(data.logs) ? data.logs.slice(-8) : undefined
2467
+ });
2468
+ }
2469
+
2470
+ function compactInitialFlashResult({ boardLabel, sdkCheck, sdkInstall, imageDir, artifactPath, plan, runResult, finalStatus, approved, includeRaw }) {
2471
+ const planSummary = compactFlashPlan(plan, boardLabel, artifactPath);
2472
+ const runSummary = runResult ? compactFlashRunResult(runResult, finalStatus) : undefined;
2473
+ const status = runSummary?.status || (planSummary.ready ? "ready_for_approval" : "not_ready");
2474
+ const summary = runSummary
2475
+ ? (runSummary.status === "completed"
2476
+ ? `${boardLabel} 初始化镜像刷写已完成。`
2477
+ : runSummary.status === "failed"
2478
+ ? `${boardLabel} 初始化镜像刷写失败,请查看 error 和最近日志。`
2479
+ : `${boardLabel} 初始化镜像刷写任务已启动,当前状态:${runSummary.status || "running"}。`)
2480
+ : (planSummary.ready
2481
+ ? `${boardLabel} 初始化镜像刷写计划已就绪,确认后可直接执行。`
2482
+ : `${boardLabel} 初始化镜像刷写计划未就绪。`);
2483
+ const result = {
2484
+ ok: !runSummary?.error && planSummary.ready === true,
2485
+ action: "initial_image_flash",
2486
+ board: boardLabel,
2487
+ status,
2488
+ approved: approved === true,
2489
+ summary_for_user: summary,
2490
+ sdk: compactSdkState(sdkCheck, sdkInstall),
2491
+ plan: planSummary,
2492
+ run: runSummary,
2493
+ next_step: runSummary
2494
+ ? (runSummary.status === "completed" ? "可以重新查询开发板状态,确认系统已启动。" : "继续查询该 local_job_id 的任务状态。")
2495
+ : (planSummary.ready ? "用户确认刷写后,调用同一工具并传入 approved=true。" : "根据 errors/warnings 修复后重试。"),
2496
+ raw_omitted: includeRaw ? undefined : true
2497
+ };
2498
+ if (includeRaw) {
2499
+ result.raw = compactObject({ sdk_check: sdkCheck, sdk_install: sdkInstall, flash_plan: plan, flash_result: runResult, final_status: finalStatus });
2500
+ }
2501
+ return result;
2502
+ }
2503
+
2504
+ async function taishanpiInitialImageFlash(args) {
2505
+ const sdkBoardId = asString(args.board_id || args.board) || "taishanpi-1m-rk3566";
2506
+ const explicitImageDir = asString(args.image_dir || args.imageDir);
2507
+ let sdkCheck = null;
2508
+ let sdkInstall = null;
2509
+ let releaseRoot = "";
2510
+ if (!explicitImageDir) {
2511
+ sdkCheck = await localToolchainValidate({ board_id: sdkBoardId, mode: "images" });
2512
+ const checkData = responseData(sdkCheck) || {};
2513
+ if (checkData.ok) {
2514
+ releaseRoot = asString(checkData.release_root);
2515
+ } else {
2516
+ const installArgs = {
2517
+ board_id: sdkBoardId,
2518
+ channel: args.channel,
2519
+ metadata_root: args.metadata_root || args.metadataRoot,
2520
+ install_root: args.install_root || args.installRoot,
2521
+ mode: "images",
2522
+ force: args.force
2523
+ };
2524
+ sdkInstall = await localToolchainInstall(installArgs);
2525
+ const installedData = responseData(sdkInstall) || {};
2526
+ releaseRoot = asString(installedData.release_root);
2527
+ }
2528
+ }
2529
+ const imageDir = explicitImageDir || (releaseRoot ? path.join(releaseRoot, "images", "current") : "");
2530
+ const localDeviceId = asString(args.local_device_id || args.localDeviceId || args.device_id || args.deviceId);
2531
+ const planCommand = ["flash", "plan", "--board", "taishanpi"];
2532
+ if (imageDir) pushOptional(planCommand, "image-dir", imageDir);
2533
+ if (localDeviceId) pushOptional(planCommand, "local-device-id", localDeviceId);
2534
+ const plan = await runEmbed(planCommand);
2535
+ const approved = boolValue(args.approved) || boolValue(args.approve);
2536
+ if (!approved) {
2537
+ return compactInitialFlashResult({
2538
+ boardLabel: "TaishanPi 1M RK3566",
2539
+ sdkCheck,
2540
+ sdkInstall,
2541
+ imageDir,
2542
+ plan,
2543
+ approved,
2544
+ includeRaw: boolValue(args.include_raw)
2545
+ });
2546
+ }
2547
+ const runCommand = ["flash", "run", "--board", "taishanpi", "--approve"];
2548
+ if (imageDir) pushOptional(runCommand, "image-dir", imageDir);
2549
+ if (localDeviceId) pushOptional(runCommand, "local-device-id", localDeviceId);
2550
+ const flashed = await runEmbed(runCommand);
2551
+ const wait = args.wait === undefined ? true : boolValue(args.wait);
2552
+ const jobId = localJobIdFromRun(flashed);
2553
+ const finalStatus = wait && jobId ? await waitForLocalJob(jobId, Number(args.timeout_ms) || 180000) : undefined;
2554
+ return compactInitialFlashResult({
2555
+ boardLabel: "TaishanPi 1M RK3566",
2556
+ sdkCheck,
2557
+ sdkInstall,
2558
+ imageDir,
2559
+ plan,
2560
+ runResult: flashed,
2561
+ finalStatus,
2562
+ approved,
2563
+ includeRaw: boolValue(args.include_raw)
2564
+ });
2565
+ }
2566
+
2567
+ async function rp2350MonitorFirmwareFlash(args) {
2568
+ const boardId = asString(args.board_id || args.board) || "pico2w-rp2350-monitor";
2569
+ const installArgs = {
2570
+ board_id: boardId,
2571
+ channel: args.channel,
2572
+ metadata_root: args.metadata_root || args.metadataRoot,
2573
+ install_root: args.install_root || args.installRoot,
2574
+ mode: "firmware",
2575
+ force: args.force
2576
+ };
2577
+ const installed = await localToolchainInstall(installArgs);
2578
+ const installedData = responseData(installed) || {};
2579
+ const releaseRoot = asString(installedData.release_root);
2580
+ const artifactPath = asString(args.artifact_path || args.artifact)
2581
+ || (releaseRoot ? path.join(releaseRoot, "toolkit-runtime", "rp2350-monitor", "firmware", "rp2350_monitor.uf2") : "");
2582
+ if (!artifactPath) {
2583
+ throw new Error("RP2350 Monitor firmware UF2 could not be resolved after local toolchain install.");
2584
+ }
2585
+ const plan = await runEmbed(["flash", "plan", "--board", "rp2350", "--artifact", artifactPath]);
2586
+ const approved = boolValue(args.approved) || boolValue(args.approve);
2587
+ if (!approved) {
2588
+ const planData = responseData(plan) || plan;
2589
+ const automaticBootsel = !!planData.automatic_bootsel
2590
+ || (Array.isArray(planData.steps) && planData.steps.some((step) => step && step.kind === "picotool_load_force_usb"));
2591
+ return {
2592
+ ok: true,
2593
+ firmware_install: installed,
2594
+ artifact_path: artifactPath,
2595
+ flash_plan: plan,
2596
+ next_step: automaticBootsel
2597
+ ? "Flash plan is ready. After user approval, call this tool again with approved=true."
2598
+ : "Flash plan needs a visible UF2 target volume before it can run."
2599
+ };
2600
+ }
2601
+ const flashed = await runEmbed(["flash", "run", "--board", "rp2350", "--artifact", artifactPath, "--approve"]);
2602
+ return {
2603
+ ok: true,
2604
+ firmware_install: installed,
2605
+ artifact_path: artifactPath,
2606
+ flash_plan: plan,
2607
+ flash_result: flashed
2608
+ };
2609
+ }
2610
+
2611
+ function rp2350InitialFirmwarePath(boardId, releaseRoot) {
2612
+ const normalized = boardId.toLowerCase();
2613
+ const boardDir = normalized.includes("coloreasy") ? "coloreasypico2" : "pico2w";
2614
+ return releaseRoot ? path.join(releaseRoot, "rp2350-initial-firmware", boardDir, "initial.uf2") : "";
2615
+ }
2616
+
2617
+ async function rp2350InitialFirmwareFlash(args) {
2618
+ const boardId = asString(args.board_id || args.board) || "pico2w-rp2350-monitor";
2619
+ const installArgs = {
2620
+ board_id: boardId,
2621
+ channel: args.channel,
2622
+ metadata_root: args.metadata_root || args.metadataRoot,
2623
+ install_root: args.install_root || args.installRoot,
2624
+ mode: "firmware",
2625
+ force: args.force
2626
+ };
2627
+ const installed = await localToolchainInstall(installArgs);
2628
+ const installedData = responseData(installed) || {};
2629
+ const releaseRoot = asString(installedData.release_root);
2630
+ const artifactPath = asString(args.artifact_path || args.artifact) || rp2350InitialFirmwarePath(boardId, releaseRoot);
2631
+ if (!artifactPath) {
2632
+ throw new Error("RP2350 initialization firmware UF2 could not be resolved after local toolchain install.");
2633
+ }
2634
+ const plan = await runEmbed(["flash", "plan", "--board", "rp2350", "--artifact", artifactPath]);
2635
+ const approved = boolValue(args.approved) || boolValue(args.approve);
2636
+ if (!approved) {
2637
+ const planData = responseData(plan) || plan;
2638
+ const automaticBootsel = !!planData.automatic_bootsel
2639
+ || (Array.isArray(planData.steps) && planData.steps.some((step) => step && step.kind === "picotool_load_force_usb"));
2640
+ return {
2641
+ ok: true,
2642
+ firmware_install: installed,
2643
+ artifact_path: artifactPath,
2644
+ flash_plan: plan,
2645
+ next_step: automaticBootsel
2646
+ ? "Initialization flash plan is ready. After user approval, call this tool again with approved=true."
2647
+ : "Initialization flash plan needs a visible UF2 target volume before it can run."
2648
+ };
2649
+ }
2650
+ const flashed = await runEmbed(["flash", "run", "--board", "rp2350", "--artifact", artifactPath, "--approve"]);
2651
+ return {
2652
+ ok: true,
2653
+ firmware_install: installed,
2654
+ artifact_path: artifactPath,
2655
+ flash_plan: plan,
2656
+ flash_result: flashed
2657
+ };
2658
+ }
2659
+
2660
+ async function pluginUpdateCheck(args) {
2661
+ const command = ["plugin", "update", "check"];
2662
+ pushOptional(command, "release-url", args.release_url || args.releaseUrl);
2663
+ pushOptional(command, "target", args.target);
2664
+ pushOptional(command, "codex-target", args.codex_target || args.codexTarget);
2665
+ pushOptional(command, "opencode-target", args.opencode_target || args.opencodeTarget);
2666
+ return await runEmbed(command);
2667
+ }
2668
+
2669
+ async function pluginUpdate(args) {
2670
+ const target = asString(args.target_plugin || args.plugin || "all").toLowerCase();
2671
+ if (!["codex", "opencode", "all"].includes(target)) {
2672
+ throw new Error("target_plugin must be codex, opencode, or all");
2673
+ }
2674
+ const command = ["plugin", "update", target];
2675
+ pushOptional(command, "release-url", args.release_url || args.releaseUrl);
2676
+ pushOptional(command, "target", args.target);
2677
+ pushOptional(command, "codex-target", args.codex_target || args.codexTarget);
2678
+ pushOptional(command, "opencode-target", args.opencode_target || args.opencodeTarget);
2679
+ return await runEmbed(command);
2680
+ }
2681
+
2682
+ async function localCompile(args) {
2683
+ const sourcePath = asString(args.source_path || args.source);
2684
+ const outputPath = asString(args.output_path || args.output);
2685
+ if (!sourcePath) throw new Error("source_path is required");
2686
+ if (!outputPath) throw new Error("output_path is required");
2687
+ const command = ["local", "compile", "taishanpi", "--source", sourcePath, "--output", outputPath];
2688
+ pushOptional(command, "account", args.account_id || args.account);
2689
+ pushOptional(command, "release-root", args.release_root || args.releaseRoot);
2690
+ return await runEmbed(command);
2691
+ }
2692
+
2693
+ async function localQtSmoke(args) {
2694
+ const buildDir = asString(args.build_dir || args.buildDir);
2695
+ if (!buildDir) throw new Error("build_dir is required");
2696
+ const command = ["local", "build", "qt-smoke", "--build-dir", buildDir];
2697
+ pushOptional(command, "account", args.account_id || args.account);
2698
+ pushOptional(command, "source", args.source_path || args.source);
2699
+ pushOptional(command, "target-name", args.target_name || args.targetName);
2700
+ pushOptional(command, "release-root", args.release_root || args.releaseRoot);
2701
+ return await runEmbed(command);
2702
+ }
2703
+
2704
+ function serverWorkspaceDisabledResult() {
2705
+ return {
2706
+ ok: false,
2707
+ error: {
2708
+ code: "server_workspaces_disabled",
2709
+ message: "Embed Labs now uses the user's local SDK/toolchain environment for source edits, builds, and board operation. Server-side user workspaces, Docker builds, and image generation are not exposed by the plugin.",
2710
+ remediation: "Use dbt_local_toolchain_install, dbt_local_toolchain_validate, dbt_local_compile, dbt_local_qt_smoke, and Local Bridge board tools. Cloud API remains available for account login, device binding, board knowledge, plugin releases, MCP service logs, and SDK downloads."
2711
+ }
2712
+ };
2713
+ }
2714
+
2715
+ function isServerWorkspaceToolName(name) {
2716
+ return [
2717
+ "dbt_workspace_provision",
2718
+ "dbt_application_generate",
2719
+ "dbt_source_list",
2720
+ "dbt_source_get",
2721
+ "dbt_source_search",
2722
+ "dbt_source_put",
2723
+ "dbt_source_patch",
2724
+ "dbt_application_compile",
2725
+ "dbt_image_generate",
2726
+ "dbt_workspace_release"
2727
+ ].includes(name);
2728
+ }
2729
+
2730
+ function isServerWorkspaceAction(action) {
2731
+ return [
2732
+ "provision",
2733
+ "workspace-provision",
2734
+ "source-list",
2735
+ "list-source",
2736
+ "source-get",
2737
+ "source-read",
2738
+ "read-source",
2739
+ "source-search",
2740
+ "search-source",
2741
+ "source-put",
2742
+ "source-write",
2743
+ "write-source",
2744
+ "source-patch",
2745
+ "patch-source",
2746
+ "app",
2747
+ "application",
2748
+ "application-generate",
2749
+ "build-run",
2750
+ "compile",
2751
+ "application-compile",
2752
+ "image",
2753
+ "image-generate",
2754
+ "release",
2755
+ "workspace-release"
2756
+ ].includes(action);
2757
+ }
2758
+
2759
+ async function workspaceProvision(args) {
2760
+ const account = asString(args.account_id || args.account);
2761
+ const project = asString(args.project_id || args.project);
2762
+ if (!account) throw new Error("account_id is required");
2763
+ if (!project) throw new Error("project_id is required");
2764
+ const command = [
2765
+ "build",
2766
+ "workspace",
2767
+ "provision",
2768
+ "--account",
2769
+ account,
2770
+ "--project",
2771
+ project,
2772
+ "--template",
2773
+ asString(args.template_id || args.template) || DEFAULT_TEMPLATE_ID,
2774
+ "--copy-mode",
2775
+ asString(args.copy_mode) || "skeleton"
2776
+ ];
2777
+ if (boolValue(args.dry_run)) command.push("--dry-run");
2778
+ return await runEmbed(command);
2779
+ }
2780
+
2781
+ async function applicationGenerate(args) {
2782
+ const workspace = asString(args.workspace_id || args.workspace);
2783
+ const prompt = asString(args.prompt || args.request);
2784
+ if (!workspace) throw new Error("workspace_id is required");
2785
+ if (!prompt) throw new Error("prompt is required");
2786
+ const command = ["build", "application", "generate", "--workspace", workspace, "--prompt", prompt];
2787
+ pushOptional(command, "account", args.account_id || args.account);
2788
+ pushOptional(command, "provider", args.provider);
2789
+ pushOptional(command, "model", args.model);
2790
+ pushOptional(command, "target", args.target);
2791
+ const generated = await runEmbed(command);
2792
+ if (!asString(args.download_output)) return generated;
2793
+ const artifact = pickArtifact(generated, "firmware");
2794
+ if (!artifact?.artifact_id) throw new Error("No downloadable application artifact found");
2795
+ const downloaded = await runEmbed(["artifact", "download", artifact.artifact_id, "--output", args.download_output]);
2796
+ return { generated, selected_artifact: artifact, downloaded };
2797
+ }
2798
+
2799
+ async function sourceList(args) {
2800
+ const workspace = asString(args.workspace_id || args.workspace);
2801
+ if (!workspace) throw new Error("workspace_id is required");
2802
+ return await runEmbed(["build", "workspace", "source", "list", workspace]);
2803
+ }
2804
+
2805
+ async function sourceGet(args) {
2806
+ const workspace = asString(args.workspace_id || args.workspace);
2807
+ const sourcePath = asString(args.source_path || args.path);
2808
+ if (!workspace) throw new Error("workspace_id is required");
2809
+ if (!sourcePath) throw new Error("source_path is required");
2810
+ const command = ["build", "workspace", "source", "get", workspace, "--path", sourcePath];
2811
+ pushOptional(command, "output", args.output_path || args.output);
2812
+ return await runEmbed(command);
2813
+ }
2814
+
2815
+ async function sourceSearch(args) {
2816
+ const workspace = asString(args.workspace_id || args.workspace);
2817
+ if (!workspace) throw new Error("workspace_id is required");
2818
+ const command = ["build", "workspace", "source", "search", workspace];
2819
+ pushOptional(command, "query", args.query);
2820
+ pushOptional(command, "glob", args.glob);
2821
+ pushOptional(command, "limit", args.limit);
2822
+ return await runEmbed(command);
2823
+ }
2824
+
2825
+ async function sourcePut(args) {
2826
+ const workspace = asString(args.workspace_id || args.workspace);
2827
+ const localPath = asString(args.local_path || args.local);
2828
+ const sourcePath = asString(args.source_path || args.path);
2829
+ if (!workspace) throw new Error("workspace_id is required");
2830
+ if (!localPath) throw new Error("local_path is required");
2831
+ if (!sourcePath) throw new Error("source_path is required");
2832
+ const command = ["build", "workspace", "source", "put", workspace, "--file", `${localPath}:${sourcePath}`];
2833
+ pushOptional(command, "account", args.account_id || args.account);
2834
+ return await runEmbed(command);
2835
+ }
2836
+
2837
+ async function sourcePatch(args) {
2838
+ const workspace = asString(args.workspace_id || args.workspace);
2839
+ const patchPath = asString(args.patch_path || args.patch);
2840
+ if (!workspace) throw new Error("workspace_id is required");
2841
+ if (!patchPath) throw new Error("patch_path is required");
2842
+ const command = ["build", "workspace", "source", "patch", workspace, "--patch", patchPath];
2843
+ pushOptional(command, "account", args.account_id || args.account);
2844
+ return await runEmbed(command);
2845
+ }
2846
+
2847
+ async function applicationCompile(args) {
2848
+ const workspace = asString(args.workspace_id || args.workspace);
2849
+ if (!workspace) throw new Error("workspace_id is required");
2850
+ const command = ["build", "application", "compile", "--workspace", workspace];
2851
+ pushOptional(command, "account", args.account_id || args.account);
2852
+ pushOptional(command, "source", args.source_path || args.source);
2853
+ pushOptional(command, "output-name", args.output_name);
2854
+ pushOptional(command, "execution-mode", args.execution_mode);
2855
+ pushOptional(command, "compiler", args.compiler);
2856
+ return await runEmbed(command);
2857
+ }
2858
+
2859
+ async function taskStatus(args) {
2860
+ const task = asString(args.task_id || args.task);
2861
+ if (!task) throw new Error("task_id is required");
2862
+ return await runEmbed(["cloud", "task", "status", task]);
2863
+ }
2864
+
2865
+ function safeArtifactOutputFileName(name) {
2866
+ return asString(name).replace(/[^A-Za-z0-9._-]+/g, "_") || "task.log";
2867
+ }
2868
+
2869
+ async function taskLogDownload(args) {
2870
+ const task = asString(args.task_id || args.task);
2871
+ if (!task) throw new Error("task_id is required");
2872
+ const status = await taskStatus({ task_id: task });
2873
+ const data = responseData(status);
2874
+ const artifacts = Array.isArray(data?.artifacts) ? data.artifacts : [];
2875
+ const log = artifacts.find((item) => item.kind === "log" || asString(item.name).endsWith(".log"));
2876
+ if (!log?.artifact_id) throw new Error(`Task ${task} does not expose a log artifact`);
2877
+ const output = asString(args.output_path || args.output) || path.join(".embed-labs", "artifacts", `${task}-${safeArtifactOutputFileName(log.name)}`);
2878
+ const downloaded = await runEmbed(["artifact", "download", log.artifact_id, "--output", output]);
2879
+ return { status, selected_artifact: log, downloaded };
2880
+ }
2881
+
2882
+ async function imageGenerate(args) {
2883
+ const workspace = asString(args.workspace_id || args.workspace);
2884
+ const prompt = asString(args.prompt || args.request);
2885
+ if (!workspace) throw new Error("workspace_id is required");
2886
+ if (!prompt) throw new Error("prompt is required");
2887
+ const command = ["build", "image", "generate", "--workspace", workspace, "--prompt", prompt];
2888
+ pushOptional(command, "account", args.account_id || args.account);
2889
+ pushOptional(command, "provider", args.provider);
2890
+ pushOptional(command, "model", args.model);
2891
+ pushOptional(command, "image-profile", args.image_profile);
2892
+ pushOptional(command, "execution-mode", args.execution_mode);
2893
+ pushOptional(command, "worker-pool", args.worker_pool);
2894
+ const generated = await runEmbed(command);
2895
+ if (!asString(args.download_output)) return generated;
2896
+ const artifact = pickArtifact(generated, "image");
2897
+ if (!artifact?.artifact_id) throw new Error("No downloadable image artifact found");
2898
+ const downloaded = await runEmbed(["artifact", "download", artifact.artifact_id, "--output", args.download_output]);
2899
+ return { generated, selected_artifact: artifact, downloaded };
2900
+ }
2901
+
2902
+ async function artifactDownload(args) {
2903
+ const artifact = asString(args.artifact_id || args.artifact);
2904
+ const output = asString(args.output_path || args.output);
2905
+ if (!artifact) throw new Error("artifact_id is required");
2906
+ if (!output) throw new Error("output_path is required");
2907
+ return await runEmbed(["artifact", "download", artifact, "--output", output]);
2908
+ }
2909
+
2910
+ async function workspaceRelease(args) {
2911
+ const workspace = asString(args.workspace_id || args.workspace);
2912
+ if (!workspace) throw new Error("workspace_id is required");
2913
+ const command = ["build", "workspace", "release", workspace];
2914
+ if (boolValue(args.dry_run)) command.push("--dry-run");
2915
+ if (boolValue(args.no_settle_storage)) command.push("--no-settle-storage");
2916
+ return await runEmbed(command);
2917
+ }
2918
+
2919
+ async function callTool(name, args = {}) {
2920
+ if (name === "dbt_auth_status") return await pluginAuthStatus();
2921
+ if (name === "dbt_plugin_update_check") return await pluginUpdateCheck(args);
2922
+ if (name === "dbt_plugin_update") return await pluginUpdate(args);
2923
+ if (name === "dbt_supported_boards") return await supportedBoards(args);
2924
+ if (name === "dbt_local_toolchain_list") return await localToolchainList(args);
2925
+ if (name === "dbt_local_toolchain_installed") return await localToolchainInstalled(args);
2926
+ if (name === "dbt_local_toolchain_latest") return await localToolchainLatest(args);
2927
+ if (name === "dbt_local_toolchain_current") return await localToolchainCurrent(args);
2928
+ const authStatus = await pluginAuthStatus();
2929
+ if (!authStatus.ok) return authStatus;
2930
+ if (name === "dbt_current_board_status" || name === "dbtstatus") return await currentBoardStatus(args, authStatus);
2931
+ if (name === "dbt_initial_image_flash") return await initialImageFlash(args);
2932
+ if (name === "dbt_rp2350_monitor_status") return await rp2350MonitorStatus(args);
2933
+ if (name === "dbt_rp2350_capabilities") return await rp2350Capabilities(args);
2934
+ if (name === "dbt_rp2350_operation") return await rp2350Operation(args);
2935
+ if (name === "dbt_rp2350_gpio_read") return await rp2350GpioRead(args);
2936
+ if (name === "dbt_rp2350_gpio_write") return await rp2350GpioWrite(args);
2937
+ if (name === "dbt_rp2350_uart_write") return await rp2350UartWrite(args);
2938
+ if (name === "dbt_rp2350_i2c_transfer") return await rp2350I2cTransfer(args);
2939
+ if (name === "dbt_rp2350_logic_capture") return await rp2350LogicCapture(args);
2940
+ if (name === "dbt_rp2350_logic_decode") return await rp2350LogicDecode(args);
2941
+ if (name === "dbt_rp2350_spi_transfer") return await rp2350SpiTransfer(args);
2942
+ if (name === "dbt_rp2350_wifi_manage") return await rp2350WifiManage(args);
2943
+ if (name === "dbt_rp2350_probe_debug") return await rp2350ProbeDebug(args);
2944
+ if (name === "dbt_rp2350_monitor_command") return await rp2350MonitorCommand(args);
2945
+ if (name === "dbt_cloud_status") return await cloudStatus(args);
2946
+ if (name === "dbt_board_knowledge_search") return await boardKnowledgeSearch(args);
2947
+ if (isServerWorkspaceToolName(name)) return serverWorkspaceDisabledResult();
2948
+ if (name === "dbt_task_status") return await taskStatus(args);
2949
+ if (name === "dbt_task_log_download") return await taskLogDownload(args);
2950
+ if (name === "dbt_artifact_download") return await artifactDownload(args);
2951
+ if (name === "dbt_board_debug") return await boardDebug(args);
2952
+ if (name === "dbt_boot_logo_update") return await bootLogoUpdate(args);
2953
+ if (name === "dbt_update_logo") return await updateLogo(args);
2954
+ if (name === "dbt_compose_boot_logo") return await composeBootLogo(args);
2955
+ if (name === "dbt_local_toolchain_install") return await localToolchainInstall(args);
2956
+ if (name === "dbt_local_toolchain_uninstall") return await localToolchainUninstall(args);
2957
+ if (name === "dbt_local_toolchain_validate") return await localToolchainValidate(args);
2958
+ if (name === "dbt_taishanpi_initial_image_flash") return await taishanpiInitialImageFlash(args);
2959
+ if (name === "dbt_rp2350_monitor_firmware_flash") return await rp2350MonitorFirmwareFlash(args);
2960
+ if (name === "dbt_rp2350_initial_firmware_flash") return await rp2350InitialFirmwareFlash(args);
2961
+ if (name === "dbt_plugin_update_check") return await pluginUpdateCheck(args);
2962
+ if (name === "dbt_plugin_update") return await pluginUpdate(args);
2963
+ if (name === "dbt_local_compile") return await localCompile(args);
2964
+ if (name === "dbt_local_qt_smoke") return await localQtSmoke(args);
2965
+ if (name === "dbt_taishanpi_qt_app_workflow") return await taishanpiQtAppWorkflow(args);
2966
+ if (name === "dbttool") {
2967
+ const action = asString(args.action).toLowerCase().replaceAll("_", "-");
2968
+ let payload = {};
2969
+ if (asString(args.arguments_json)) payload = JSON.parse(args.arguments_json);
2970
+ if (asString(args.request) && !payload.prompt) payload.prompt = asString(args.request);
2971
+ if (["supported-boards", "boards", "board-list", "registry-list"].includes(action)) return await supportedBoards(payload);
2972
+ if (["status", "cloud-status", "methods", "knowledge", "capabilities"].includes(action)) return await cloudStatus(payload);
2973
+ if (["board-status", "status"].includes(action)) return await currentBoardStatus(payload, authStatus);
2974
+ if (["rp2350-capabilities", "rpmon-capabilities", "rp2350-catalog", "rpmon-catalog"].includes(action)) return await rp2350Capabilities(payload);
2975
+ if (["rp2350-operation", "rpmon-operation"].includes(action)) return await rp2350Operation(payload);
2976
+ if (["rp2350-monitor-status", "rp2350-status", "rpmon-status"].includes(action)) return await rp2350MonitorStatus(payload);
2977
+ if (["rp2350-gpio-read", "rpmon-gpio-read", "gpio-read"].includes(action)) return await rp2350GpioRead(payload);
2978
+ if (["rp2350-gpio-write", "rpmon-gpio-write", "gpio-write"].includes(action)) return await rp2350GpioWrite(payload);
2979
+ if (["rp2350-uart-write", "rpmon-uart-write", "uart-write"].includes(action)) return await rp2350UartWrite(payload);
2980
+ if (["rp2350-i2c-transfer", "rpmon-i2c-transfer", "i2c-transfer"].includes(action)) return await rp2350I2cTransfer(payload);
2981
+ if (["rp2350-logic-capture", "rpmon-logic-capture", "logic-capture"].includes(action)) return await rp2350LogicCapture(payload);
2982
+ if (["rp2350-logic-decode", "rpmon-logic-decode", "logic-decode"].includes(action)) return await rp2350LogicDecode(payload);
2983
+ if (["rp2350-spi-transfer", "rpmon-spi-transfer", "spi-transfer", "spi-test"].includes(action)) return await rp2350SpiTransfer(payload);
2984
+ if (["rp2350-wifi", "rpmon-wifi", "wifi-manage"].includes(action)) return await rp2350WifiManage(payload);
2985
+ if (["rp2350-probe-debug", "rpmon-probe-debug", "probe-debug"].includes(action)) return await rp2350ProbeDebug(payload);
2986
+ if (["rp2350-monitor-command", "rpmon-command"].includes(action)) return await rp2350MonitorCommand(payload);
2987
+ if (["initial-image-flash", "flash-initial-image", "initial-flash", "flash-initial", "init-flash"].includes(action)) return await initialImageFlash(payload);
2988
+ if (["debug", "agent-run", "deploy"].includes(action)) return await boardDebug(payload);
2989
+ if (["boot-logo-update", "update-boot-logo", "update-logo", "boot-logo", "logo", "boot-logo-modify"].includes(action)) return await bootLogoUpdate(payload);
2990
+ if (["compose-boot-logo", "boot-logo-compose", "compose-logo"].includes(action)) return await composeBootLogo(payload);
2991
+ if (["local-toolchain-list", "toolchain-list", "list-toolchains", "board-environments", "local-environments"].includes(action)) return await localToolchainList(payload);
2992
+ if (["local-toolchain-installed", "toolchain-installed", "installed-toolchains", "installed-board-environments"].includes(action)) return await localToolchainInstalled(payload);
2993
+ if (["local-toolchain-latest", "toolchain-latest", "latest-toolchain"].includes(action)) return await localToolchainLatest(payload);
2994
+ if (["local-toolchain-current", "toolchain-current", "current-toolchain"].includes(action)) return await localToolchainCurrent(payload);
2995
+ if (["local-toolchain-install", "toolchain-install", "install-toolchain"].includes(action)) return await localToolchainInstall(payload);
2996
+ if (["local-toolchain-uninstall", "toolchain-uninstall", "uninstall-toolchain", "remove-toolchain"].includes(action)) return await localToolchainUninstall(payload);
2997
+ if (["local-toolchain", "local-toolchain-validate", "toolchain-validate"].includes(action)) return await localToolchainValidate(payload);
2998
+ if (["taishanpi-initial-image-flash", "taishanpi-initial-flash", "taishanpi-flash-initial", "rk3566-initial-flash"].includes(action)) return await taishanpiInitialImageFlash(payload);
2999
+ if (["rp2350-monitor-firmware-flash", "rp2350-firmware-flash", "pico-monitor-flash"].includes(action)) return await rp2350MonitorFirmwareFlash(payload);
3000
+ if (["rp2350-initial-firmware-flash", "rp2350-initial-flash", "pico-initial-flash", "pico-initial-firmware-flash"].includes(action)) return await rp2350InitialFirmwareFlash(payload);
3001
+ if (["plugin-update-check", "check-plugin-update", "update-check"].includes(action)) return await pluginUpdateCheck(payload);
3002
+ if (["plugin-update", "update-plugin", "upgrade-plugin"].includes(action)) return await pluginUpdate(payload);
3003
+ if (["local-compile", "compile-local"].includes(action)) return await localCompile(payload);
3004
+ if (["local-qt-smoke", "qt-smoke", "local-qt-build"].includes(action)) return await localQtSmoke(payload);
3005
+ if (["taishanpi-qt-app", "qt-app", "qt-workflow", "taishanpi-qt-workflow", "qt-develop"].includes(action)) return await taishanpiQtAppWorkflow(payload);
3006
+ if (isServerWorkspaceAction(action)) return serverWorkspaceDisabledResult();
3007
+ if (["task-status", "status-task"].includes(action)) return await taskStatus(payload);
3008
+ if (["task-log", "task-log-download", "log-download"].includes(action)) return await taskLogDownload(payload);
3009
+ if (["download", "artifact-download"].includes(action)) return await artifactDownload(payload);
3010
+ return { ok: false, error_code: "unsupported_dbt_action" };
3011
+ }
3012
+ throw new Error(`Unknown tool: ${name}`);
3013
+ }
3014
+
3015
+ let responseFraming = "jsonl";
3016
+
3017
+ function send(message) {
3018
+ const json = JSON.stringify(message);
3019
+ if (responseFraming === "content-length") {
3020
+ const body = Buffer.from(json, "utf8");
3021
+ process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
3022
+ process.stdout.write(body);
3023
+ return;
3024
+ }
3025
+ process.stdout.write(`${json}\n`);
3026
+ }
3027
+
3028
+ async function handleRpc(message) {
3029
+ if (!message || typeof message !== "object") return;
3030
+ if (!("id" in message)) return;
3031
+ try {
3032
+ if (message.method === "initialize") {
3033
+ send({
3034
+ jsonrpc: "2.0",
3035
+ id: message.id,
3036
+ result: {
3037
+ protocolVersion: "2024-11-05",
3038
+ capabilities: { tools: {} },
3039
+ serverInfo: mcpServerInfo()
3040
+ }
3041
+ });
3042
+ return;
3043
+ }
3044
+ if (message.method === "tools/list") {
3045
+ send({ jsonrpc: "2.0", id: message.id, result: { tools } });
3046
+ return;
3047
+ }
3048
+ if (message.method === "tools/call") {
3049
+ const result = await callTool(message.params?.name, message.params?.arguments || {});
3050
+ send({
3051
+ jsonrpc: "2.0",
3052
+ id: message.id,
3053
+ result: {
3054
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3055
+ isError: false
3056
+ }
3057
+ });
3058
+ return;
3059
+ }
3060
+ if (message.method === "ping") {
3061
+ send({ jsonrpc: "2.0", id: message.id, result: {} });
3062
+ return;
3063
+ }
3064
+ send({
3065
+ jsonrpc: "2.0",
3066
+ id: message.id,
3067
+ error: { code: -32601, message: `Unsupported method: ${message.method}` }
3068
+ });
3069
+ } catch (error) {
3070
+ send({
3071
+ jsonrpc: "2.0",
3072
+ id: message.id,
3073
+ error: { code: -32000, message: error?.message || String(error) }
3074
+ });
3075
+ }
3076
+ }
3077
+
3078
+ function startMcp() {
3079
+ let buffer = Buffer.alloc(0);
3080
+ process.stdin.on("data", (chunk) => {
3081
+ buffer = Buffer.concat([buffer, chunk]);
3082
+ while (buffer.length > 0) {
3083
+ const asText = buffer.toString("utf8");
3084
+ if (/^Content-Length:/i.test(asText)) {
3085
+ const marker = buffer.indexOf("\r\n\r\n");
3086
+ if (marker < 0) return;
3087
+ const header = buffer.slice(0, marker).toString("utf8");
3088
+ const match = /Content-Length:\s*(\d+)/i.exec(header);
3089
+ if (!match) {
3090
+ buffer = buffer.slice(marker + 4);
3091
+ continue;
3092
+ }
3093
+ const length = Number.parseInt(match[1], 10);
3094
+ const start = marker + 4;
3095
+ if (buffer.length < start + length) return;
3096
+ const body = buffer.slice(start, start + length).toString("utf8");
3097
+ buffer = buffer.slice(start + length);
3098
+ responseFraming = "content-length";
3099
+ try {
3100
+ void handleRpc(JSON.parse(body));
3101
+ } catch {
3102
+ // Ignore malformed frames; a valid client will retry with a request id.
3103
+ }
3104
+ continue;
3105
+ }
3106
+
3107
+ const newline = buffer.indexOf("\n");
3108
+ if (newline < 0) return;
3109
+ const line = buffer.slice(0, newline).toString("utf8").trim();
3110
+ buffer = buffer.slice(newline + 1);
3111
+ if (!line) continue;
3112
+ responseFraming = "jsonl";
3113
+ try {
3114
+ void handleRpc(JSON.parse(line));
3115
+ } catch {
3116
+ // Ignore malformed frames; a valid client will retry with a request id.
3117
+ }
3118
+ }
3119
+ });
3120
+ }
3121
+
3122
+ async function main() {
3123
+ const [mode, name, json = "{}"] = process.argv.slice(2);
3124
+ if (mode === "--list-tools") {
3125
+ process.stdout.write(`${JSON.stringify(tools.map((item) => item.name), null, 2)}\n`);
3126
+ return;
3127
+ }
3128
+ if (mode === "--call-tool") {
3129
+ const result = await callTool(name, JSON.parse(json || "{}"));
3130
+ process.stdout.write(`${JSON.stringify({ ok: true, result }, null, 2)}\n`);
3131
+ return;
3132
+ }
3133
+ startMcp();
3134
+ }
3135
+
3136
+ main().catch((error) => {
3137
+ process.stderr.write(`${error?.message || String(error)}\n`);
3138
+ process.exitCode = 1;
3139
+ });