crosspad-mcp-server 8.1.2 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +14 -0
- package/.mcp.json +9 -0
- package/README.md +95 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +369 -49
- package/dist/index.js.map +1 -1
- package/dist/tools/idf-flash.js +2 -2
- package/dist/tools/idf-flash.js.map +1 -1
- package/dist/tools/idf-monitor.d.ts +3 -1
- package/dist/tools/idf-monitor.js +19 -3
- package/dist/tools/idf-monitor.js.map +1 -1
- package/dist/tools/midi.js +20 -16
- package/dist/tools/midi.js.map +1 -1
- package/dist/tools/symbols.d.ts +3 -1
- package/dist/tools/symbols.js +31 -1
- package/dist/tools/symbols.js.map +1 -1
- package/dist/tools/trace-buffer.d.ts +40 -0
- package/dist/tools/trace-buffer.js +74 -0
- package/dist/tools/trace-buffer.js.map +1 -0
- package/dist/tools/trace-device.d.ts +10 -0
- package/dist/tools/trace-device.js +26 -0
- package/dist/tools/trace-device.js.map +1 -0
- package/dist/tools/trace-doctor.d.ts +43 -0
- package/dist/tools/trace-doctor.js +150 -0
- package/dist/tools/trace-doctor.js.map +1 -0
- package/dist/tools/trace-export.d.ts +4 -0
- package/dist/tools/trace-export.js +14 -0
- package/dist/tools/trace-export.js.map +1 -0
- package/dist/tools/trace-session.d.ts +118 -0
- package/dist/tools/trace-session.js +346 -0
- package/dist/tools/trace-session.js.map +1 -0
- package/dist/tools/trace-symbols.d.ts +24 -0
- package/dist/tools/trace-symbols.js +44 -0
- package/dist/tools/trace-symbols.js.map +1 -0
- package/dist/tools/trace-webui.d.ts +53 -0
- package/dist/tools/trace-webui.js +222 -0
- package/dist/tools/trace-webui.js.map +1 -0
- package/dist/utils/device.d.ts +5 -0
- package/dist/utils/device.js +43 -15
- package/dist/utils/device.js.map +1 -1
- package/dist/utils/exec.js +26 -0
- package/dist/utils/exec.js.map +1 -1
- package/dist/utils/userConfig.d.ts +13 -0
- package/dist/utils/userConfig.js +43 -0
- package/dist/utils/userConfig.js.map +1 -0
- package/package.json +12 -4
- package/skills/crosspad/SKILL.md +58 -0
- package/skills/crosspad/reference/faq.md +40 -0
- package/skills/crosspad/reference/install.md +84 -0
- package/skills/crosspad/reference/repos.md +29 -0
- package/skills/crosspad/reference/role-contributor.md +64 -0
- package/skills/crosspad/reference/role-fw-dev.md +44 -0
- package/skills/crosspad/reference/role-user.md +49 -0
- package/skills/crosspad/reference/tools.md +68 -0
- package/skills/crosspad/scripts/doctor.sh +65 -0
- package/skills/crosspad/scripts/setup.sh +53 -0
- package/skills/swd-tracer/SKILL.md +135 -0
- package/skills/swd-tracer/reference/signals.md +42 -0
- package/skills/swd-tracer/scripts/detect-env.sh +61 -0
- package/skills/swd-tracer/scripts/install-udev-rules.sh +25 -0
- package/skills/swd-tracer/scripts/setup-venv.sh +26 -0
- package/tracer/PROTOCOL.md +260 -0
- package/tracer/README.md +327 -0
- package/tracer/swd_tracer.py +1066 -0
- package/tracer/ui/index.html +834 -0
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const require = createRequire(import.meta.url);
|
|
|
7
7
|
const { version } = require("../package.json");
|
|
8
8
|
import { crosspadBuild, crosspadRun, crosspadKill } from "./tools/build.js";
|
|
9
9
|
import { crosspadBuildCheck } from "./tools/build-check.js";
|
|
10
|
+
import { BIN_EXE as _BIN_EXE } from "./config.js";
|
|
10
11
|
import { crosspadLog } from "./tools/log.js";
|
|
11
12
|
import { crosspadIdfBuild } from "./tools/idf-build.js";
|
|
12
13
|
import { crosspadIdfFlash, crosspadIdfOta } from "./tools/idf-flash.js";
|
|
@@ -24,6 +25,13 @@ import { crosspadStats } from "./tools/stats.js";
|
|
|
24
25
|
import { crosspadSettingsGet, crosspadSettingsSet } from "./tools/settings.js";
|
|
25
26
|
import { crosspadMidiSend } from "./tools/midi.js";
|
|
26
27
|
import { crosspadAppList, crosspadAppInstall, crosspadAppRemove, crosspadAppUpdate, crosspadAppSync, } from "./tools/app-manager.js";
|
|
28
|
+
import { runDoctor, realProbe } from "./tools/trace-doctor.js";
|
|
29
|
+
import { setConfigValue } from "./utils/userConfig.js";
|
|
30
|
+
import { listSymbols } from "./tools/trace-symbols.js";
|
|
31
|
+
import { getDeviceState } from "./tools/trace-device.js";
|
|
32
|
+
import { TraceSession, getActiveSession, setActiveSession } from "./tools/trace-session.js";
|
|
33
|
+
import { getDashboard, openInBrowser, buildUiUrl } from "./tools/trace-webui.js";
|
|
34
|
+
import { writeCsv } from "./tools/trace-export.js";
|
|
27
35
|
// Server instructions — MCP clients prepend these to the LLM system prompt.
|
|
28
36
|
// This is the *primary* mechanism by which a Claude session "knows" to pick
|
|
29
37
|
// crosspad_* tools when working inside any CrossPad repo. CLAUDE.md and memory
|
|
@@ -32,6 +40,8 @@ import { crosspadAppList, crosspadAppInstall, crosspadAppRemove, crosspadAppUpda
|
|
|
32
40
|
const SERVER_INSTRUCTIONS = `
|
|
33
41
|
You have access to the CrossPad MCP server, which exposes purpose-built tools for the CrossPad embedded music controller monorepo (repos: crosspad-pc, platform-idf, ESP32-S3, crosspad-core, crosspad-gui, plus app submodules).
|
|
34
42
|
|
|
43
|
+
NEW TO A CROSSPAD REPO OR SETTING UP? Use the \`crosspad\` skill first — it maps the ecosystem (repos, MCP tools, roles), walks install/config, and routes to per-role guides + an FAQ. Run \`bash scripts/doctor.sh\` from that skill to check your environment.
|
|
44
|
+
|
|
35
45
|
WHEN TO USE THESE TOOLS — in any conversation that touches a CrossPad repo, prefer the crosspad_* tools over raw shell equivalents:
|
|
36
46
|
|
|
37
47
|
- Inspecting code → crosspad_search_symbols (NOT \`grep -r\`); crosspad_list_interfaces; crosspad_interface_implementations.
|
|
@@ -42,6 +52,7 @@ WHEN TO USE THESE TOOLS — in any conversation that touches a CrossPad repo, pr
|
|
|
42
52
|
- Sim interaction → crosspad_screenshot, crosspad_input, crosspad_midi, crosspad_stats, crosspad_settings_get/set.
|
|
43
53
|
- Apps (registry) → crosspad_apps_list / install / remove / update / sync (NOT manual submodule git ops).
|
|
44
54
|
- Commits → crosspad_commit (NOT raw \`git commit\`) — handles multi-repo paths and refuses on merge conflicts.
|
|
55
|
+
- SWD tracing → crosspad_trace (STM32 firmware variable RT trace over ST-Link). Run action=doctor first; resolve issues; then action=symbols → start → read.
|
|
45
56
|
|
|
46
57
|
WHY: these tools resolve repos dynamically from env vars, parse build output into structured errors[], stream progress, and refuse unsafe operations. Manual shell equivalents will work but lose this scaffolding and frequently break across the 5 repos.
|
|
47
58
|
|
|
@@ -226,13 +237,34 @@ const O_Devices = {
|
|
|
226
237
|
devices: z.array(z.object({
|
|
227
238
|
port: z.string(),
|
|
228
239
|
description: z.string().optional(),
|
|
229
|
-
vid: z.
|
|
230
|
-
pid: z.
|
|
240
|
+
vid: z.number().int().optional(),
|
|
241
|
+
pid: z.number().int().optional(),
|
|
231
242
|
is_crosspad: z.boolean(),
|
|
243
|
+
kind: z.enum(["esp-native", "stm-bridge"]).nullable().optional(),
|
|
232
244
|
}).passthrough()),
|
|
233
245
|
crosspad_count: z.number().int().optional(),
|
|
234
246
|
...ErrorField,
|
|
235
247
|
};
|
|
248
|
+
const O_Trace = {
|
|
249
|
+
success: z.boolean(),
|
|
250
|
+
action: z.string().optional(),
|
|
251
|
+
ok: z.boolean().optional(),
|
|
252
|
+
issues: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
253
|
+
symbols: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
254
|
+
device_state: z.string().optional(),
|
|
255
|
+
actual_fs: z.number().optional(),
|
|
256
|
+
sample_count: z.number().int().optional(),
|
|
257
|
+
signals: z.array(z.string()).optional(),
|
|
258
|
+
series: z.record(z.string(), z.unknown()).optional(),
|
|
259
|
+
stats: z.record(z.string(), z.unknown()).optional(),
|
|
260
|
+
file_path: z.string().optional(),
|
|
261
|
+
ui_url: z.string().optional(),
|
|
262
|
+
key: z.string().optional(),
|
|
263
|
+
// §11.6: last few daemon stderr lines, surfaced when device_state is an
|
|
264
|
+
// error / probe_lost / exited so the caller sees *why* without a re-run.
|
|
265
|
+
stderr_tail: z.string().optional(),
|
|
266
|
+
...ErrorField,
|
|
267
|
+
};
|
|
236
268
|
const O_Test = {
|
|
237
269
|
success: z.boolean(),
|
|
238
270
|
tests_found: z.boolean(),
|
|
@@ -355,15 +387,23 @@ const O_AppAction = {
|
|
|
355
387
|
const BuildPlatform = z.enum(["pc", "idf"]).describe("Target platform: 'pc' = host simulator, 'idf' = ESP32-S3 firmware.");
|
|
356
388
|
const PlatformPcOnly = z.enum(["pc"]).default("pc").describe("Platform — currently only 'pc' is supported here.");
|
|
357
389
|
server.registerTool("crosspad_build", {
|
|
358
|
-
description: "Build CrossPad for the given platform
|
|
390
|
+
description: "Build CrossPad for the given platform.\n" +
|
|
391
|
+
" • platform='pc' → CMake + Ninja host simulator. PREFER THIS over `cmake --build build` (picks right MSVC env on Windows, parses errors/warnings, streams progress).\n" +
|
|
392
|
+
" • platform='idf' → idf.py build for ESP32-S3 firmware. PREFER THIS over raw `idf.py build` (sources IDF env, auto-fullcleans when new apps detected, parses errors/warnings).\n" +
|
|
393
|
+
"Mode×platform compatibility:\n" +
|
|
394
|
+
" • incremental → both (default)\n" +
|
|
395
|
+
" • clean → both (wipes build dir, then builds)\n" +
|
|
396
|
+
" • reconfigure → PC only (re-runs cmake without wiping cache)\n" +
|
|
397
|
+
" • fullclean → IDF only (runs idf.py fullclean, then builds)",
|
|
359
398
|
inputSchema: {
|
|
360
399
|
platform: BuildPlatform,
|
|
361
400
|
mode: z.enum(["incremental", "clean", "fullclean", "reconfigure"])
|
|
362
401
|
.default("incremental")
|
|
363
|
-
.describe("
|
|
402
|
+
.describe("Build mode. Compatibility: incremental & clean = both platforms; reconfigure = PC only; fullclean = IDF only. " +
|
|
403
|
+
"Pick incremental for normal iteration; clean if you suspect stale artifacts; fullclean (IDF) after adding new apps; reconfigure (PC) after editing CMakeLists."),
|
|
364
404
|
build_type: z.enum(["Debug", "Release", "RelWithDebInfo"])
|
|
365
405
|
.default("Debug")
|
|
366
|
-
.describe("CMake build type — PC
|
|
406
|
+
.describe("CMake build type — PC ONLY (ignored for IDF; ESP32 build type comes from sdkconfig). Only honored on mode=clean|reconfigure (incremental keeps existing cache)."),
|
|
367
407
|
},
|
|
368
408
|
outputSchema: O_Build,
|
|
369
409
|
annotations: ANN_DESTRUCTIVE,
|
|
@@ -418,12 +458,12 @@ server.registerTool("crosspad_check", {
|
|
|
418
458
|
},
|
|
419
459
|
outputSchema: O_BuildCheck,
|
|
420
460
|
annotations: ANN_READ_ONLY,
|
|
421
|
-
}, async () => jsonResponse(crosspadBuildCheck()));
|
|
461
|
+
}, async () => jsonResponse({ success: true, exe_path: _BIN_EXE, ...crosspadBuildCheck() }));
|
|
422
462
|
// ═══════════════════════════════════════════════════════════════════════
|
|
423
463
|
// FLASH — unified UART/OTA into one tool with `transport` axis
|
|
424
464
|
// ═══════════════════════════════════════════════════════════════════════
|
|
425
465
|
server.registerTool("crosspad_flash", {
|
|
426
|
-
description: "Flash firmware to a connected CrossPad device. transport='uart' uses idf.py flash (device must be in bootloader mode). transport='ota' uses platform-idf/tools/ota_flash.py over USB CDC (no bootloader mode required). Requires prior crosspad_build platform=idf.",
|
|
466
|
+
description: "Flash ESP firmware to a connected CrossPad device. transport='uart' uses idf.py flash (device must be in bootloader mode). transport='ota' uses platform-idf/tools/ota_flash.py over USB CDC (no bootloader mode required). Requires prior crosspad_build platform=idf. Works on both CrossPad generations: rev <2.0 (ESP native USB) and rev 2.0 (port is the STM32 CDC bridge — STM emulates the esptool DTR/RTS auto-reset and forwards the flash to the ESP over LPUART2; rev-2.0 STM must be in passthrough mode, i.e. NOT booted with pad-4 held).",
|
|
427
467
|
inputSchema: {
|
|
428
468
|
transport: z.enum(["uart", "ota"]).describe("'uart' = bootloader-mode flash via idf.py; 'ota' = USB-CDC OTA flash via ota_flash.py."),
|
|
429
469
|
port: Port.optional(),
|
|
@@ -444,18 +484,24 @@ server.registerTool("crosspad_flash", {
|
|
|
444
484
|
return jsonResponse(await crosspadIdfOta(port, firmware_path, onLine, extra.signal));
|
|
445
485
|
});
|
|
446
486
|
server.registerTool("crosspad_log", {
|
|
447
|
-
description: "Capture logs
|
|
487
|
+
description: "Capture logs (consolidated; replaces crosspad_log_pc and crosspad_log_idf in v6).\n" +
|
|
488
|
+
" • target='pc' → spawn the built sim binary, capture stdout/stderr, then kill it. " +
|
|
489
|
+
"Fields used: timeout_seconds (default 5), max_lines (default 200). `port` and `filter` MUST be omitted.\n" +
|
|
490
|
+
" • target='idf' → read serial from a connected ESP32-S3 via pyserial (no TTY needed). " +
|
|
491
|
+
"Fields used: port (auto-detected if omitted), timeout_seconds (default 10), max_lines (default 500), filter (substring, case-insensitive).",
|
|
448
492
|
inputSchema: {
|
|
449
|
-
target: z.enum(["pc", "idf"]).describe("'pc' = run
|
|
450
|
-
port: Port.optional().describe("Serial port
|
|
451
|
-
timeout_seconds: TimeoutSec.optional().describe("Capture duration. Defaults:
|
|
452
|
-
max_lines: MaxLines.optional().describe("Max output lines. Defaults: 200
|
|
493
|
+
target: z.enum(["pc", "idf"]).describe("'pc' = run+capture sim binary; 'idf' = read serial from connected device. Other fields are conditional — see description."),
|
|
494
|
+
port: Port.optional().describe("idf only. Serial port path. Auto-detected if omitted; required when multiple devices connected. MUST be omitted for target=pc."),
|
|
495
|
+
timeout_seconds: TimeoutSec.optional().describe("Capture duration in seconds. Defaults: 5 (pc), 10 (idf)."),
|
|
496
|
+
max_lines: MaxLines.optional().describe("Max output lines. Defaults: 200 (pc), 500 (idf)."),
|
|
453
497
|
filter: z.string().optional()
|
|
454
|
-
.describe("Case-insensitive substring filter
|
|
498
|
+
.describe("idf only. Case-insensitive substring filter — only matching lines returned. MUST be omitted for target=pc."),
|
|
499
|
+
reset_to_boot: z.boolean().optional()
|
|
500
|
+
.describe("idf only. Pulse the device reset (esptool DTR/RTS sequence, works through the STM bridge) before capturing, so the log starts at boot t=0. Use for boot-time profiling. Default false (passive read of the running device)."),
|
|
455
501
|
},
|
|
456
502
|
outputSchema: O_Log,
|
|
457
503
|
annotations: ANN_READ_ONLY,
|
|
458
|
-
}, async ({ target, port, timeout_seconds, max_lines, filter }, extra) => {
|
|
504
|
+
}, async ({ target, port, timeout_seconds, max_lines, filter, reset_to_boot }, extra) => {
|
|
459
505
|
if (target === "pc") {
|
|
460
506
|
if (port)
|
|
461
507
|
return err("Field 'port' is not used when target='pc'.");
|
|
@@ -469,25 +515,232 @@ server.registerTool("crosspad_log", {
|
|
|
469
515
|
// target === "idf"
|
|
470
516
|
const onLine = makeProgressLogger("log-idf", extra);
|
|
471
517
|
return jsonResponse({
|
|
472
|
-
...(await crosspadIdfMonitor(port, timeout_seconds ?? 10, max_lines ?? 500, filter, onLine, extra.signal)),
|
|
518
|
+
...(await crosspadIdfMonitor(port, timeout_seconds ?? 10, max_lines ?? 500, filter, onLine, extra.signal, reset_to_boot ?? false)),
|
|
473
519
|
});
|
|
474
520
|
});
|
|
475
521
|
server.registerTool("crosspad_devices", {
|
|
476
|
-
description: "List all connected USB serial devices. Identifies CrossPad devices (
|
|
522
|
+
description: "List all connected USB serial devices. Identifies CrossPad devices separately and tags each with `kind`: 'esp-native' (rev <2.0, ESP32-S3 native USB, VID 0x303a/PID 0x3456) or 'stm-bridge' (rev 2.0, STM32 composite CDC+MIDI bridge, VID 0x0483/PID 0x5740 — STM programs the ESP over LPUART2).",
|
|
477
523
|
inputSchema: {},
|
|
478
524
|
outputSchema: O_Devices,
|
|
479
525
|
annotations: ANN_READ_ONLY,
|
|
480
526
|
}, async () => jsonResponse(listDevices()));
|
|
481
527
|
// ═══════════════════════════════════════════════════════════════════════
|
|
528
|
+
// SWD TRACER
|
|
529
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
530
|
+
// §12.4 injectable browser opener — defaults to the real platform opener but is
|
|
531
|
+
// overridable (setTraceBrowserOpener) so a unit test can assert auto-open is
|
|
532
|
+
// called/skipped without launching a real browser. Returns true if it spawned.
|
|
533
|
+
let traceBrowserOpener = openInBrowser;
|
|
534
|
+
export function setTraceBrowserOpener(fn) { traceBrowserOpener = fn; }
|
|
535
|
+
const TraceAction = z.enum([
|
|
536
|
+
"doctor", "config_set", "symbols", "start", "stop",
|
|
537
|
+
"add", "remove", "status", "read", "save", "device_state", "ui",
|
|
538
|
+
]);
|
|
539
|
+
server.registerTool("crosspad_trace", {
|
|
540
|
+
description: "Real-time SWD tracer for the STM32G0B1 firmware (ST-Link). Non-halting RAM polling of firmware variables resolved from the Debug ELF (like ST-Studio/CubeMonitor). Pick an `action`:\n" +
|
|
541
|
+
" • doctor → environment precheck → issues[] (run this FIRST; resolve issues, then config_set).\n" +
|
|
542
|
+
" • config_set → persist a resolved path/serial to ~/.config/crosspad-mcp/config.json (key,value).\n" +
|
|
543
|
+
" • symbols → list/search traceable variables from the ELF (query optional).\n" +
|
|
544
|
+
" • start → begin a background trace (signals[], rate_hz).\n" +
|
|
545
|
+
" • stop → end the active trace.\n" +
|
|
546
|
+
" • add/remove → mutate the live poll set of the active trace (signals[]); returns the current signal set.\n" +
|
|
547
|
+
" • status → device_state (running/stop_suspected/exited), sample_count, actual_fs, signals.\n" +
|
|
548
|
+
" • read → recent samples downsampled + per-signal stats (cheap; safe for the LLM).\n" +
|
|
549
|
+
" • save → export the in-memory buffer to CSV (returns file_path).\n" +
|
|
550
|
+
" • device_state → deep low-power/STOP register dump.\n" +
|
|
551
|
+
" • ui → returns the localhost dashboard URL.\n" +
|
|
552
|
+
"Signal names accept array indexing, e.g. 's_inputs[0]', 's_adc_raw[3]'.",
|
|
553
|
+
inputSchema: {
|
|
554
|
+
action: TraceAction,
|
|
555
|
+
signals: z.array(z.string()).optional().describe("start: variable names from `symbols` (e.g. ['s_vbat_mv','s_inputs[0]'])."),
|
|
556
|
+
rate_hz: z.number().int().min(0).max(2000).optional().describe("start: target sample rate (0 = as fast as the probe allows). Actual Fs is reported."),
|
|
557
|
+
swo: z.array(z.string()).optional().describe("start (EXPERIMENTAL): map ITM stimulus ports to signal names, e.g. ['0:phase','1:isr_us']. Requires firmware that emits ITM on the SWO pin (NOT present in current CrossPad firmware — UNTESTED against real ITM). Omit for plain RAM polling. Fails soft: if SWV init fails, polling continues normally."),
|
|
558
|
+
query: z.string().optional().describe("symbols: case-insensitive substring filter."),
|
|
559
|
+
key: z.string().optional().describe("config_set: one of stm_elf_path|pyocd_python|probe_serial|trace_dir."),
|
|
560
|
+
value: z.string().optional().describe("config_set: the value to persist."),
|
|
561
|
+
window_from: z.number().optional().describe("read: start time (s) of the window."),
|
|
562
|
+
window_to: z.number().optional().describe("read: end time (s) of the window."),
|
|
563
|
+
max_points: z.number().int().min(1).max(5000).optional().describe("read: max points per signal (default 200)."),
|
|
564
|
+
format: z.enum(["csv"]).optional().describe("save: export format (csv)."),
|
|
565
|
+
},
|
|
566
|
+
outputSchema: O_Trace,
|
|
567
|
+
annotations: ANN_SIDE_EFFECT,
|
|
568
|
+
}, async ({ action, signals, rate_hz, swo, query, key, value, window_from, window_to, max_points, format }, extra) => {
|
|
569
|
+
switch (action) {
|
|
570
|
+
case "doctor": {
|
|
571
|
+
const r = await runDoctor(realProbe());
|
|
572
|
+
return ok({ action, ok: r.ok, issues: r.issues, device_state: r.probe ? "connected" : "no_probe" });
|
|
573
|
+
}
|
|
574
|
+
case "config_set": {
|
|
575
|
+
const allowed = ["stm_elf_path", "pyocd_python", "probe_serial", "trace_dir"];
|
|
576
|
+
if (!key || !allowed.includes(key))
|
|
577
|
+
return err(`config_set requires key in ${allowed.join("|")}`);
|
|
578
|
+
if (value === undefined)
|
|
579
|
+
return err("config_set requires `value`.");
|
|
580
|
+
setConfigValue(key, value);
|
|
581
|
+
return ok({ action, key, file_path: "~/.config/crosspad-mcp/config.json" });
|
|
582
|
+
}
|
|
583
|
+
case "symbols": {
|
|
584
|
+
const r = await listSymbols(query, undefined, extra.signal);
|
|
585
|
+
if (!r.success)
|
|
586
|
+
return err(r.error ?? "symbol resolution failed", { action });
|
|
587
|
+
return ok({ action, symbols: r.symbols });
|
|
588
|
+
}
|
|
589
|
+
case "start": {
|
|
590
|
+
if (!signals || signals.length === 0)
|
|
591
|
+
return err("start requires non-empty signals[].");
|
|
592
|
+
if (getActiveSession()?.isRunning())
|
|
593
|
+
return err("A trace is already running — stop it first.");
|
|
594
|
+
const doc = await runDoctor(realProbe());
|
|
595
|
+
if (!doc.ok) {
|
|
596
|
+
// §11.7: a vanished probe gets a distinct, actionable refusal.
|
|
597
|
+
if (doc.issues.some((i) => i.id === "no_probe_detected")) {
|
|
598
|
+
return err("No ST-Link detected on USB — replug the probe and retry (verify with `pyocd list` / `lsusb`).", { action, issues: doc.issues, device_state: "no_probe" });
|
|
599
|
+
}
|
|
600
|
+
return err("Doctor reported blocking issues — resolve them first.", { action, issues: doc.issues });
|
|
601
|
+
}
|
|
602
|
+
const sess = new TraceSession({ signals, rateHz: rate_hz ?? 0, swo });
|
|
603
|
+
sess.start();
|
|
604
|
+
setActiveSession(sess);
|
|
605
|
+
// §12.1/§12.4: ensure the PERSISTENT dashboard server is up (idempotent —
|
|
606
|
+
// reuses it across traces), auto-open the browser ONLY if no client is
|
|
607
|
+
// already connected (covers an external browser AND a VS Code Simple
|
|
608
|
+
// Browser tab opened on a previous trace), then bind this session so its
|
|
609
|
+
// frames broadcast to the UI (also emits trace_start).
|
|
610
|
+
const dashboard = getDashboard();
|
|
611
|
+
let uiUrl;
|
|
612
|
+
try {
|
|
613
|
+
uiUrl = await dashboard.ensureStarted();
|
|
614
|
+
if (!dashboard.hasClients())
|
|
615
|
+
traceBrowserOpener(uiUrl);
|
|
616
|
+
dashboard.bind(sess);
|
|
617
|
+
}
|
|
618
|
+
catch { /* UI optional — never block a trace on the dashboard */ }
|
|
619
|
+
// §11.6: don't lie. Wait for the first real frame and report what the
|
|
620
|
+
// daemon actually did (connect can fail fast → error/exit) instead of an
|
|
621
|
+
// optimistic "running" that masks a dead connect.
|
|
622
|
+
const first = await sess.waitForFirstFrame(3000);
|
|
623
|
+
if (first?.type === "error") {
|
|
624
|
+
// Connect failed — the daemon already exited. Clear the active session
|
|
625
|
+
// and surface the daemon's error + stderr tail. Unbind the dashboard
|
|
626
|
+
// (server stays up) so the next trace starts from a clean idle state.
|
|
627
|
+
sess.stop();
|
|
628
|
+
dashboard.unbind();
|
|
629
|
+
setActiveSession(null);
|
|
630
|
+
return err(`Trace connect failed: ${first.error}`, { action, device_state: "error: " + first.error, stderr_tail: sess.stderrTail(5) || undefined });
|
|
631
|
+
}
|
|
632
|
+
if (first && (first.type === "signals" || first.type === "sample")) {
|
|
633
|
+
return ok({ action, device_state: "running", signals, file_path: sess.filePath ?? undefined, ui_url: uiUrl });
|
|
634
|
+
}
|
|
635
|
+
// No frame within the window. If the proc already died, it's a failure;
|
|
636
|
+
// otherwise it's still connecting (honest) — caller can poll status.
|
|
637
|
+
if (!sess.isRunning()) {
|
|
638
|
+
dashboard.unbind();
|
|
639
|
+
setActiveSession(null);
|
|
640
|
+
return err(`Trace daemon exited before producing data (${sess.deviceState}).`, { action, device_state: sess.deviceState, stderr_tail: sess.stderrTail(5) || undefined });
|
|
641
|
+
}
|
|
642
|
+
return ok({ action, device_state: "connecting", signals, file_path: sess.filePath ?? undefined, ui_url: uiUrl });
|
|
643
|
+
}
|
|
644
|
+
case "stop": {
|
|
645
|
+
const s = getActiveSession();
|
|
646
|
+
if (!s)
|
|
647
|
+
return err("No active trace.");
|
|
648
|
+
const count = s.buffer.count();
|
|
649
|
+
// §11.5/§12.1: initiate teardown, but defer unbinding the dashboard and
|
|
650
|
+
// clearing the active session until the daemon has REALLY exited (the
|
|
651
|
+
// stop→SIGTERM→SIGKILL escalation can take ~4.5s). Clearing early would
|
|
652
|
+
// let a racing `start` spawn a second daemon onto the still-busy probe
|
|
653
|
+
// and make `status` report idle mid-teardown. onStopped fires
|
|
654
|
+
// synchronously if the process is already gone.
|
|
655
|
+
const d = getDashboard();
|
|
656
|
+
s.onStopped(() => {
|
|
657
|
+
d.unbind();
|
|
658
|
+
if (getActiveSession() === s)
|
|
659
|
+
setActiveSession(null);
|
|
660
|
+
});
|
|
661
|
+
s.stop();
|
|
662
|
+
return ok({ action, sample_count: count, file_path: s.filePath ?? undefined });
|
|
663
|
+
}
|
|
664
|
+
case "status": {
|
|
665
|
+
const s = getActiveSession();
|
|
666
|
+
if (!s)
|
|
667
|
+
return ok({ action, device_state: "idle", sample_count: 0 });
|
|
668
|
+
const n = s.buffer.count();
|
|
669
|
+
const elapsed = (performance.now() - s.startedAt) / 1000;
|
|
670
|
+
// §11.6: when the daemon is unhealthy, fold in the last stderr lines so
|
|
671
|
+
// the caller sees *why* without re-running.
|
|
672
|
+
const ds = s.deviceState;
|
|
673
|
+
const unhealthy = ds.startsWith("error") || ds === "probe_lost" || ds === "exited" || ds.startsWith("spawn_failed");
|
|
674
|
+
const tail = unhealthy ? (s.stderrTail(5) || undefined) : undefined;
|
|
675
|
+
return ok({ action, device_state: ds, sample_count: n, actual_fs: elapsed > 0 ? n / elapsed : 0, signals: s.buffer.signalNames(), stderr_tail: tail });
|
|
676
|
+
}
|
|
677
|
+
case "read": {
|
|
678
|
+
const s = getActiveSession();
|
|
679
|
+
if (!s)
|
|
680
|
+
return err("No active trace.");
|
|
681
|
+
const mp = max_points ?? 200;
|
|
682
|
+
const win = (window_from !== undefined || window_to !== undefined) ? { fromT: window_from, toT: window_to } : undefined;
|
|
683
|
+
const series = {};
|
|
684
|
+
const stats = {};
|
|
685
|
+
for (const sig of s.buffer.signalNames()) {
|
|
686
|
+
series[sig] = s.buffer.downsample(sig, mp, win);
|
|
687
|
+
stats[sig] = s.buffer.stats(sig);
|
|
688
|
+
}
|
|
689
|
+
return ok({ action, series, stats, device_state: s.deviceState, sample_count: s.buffer.count() });
|
|
690
|
+
}
|
|
691
|
+
case "save": {
|
|
692
|
+
const s = getActiveSession();
|
|
693
|
+
if (!s)
|
|
694
|
+
return err("No active trace.");
|
|
695
|
+
// `format` is constrained to "csv" by the input schema — no runtime branch needed.
|
|
696
|
+
const csvPath = (s.filePath ?? "/tmp/trace").replace(/\.cptrace$/, "") + ".csv";
|
|
697
|
+
writeCsv(csvPath, s.buffer, s.buffer.signalNames());
|
|
698
|
+
return ok({ action, file_path: csvPath });
|
|
699
|
+
}
|
|
700
|
+
case "device_state": {
|
|
701
|
+
const r = await getDeviceState(extra.signal);
|
|
702
|
+
if (!r.success)
|
|
703
|
+
return err(r.error ?? "device_state read failed", { action });
|
|
704
|
+
// Pack regs/decoded into `stats` (a schema field) so they survive
|
|
705
|
+
// outputSchema validation — O_Trace has no top-level regs/decoded keys.
|
|
706
|
+
return ok({
|
|
707
|
+
action,
|
|
708
|
+
device_state: r.accessible ? "accessible" : "inaccessible",
|
|
709
|
+
stats: { regs: r.regs, decoded: r.decoded, accessible: r.accessible },
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
case "ui": {
|
|
713
|
+
// §12.1: the dashboard is persistent and independent of any trace — just
|
|
714
|
+
// ensure the server is up and hand back the url. Works even when idle
|
|
715
|
+
// (no active trace): the UI shows a "waiting for trace…" state and
|
|
716
|
+
// /symbols falls back to the default ELF for autocomplete.
|
|
717
|
+
const url = await getDashboard().ensureStarted();
|
|
718
|
+
return ok({ action, ui_url: url });
|
|
719
|
+
}
|
|
720
|
+
case "add":
|
|
721
|
+
case "remove": {
|
|
722
|
+
const s = getActiveSession();
|
|
723
|
+
if (!s || !s.isRunning())
|
|
724
|
+
return err("No active trace — start one first.", { action });
|
|
725
|
+
if (!signals || signals.length === 0)
|
|
726
|
+
return err(`${action} requires non-empty signals[].`, { action });
|
|
727
|
+
// §4/§6: await the post-reconcile set (so the response reflects array
|
|
728
|
+
// expansion and dropped `unresolved` specs, not the pre-reconcile guess).
|
|
729
|
+
const reconciled = action === "add" ? await s.addSignals(signals) : await s.removeSignals(signals);
|
|
730
|
+
return ok({ action, signals: reconciled });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
482
735
|
// TEST
|
|
483
736
|
// ═══════════════════════════════════════════════════════════════════════
|
|
484
737
|
server.registerTool("crosspad_test_run", {
|
|
485
738
|
description: "Build and run the Catch2 test suite for crosspad-pc. PREFER THIS over invoking the test binary directly — configures cmake with BUILD_TESTING=ON, parses Catch2 output into passed/failed counts and errors, supports filter and list_only.",
|
|
486
739
|
inputSchema: {
|
|
487
740
|
filter: z.string().default("")
|
|
488
|
-
.describe("Catch2 test filter (e.g. '[core]', 'PadManager*').
|
|
741
|
+
.describe("Catch2 test filter (e.g. '[core]', 'PadManager*'). Default '' (empty) runs ALL tests — there is no opt-out for 'no tests'."),
|
|
489
742
|
list_only: z.boolean().default(false)
|
|
490
|
-
.describe("
|
|
743
|
+
.describe("If true, list discovered tests matching `filter` without running them. Default false."),
|
|
491
744
|
},
|
|
492
745
|
outputSchema: O_Test,
|
|
493
746
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
@@ -499,12 +752,14 @@ server.registerTool("crosspad_test_run", {
|
|
|
499
752
|
// SIM — screenshot
|
|
500
753
|
// ═══════════════════════════════════════════════════════════════════════
|
|
501
754
|
server.registerTool("crosspad_screenshot", {
|
|
502
|
-
description: "Capture a PNG screenshot from the running PC simulator.
|
|
755
|
+
description: "Capture a PNG screenshot from the running PC simulator. " +
|
|
756
|
+
"Default behavior (return_inline=false): saves to <crosspad-pc>/screenshots/ and returns metadata + file_path (cheap, no token cost). " +
|
|
757
|
+
"Set return_inline=true ONLY when the LLM needs to actually see the image — that returns base64 inline and burns ~50-150k tokens.",
|
|
503
758
|
inputSchema: {
|
|
504
759
|
filename: z.string().optional()
|
|
505
|
-
.describe("Custom filename (saved under <crosspad-pc>/screenshots/). Default: screenshot_<timestamp>.png"),
|
|
760
|
+
.describe("Custom filename (saved under <crosspad-pc>/screenshots/). Default: screenshot_<timestamp>.png. Ignored when return_inline=true."),
|
|
506
761
|
return_inline: z.boolean().default(false)
|
|
507
|
-
.describe("
|
|
762
|
+
.describe("false (default) = save to disk, return file_path (token-cheap). true = return base64 image content for the LLM to view (token-expensive — only when the image must be analyzed)."),
|
|
508
763
|
},
|
|
509
764
|
outputSchema: O_Screenshot,
|
|
510
765
|
annotations: ANN_SIDE_EFFECT,
|
|
@@ -541,19 +796,29 @@ server.registerTool("crosspad_screenshot", {
|
|
|
541
796
|
// SIM — input events
|
|
542
797
|
// ═══════════════════════════════════════════════════════════════════════
|
|
543
798
|
server.registerTool("crosspad_input", {
|
|
544
|
-
description: "Send
|
|
799
|
+
description: "Send one input event to the running PC simulator (consolidated; replaces 7 v5 tools). " +
|
|
800
|
+
"Pick an `action`, then supply ONLY the fields it needs — extras are ignored. " +
|
|
801
|
+
"Required fields per action:\n" +
|
|
802
|
+
" • pad_press → pad (velocity optional, default 127)\n" +
|
|
803
|
+
" • pad_release → pad\n" +
|
|
804
|
+
" • encoder_rotate → delta (positive=CW, negative=CCW)\n" +
|
|
805
|
+
" • encoder_press → (none)\n" +
|
|
806
|
+
" • encoder_release → (none)\n" +
|
|
807
|
+
" • click → x, y\n" +
|
|
808
|
+
" • key → keycode (SDL keycode int)\n" +
|
|
809
|
+
"Requires the simulator to be running (crosspad_run first).",
|
|
545
810
|
inputSchema: {
|
|
546
811
|
action: z.enum([
|
|
547
812
|
"pad_press", "pad_release",
|
|
548
813
|
"encoder_rotate", "encoder_press", "encoder_release",
|
|
549
814
|
"click", "key",
|
|
550
|
-
]).describe("Which input event to dispatch."),
|
|
551
|
-
pad: PadIndex.optional().describe("
|
|
552
|
-
velocity: Velocity.optional().describe("
|
|
553
|
-
delta: z.number().int().optional().describe("
|
|
554
|
-
x: z.number().int().min(0).optional().describe("X pixel coordinate (
|
|
555
|
-
y: z.number().int().min(0).optional().describe("Y pixel coordinate (
|
|
556
|
-
keycode: z.number().int().optional().describe("SDL keycode (
|
|
815
|
+
]).describe("Which input event to dispatch — see description for required fields per action."),
|
|
816
|
+
pad: PadIndex.optional().describe("Required for action=pad_press|pad_release. Pad index 0-15."),
|
|
817
|
+
velocity: Velocity.optional().describe("Optional for action=pad_press (default 127). Ignored for other actions."),
|
|
818
|
+
delta: z.number().int().optional().describe("Required for action=encoder_rotate. Positive=CW, negative=CCW. Typical range -10..10."),
|
|
819
|
+
x: z.number().int().min(0).optional().describe("Required for action=click. X pixel coordinate (0 = left)."),
|
|
820
|
+
y: z.number().int().min(0).optional().describe("Required for action=click. Y pixel coordinate (0 = top)."),
|
|
821
|
+
keycode: z.number().int().optional().describe("Required for action=key. SDL keycode (e.g. 27=ESC, 32=SPACE, 13=RETURN)."),
|
|
557
822
|
},
|
|
558
823
|
outputSchema: O_Input,
|
|
559
824
|
annotations: ANN_SIDE_EFFECT,
|
|
@@ -598,16 +863,23 @@ server.registerTool("crosspad_input", {
|
|
|
598
863
|
// SIM — MIDI
|
|
599
864
|
// ═══════════════════════════════════════════════════════════════════════
|
|
600
865
|
server.registerTool("crosspad_midi", {
|
|
601
|
-
description: "Send
|
|
866
|
+
description: "Send one MIDI event to the running PC simulator (consolidated; replaces 4 v5 tools). " +
|
|
867
|
+
"Pick a `type`, then supply ONLY the fields it needs — extras are ignored. " +
|
|
868
|
+
"Required fields per type:\n" +
|
|
869
|
+
" • note_on → note (velocity optional, default 127)\n" +
|
|
870
|
+
" • note_off → note (velocity optional, default 0)\n" +
|
|
871
|
+
" • cc → cc_num, value ⚠️ NOT YET SUPPORTED BY PC SIM (no midi_cc handler — call fails fast)\n" +
|
|
872
|
+
" • program_change → program ⚠️ NOT YET SUPPORTED BY PC SIM (no midi_program_change handler — call fails fast)\n" +
|
|
873
|
+
"`channel` (0-15) defaults to 0 for every type. Only note_on/note_off actually reach the sim today.",
|
|
602
874
|
inputSchema: {
|
|
603
875
|
type: z.enum(["note_on", "note_off", "cc", "program_change"])
|
|
604
|
-
.describe("MIDI event type."),
|
|
876
|
+
.describe("MIDI event type — see description for required fields per type."),
|
|
605
877
|
channel: Channel,
|
|
606
|
-
note: Note.optional().describe("MIDI note
|
|
607
|
-
velocity: Velocity.optional().describe("
|
|
608
|
-
cc_num: Cc.optional().describe("
|
|
609
|
-
value: Cc7.optional().describe("Controller value
|
|
610
|
-
program: Program.optional().describe("Program number
|
|
878
|
+
note: Note.optional().describe("Required for type=note_on|note_off. MIDI note 0-127 (60 = middle C)."),
|
|
879
|
+
velocity: Velocity.optional().describe("Optional for type=note_on (default 127) and note_off (default 0). Ignored for cc/program_change."),
|
|
880
|
+
cc_num: Cc.optional().describe("Required for type=cc. MIDI controller number 0-127."),
|
|
881
|
+
value: Cc7.optional().describe("Required for type=cc. Controller value 0-127."),
|
|
882
|
+
program: Program.optional().describe("Required for type=program_change. Program number 0-127."),
|
|
611
883
|
},
|
|
612
884
|
outputSchema: O_Midi,
|
|
613
885
|
annotations: ANN_SIDE_EFFECT,
|
|
@@ -663,9 +935,9 @@ server.registerTool("crosspad_settings_set", {
|
|
|
663
935
|
description: "Write a single setting on the running simulator.",
|
|
664
936
|
inputSchema: {
|
|
665
937
|
key: z.string().min(1)
|
|
666
|
-
.describe("Setting key (
|
|
938
|
+
.describe("Setting key. Either a flat name ('lcd_brightness') or dotted category.field ('keypad.eco_mode', 'vibration.enable'). Use crosspad_settings_get to discover valid keys."),
|
|
667
939
|
value: z.number()
|
|
668
|
-
.describe("Numeric value. Booleans
|
|
940
|
+
.describe("Numeric value. Booleans must be encoded as 0=false, 1=true (no native bool support over the wire)."),
|
|
669
941
|
},
|
|
670
942
|
outputSchema: O_SettingsSet,
|
|
671
943
|
annotations: ANN_DESTRUCTIVE,
|
|
@@ -707,9 +979,9 @@ server.registerTool("crosspad_commit", {
|
|
|
707
979
|
description: "Commit staged changes in a specific CrossPad repo. PREFER THIS over raw `git commit` — handles repo aliases (idf/pc/arduino/core/gui), refuses on merge conflicts, uses 0600 tempfiles for messages (no shell-quoting issues with quotes/newlines/backticks), and never pushes. Stages files[] first if supplied.",
|
|
708
980
|
inputSchema: {
|
|
709
981
|
repo: RepoAlias,
|
|
710
|
-
message: z.string().min(1).describe("Commit message"),
|
|
982
|
+
message: z.string().min(1).describe("Commit message. Newlines/quotes/backticks are safe — passed via 0600 tempfile, not shell-quoted."),
|
|
711
983
|
files: z.array(z.string()).optional()
|
|
712
|
-
.describe("
|
|
984
|
+
.describe("If supplied: stage exactly these files (repo-relative paths) then commit. If omitted: commit whatever is currently staged in the repo (no auto-stage)."),
|
|
713
985
|
},
|
|
714
986
|
outputSchema: O_Commit,
|
|
715
987
|
annotations: ANN_DESTRUCTIVE,
|
|
@@ -718,19 +990,21 @@ server.registerTool("crosspad_commit", {
|
|
|
718
990
|
// CODE — search and analysis
|
|
719
991
|
// ═══════════════════════════════════════════════════════════════════════
|
|
720
992
|
server.registerTool("crosspad_search_symbols", {
|
|
721
|
-
description: "Search for symbol DEFINITIONS (classes, functions, macros, enums, typedefs) across CrossPad repos via git grep. PREFER THIS over raw `grep -r` or `git grep` — it filters to definitions only (skips call sites/declarations), classifies kind, and aggregates across all
|
|
993
|
+
description: "Search for symbol DEFINITIONS (classes, functions, macros, enums, typedefs) across CrossPad repos via git grep. PREFER THIS over raw `grep -r` or `git grep` — it filters to definitions only (skips call sites/declarations), classifies kind, and aggregates across all repos automatically. Substring match: 'Foo' matches FooBar, MyFoo. Vendored/generated trees (lvgl, managed_components, thorvg, TFT_eSPI, STM Drivers/Middlewares/CMSIS, build, …) are skipped by default — pass include_vendored=true to scan them.",
|
|
722
994
|
inputSchema: {
|
|
723
995
|
query: z.string().min(1).describe("Symbol name (substring match, case-insensitive on filter)"),
|
|
724
996
|
kind: z.enum(["class", "function", "macro", "enum", "typedef", "all"]).default("all"),
|
|
725
997
|
repos: z.array(z.string()).default(["all"])
|
|
726
|
-
.describe("Repo names to scan, or ['all']. Names: crosspad-core, crosspad-gui, crosspad-pc, platform-idf, ESP32-S3."),
|
|
998
|
+
.describe("Repo names to scan, or ['all']. Names: crosspad-core, crosspad-gui, crosspad-pc, platform-idf, ESP32-S3, stm32-r20."),
|
|
727
999
|
max_results: z.number().int().min(1).max(500).default(50),
|
|
728
1000
|
context_lines: z.number().int().min(0).max(10).default(0)
|
|
729
1001
|
.describe("Surrounding lines per match (like grep -C). 0 = no context."),
|
|
1002
|
+
include_vendored: z.boolean().default(false)
|
|
1003
|
+
.describe("Scan vendored/generated trees too (lvgl, managed_components, STM Drivers/Middlewares, build, …). Default false — these are almost always noise."),
|
|
730
1004
|
},
|
|
731
1005
|
outputSchema: O_SearchSymbols,
|
|
732
1006
|
annotations: ANN_READ_ONLY,
|
|
733
|
-
}, async ({ query, kind, repos, max_results, context_lines }) => jsonResponse({ success: true, ...crosspadSearchSymbols(query, kind, repos, max_results, context_lines) }));
|
|
1007
|
+
}, async ({ query, kind, repos, max_results, context_lines, include_vendored }) => jsonResponse({ success: true, ...crosspadSearchSymbols(query, kind, repos, max_results, context_lines, include_vendored) }));
|
|
734
1008
|
server.registerTool("crosspad_list_interfaces", {
|
|
735
1009
|
description: "List all crosspad-core interfaces (I*-prefixed classes in crosspad-core/include/crosspad/).",
|
|
736
1010
|
inputSchema: {},
|
|
@@ -738,9 +1012,11 @@ server.registerTool("crosspad_list_interfaces", {
|
|
|
738
1012
|
annotations: ANN_READ_ONLY,
|
|
739
1013
|
}, async () => jsonResponse({ success: true, ...crosspadInterfaces("list") }));
|
|
740
1014
|
server.registerTool("crosspad_interface_implementations", {
|
|
741
|
-
description: "Find all classes implementing a given interface across CrossPad repos. Returns className, file path, platform.",
|
|
1015
|
+
description: "Find all classes implementing a given interface across CrossPad repos. Returns className, file path, platform. Use crosspad_list_interfaces first if you don't know exact names.",
|
|
742
1016
|
inputSchema: {
|
|
743
|
-
interface_name: z.string().min(1)
|
|
1017
|
+
interface_name: z.string().min(1)
|
|
1018
|
+
.regex(/^I[A-Z][A-Za-z0-9_]*$/, "Interface name must start with 'I' followed by an uppercase letter (e.g. 'IDisplay').")
|
|
1019
|
+
.describe("Interface name — MUST start with 'I' and use the exact crosspad-core casing (e.g. 'IDisplay', 'IPadLogicHandler', 'IKeyValueStore'). Not 'Display', not 'iDisplay'."),
|
|
744
1020
|
},
|
|
745
1021
|
outputSchema: O_Architecture,
|
|
746
1022
|
annotations: ANN_READ_ONLY,
|
|
@@ -798,15 +1074,23 @@ server.registerTool("crosspad_apps_remove", {
|
|
|
798
1074
|
return jsonResponse((await crosspadAppRemove(app_name, platform, onLine, extra.signal)));
|
|
799
1075
|
});
|
|
800
1076
|
server.registerTool("crosspad_apps_update", {
|
|
801
|
-
description: "Update one or all installed apps on a platform.
|
|
1077
|
+
description: "Update one or all installed apps on a platform. EXACTLY ONE of these must be supplied: " +
|
|
1078
|
+
"set `app_name` to update a single app, OR set `update_all=true` to update every installed app on the platform. " +
|
|
1079
|
+
"Supplying both, or neither, is an error.",
|
|
802
1080
|
inputSchema: {
|
|
803
1081
|
platform: Platform,
|
|
804
|
-
app_name: AppName.optional().describe("App ID to update.
|
|
805
|
-
update_all: z.boolean().default(false),
|
|
1082
|
+
app_name: AppName.optional().describe("App ID (e.g. 'metronome') to update one app. Mutually exclusive with update_all=true."),
|
|
1083
|
+
update_all: z.boolean().default(false).describe("If true, update all installed apps on `platform`. Mutually exclusive with app_name."),
|
|
806
1084
|
},
|
|
807
1085
|
outputSchema: O_AppAction,
|
|
808
1086
|
annotations: ANN_DESTRUCTIVE_OPEN,
|
|
809
1087
|
}, async ({ platform, app_name, update_all }, extra) => {
|
|
1088
|
+
if (!app_name && !update_all) {
|
|
1089
|
+
return err("Specify `app_name` to update a single app, OR set `update_all=true` to update every installed app.");
|
|
1090
|
+
}
|
|
1091
|
+
if (app_name && update_all) {
|
|
1092
|
+
return err("`app_name` and `update_all=true` are mutually exclusive — pick one.");
|
|
1093
|
+
}
|
|
810
1094
|
const onLine = makeProgressLogger("apps-update", extra);
|
|
811
1095
|
return jsonResponse((await crosspadAppUpdate(platform, app_name, update_all, onLine, extra.signal)));
|
|
812
1096
|
});
|
|
@@ -868,6 +1152,42 @@ server.resource("crosspad-workspace", "crosspad://workspace", {
|
|
|
868
1152
|
};
|
|
869
1153
|
});
|
|
870
1154
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1155
|
+
// crosspad://trace — live SWD trace session status. Cheap snapshot the LLM
|
|
1156
|
+
// can read without a tool call to learn whether a trace is running, the
|
|
1157
|
+
// device state, achieved Fs, signals, and the dashboard URL.
|
|
1158
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1159
|
+
server.resource("crosspad-trace", "crosspad://trace", {
|
|
1160
|
+
description: "Live SWD trace session status: active flag, device_state, sample_count, achieved Fs, traced signals, and the web UI URL. Returns {active:false} when idle.",
|
|
1161
|
+
mimeType: "application/json",
|
|
1162
|
+
}, async () => {
|
|
1163
|
+
const s = getActiveSession();
|
|
1164
|
+
const payload = s
|
|
1165
|
+
? {
|
|
1166
|
+
active: true,
|
|
1167
|
+
device_state: s.deviceState,
|
|
1168
|
+
sample_count: s.buffer.count(),
|
|
1169
|
+
actual_fs: (() => {
|
|
1170
|
+
const elapsed = (performance.now() - s.startedAt) / 1000;
|
|
1171
|
+
return elapsed > 0 ? s.buffer.count() / elapsed : 0;
|
|
1172
|
+
})(),
|
|
1173
|
+
signals: s.buffer.signalNames(),
|
|
1174
|
+
file_path: s.filePath ?? null,
|
|
1175
|
+
// §12.1: the dashboard URL is owned by the persistent singleton; it's a
|
|
1176
|
+
// stable loopback URL once any trace/ui action has started the server.
|
|
1177
|
+
ui_url: getDashboard().port ? buildUiUrl(getDashboard().port) : null,
|
|
1178
|
+
}
|
|
1179
|
+
: { active: false };
|
|
1180
|
+
return {
|
|
1181
|
+
contents: [
|
|
1182
|
+
{
|
|
1183
|
+
uri: "crosspad://trace",
|
|
1184
|
+
mimeType: "application/json",
|
|
1185
|
+
text: JSON.stringify(payload, null, 2),
|
|
1186
|
+
},
|
|
1187
|
+
],
|
|
1188
|
+
};
|
|
1189
|
+
});
|
|
1190
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
871
1191
|
// RESOURCES — apps registry & installed manifest per platform
|
|
872
1192
|
// One static resource per file-per-detected-platform. LLM/clients can
|
|
873
1193
|
// inspect raw JSON without spending a tool call. Resource set updates only
|
|
@@ -929,7 +1249,7 @@ import path from "path";
|
|
|
929
1249
|
// clients must construct concrete URIs.
|
|
930
1250
|
// ═══════════════════════════════════════════════════════════════════════
|
|
931
1251
|
server.registerResource("crosspad-symbol", new ResourceTemplate("crosspad://symbols/{repo}/{symbol}", { list: undefined }), {
|
|
932
|
-
description: "Resolve a single symbol by repo+name. URI: crosspad://symbols/<repo>/<symbol>. <repo> is one of: crosspad-core, crosspad-gui, crosspad-pc, platform-idf, ESP32-S3, or 'all'. Returns JSON with matching definition(s) (class/function/macro/enum/typedef). For substring/wildcard search, use the crosspad_search_symbols tool.",
|
|
1252
|
+
description: "Resolve a single symbol by repo+name. URI: crosspad://symbols/<repo>/<symbol>. <repo> is one of: crosspad-core, crosspad-gui, crosspad-pc, platform-idf, ESP32-S3, stm32-r20, or 'all'. Returns JSON with matching definition(s) (class/function/macro/enum/typedef). For substring/wildcard search, use the crosspad_search_symbols tool.",
|
|
933
1253
|
mimeType: "application/json",
|
|
934
1254
|
}, async (uri, variables) => {
|
|
935
1255
|
const repo = decodeURIComponent(String(Array.isArray(variables.repo) ? variables.repo[0] : variables.repo ?? "")).trim();
|