@kvell007/embed-labs-cli 0.1.0-alpha.73 → 0.1.0-alpha.74

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