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

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