From 204e0ebcc2302cd90dc8a415c33f7e9949e8e36d Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:15:33 -0500 Subject: [PATCH 01/10] feat: add introspect module with commands endpoint Add a new `introspect` command module that returns the JSON Schema describing all incoming message types. The `introspect.commands` command works before `initialize`, enabling API discovery pre-negotiation. - Create `src/lib/introspect/` module (command, incoming_message, outgoing_message, message_handler) following existing patterns - Register in Instance enum and wire into server.ts - Add `ts-json-schema-generator` for build-time schema generation - Rename interfaces for `$ref` reuse (node, driver, controller, multicast_group incoming messages) - Add introspection section to README with tool recommendations Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + README.md | 27 ++ package-lock.json | 274 ++++++++++++++++++++ package.json | 5 +- src/lib/command.ts | 2 + src/lib/controller/incoming_message.ts | 4 +- src/lib/driver/incoming_message.ts | 112 ++++---- src/lib/incoming_message.ts | 2 + src/lib/instance.ts | 1 + src/lib/introspect/command.ts | 3 + src/lib/introspect/incoming_message.ts | 11 + src/lib/introspect/message_handler.ts | 21 ++ src/lib/introspect/outgoing_message.ts | 5 + src/lib/multicast_group/incoming_message.ts | 4 +- src/lib/node/incoming_message.ts | 68 ++--- src/lib/outgoing_message.ts | 2 + src/lib/server.ts | 2 + 17 files changed, 449 insertions(+), 95 deletions(-) create mode 100644 src/lib/introspect/command.ts create mode 100644 src/lib/introspect/incoming_message.ts create mode 100644 src/lib/introspect/message_handler.ts create mode 100644 src/lib/introspect/outgoing_message.ts diff --git a/.gitignore b/.gitignore index d38be344d..2ef4fe209 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ config.json *.tgz cache .claude +/src/lib/generated/ diff --git a/README.md b/README.md index d07754d62..1f3c83b47 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,33 @@ interface { } ``` +### Introspection + +The server supports introspection commands that allow clients to discover the API at runtime. These commands work **before `initialize`**, so clients can explore the API before negotiating a schema version. + +#### Get command schema + +Returns the full [JSON Schema](https://json-schema.org/) describing all incoming message types. The schema uses `$ref` pointers and a shared `definitions` dictionary for compact representation. + +```ts +interface { + messageId: string; + command: "introspect.commands"; +} +``` + +Returns a JSON Schema (Draft 7) object describing all valid incoming messages. + +**Exploring the schema** + +The returned schema can be explored with any JSON Schema-compatible tool: + +- **[jq](https://jqlang.org/)** — Query the schema from the command line (e.g. `jq '.definitions.IncomingMessageNode'`) +- **[JSON Schema Viewer](https://json-schema.app/)** — Paste the schema into the online viewer for interactive exploration +- **[JSON Crack](https://jsoncrack.com/)** — Visualize the schema as an interactive graph +- **VS Code** — Extensions like [JSON Schema Viewer](https://marketplace.visualstudio.com/items?itemName=jock.svg) provide in-editor navigation +- **[jsonschema](https://python-jsonschema.readthedocs.io/) (Python)** / **[ajv](https://ajv.js.org/) (JS)** — Validate messages programmatically against the schema + ### Driver level commands #### Get the config of the driver diff --git a/package-lock.json b/package-lock.json index 9da8a51c7..5a101535e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "lint-staged": "^16.2.6", "prettier": "^3.6.2", "semver": "^7.5.4", + "ts-json-schema-generator": "^2.4.0", "tsx": "^4.19.2", "typescript": "^5.3.3", "typescript-eslint": "^8.46.1", @@ -891,6 +892,16 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -2869,6 +2880,36 @@ "dev": true, "license": "MIT" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2919,6 +2960,31 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3109,6 +3175,22 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3488,6 +3570,16 @@ "node": ">= 12.0.0" } }, + "node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/mdns-server": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/mdns-server/-/mdns-server-1.0.11.tgz", @@ -3545,6 +3637,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -3586,6 +3688,16 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nrf-intel-hex": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/nrf-intel-hex/-/nrf-intel-hex-1.4.0.tgz", @@ -3717,6 +3829,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3765,6 +3884,23 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4314,6 +4450,39 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-json-schema-generator": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-2.4.0.tgz", + "integrity": "sha512-HbmNsgs58CfdJq0gpteRTxPXG26zumezOs+SB9tgky6MpqiFgQwieCn2MW70+sxpHouZ/w9LW0V6L4ZQO4y1Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "commander": "^13.1.0", + "glob": "^11.0.1", + "json5": "^2.2.3", + "normalize-path": "^3.0.0", + "safe-stable-stringify": "^2.5.0", + "tslib": "^2.8.1", + "typescript": "^5.8.2" + }, + "bin": { + "ts-json-schema-generator": "bin/ts-json-schema-generator.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ts-json-schema-generator/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5584,6 +5753,12 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true }, + "@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true + }, "@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -6950,6 +7125,24 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "dev": true }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6978,6 +7171,20 @@ "resolve-pkg-maps": "^1.0.0" } }, + "glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "dev": true, + "requires": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + } + }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7108,6 +7315,15 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "requires": { + "@isaacs/cliui": "^9.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7376,6 +7592,12 @@ "triple-beam": "^1.3.0" } }, + "lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true + }, "mdns-server": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/mdns-server/-/mdns-server-1.0.11.tgz", @@ -7415,6 +7637,12 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, + "minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true + }, "moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -7444,6 +7672,12 @@ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", "dev": true }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, "nrf-intel-hex": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/nrf-intel-hex/-/nrf-intel-hex-1.4.0.tgz", @@ -7528,6 +7762,12 @@ "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", "dev": true }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7561,6 +7801,16 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "requires": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7925,6 +8175,30 @@ "dev": true, "requires": {} }, + "ts-json-schema-generator": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-2.4.0.tgz", + "integrity": "sha512-HbmNsgs58CfdJq0gpteRTxPXG26zumezOs+SB9tgky6MpqiFgQwieCn2MW70+sxpHouZ/w9LW0V6L4ZQO4y1Ug==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.15", + "commander": "^13.1.0", + "glob": "^11.0.1", + "json5": "^2.2.3", + "normalize-path": "^3.0.0", + "safe-stable-stringify": "^2.5.0", + "tslib": "^2.8.1", + "typescript": "^5.8.2" + }, + "dependencies": { + "commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true + } + } + }, "tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index e8f2938a0..2c04a09a9 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,9 @@ "scripts": { "lint": "eslint", "lint:fix": "eslint --fix && prettier -w .", - "test": "prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", + "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", + "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --out src/lib/generated/incoming_message_schema.json", + "prebuild": "npm run generate:schema", "build": "tsc -p .", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", "prepare": "npm run build", @@ -66,6 +68,7 @@ "lint-staged": "^16.2.6", "prettier": "^3.6.2", "semver": "^7.5.4", + "ts-json-schema-generator": "^2.4.0", "tsx": "^4.19.2", "typescript": "^5.3.3", "typescript-eslint": "^8.46.1", diff --git a/src/lib/command.ts b/src/lib/command.ts index 16008a29e..03ccd6071 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -3,9 +3,11 @@ export { ConfigManagerCommand } from "./config_manager/command.js"; export { ControllerCommand } from "./controller/command.js"; export { DriverCommand } from "./driver/command.js"; export { EndpointCommand } from "./endpoint/command.js"; +export { IntrospectCommand } from "./introspect/command.js"; export { MulticastGroupCommand } from "./multicast_group/command.js"; export { NodeCommand } from "./node/command.js"; export { UtilsCommand } from "./utils/command.js"; +export { ZnifferCommand } from "./zniffer/command.js"; export enum ServerCommand { startListening = "start_listening", diff --git a/src/lib/controller/incoming_message.ts b/src/lib/controller/incoming_message.ts index e64975263..c9c21a620 100644 --- a/src/lib/controller/incoming_message.ts +++ b/src/lib/controller/incoming_message.ts @@ -285,7 +285,7 @@ export interface IncomingCommandControllerFirmwareUpdateOTW extends IncomingComm fileFormat?: FirmwareFileFormat; } -export interface IncomingCommandIsFirmwareUpdateInProgress extends IncomingCommandControllerBase { +export interface IncomingCommandControllerIsFirmwareUpdateInProgress extends IncomingCommandControllerBase { command: ControllerCommand.isFirmwareUpdateInProgress; } @@ -354,7 +354,7 @@ export type IncomingMessageController = | IncomingCommandControllerBeginOTAFirmwareUpdate | IncomingCommandControllerFirmwareUpdateOTA | IncomingCommandControllerFirmwareUpdateOTW - | IncomingCommandIsFirmwareUpdateInProgress + | IncomingCommandControllerIsFirmwareUpdateInProgress | IncomingCommandControllerSetMaxLongRangePowerlevel | IncomingCommandControllerGetMaxLongRangePowerlevel | IncomingCommandControllerSetLongRangeChannel diff --git a/src/lib/driver/incoming_message.ts b/src/lib/driver/incoming_message.ts index 02edeb563..defa13abb 100644 --- a/src/lib/driver/incoming_message.ts +++ b/src/lib/driver/incoming_message.ts @@ -9,87 +9,87 @@ import { } from "zwave-js"; import { LogContexts } from "../logging.js"; -interface IncomingCommandGetConfig extends IncomingCommandBase { +interface IncomingCommandDriverGetConfig extends IncomingCommandBase { command: DriverCommand.getConfig; } -interface IncomingCommandUpdateLogConfig extends IncomingCommandBase { +interface IncomingCommandDriverUpdateLogConfig extends IncomingCommandBase { command: DriverCommand.updateLogConfig; config: Partial; } -interface IncomingCommandGetLogConfig extends IncomingCommandBase { +interface IncomingCommandDriverGetLogConfig extends IncomingCommandBase { command: DriverCommand.getLogConfig; } -interface IncomingCommandEnableStatistics extends IncomingCommandBase { +interface IncomingCommandDriverEnableStatistics extends IncomingCommandBase { command: DriverCommand.enableStatistics; applicationName: string; applicationVersion: string; } -interface IncomingCommandDisableStatistics extends IncomingCommandBase { +interface IncomingCommandDriverDisableStatistics extends IncomingCommandBase { command: DriverCommand.disableStatistics; } -interface IncomingCommandIsStatisticsEnabled extends IncomingCommandBase { +interface IncomingCommandDriverIsStatisticsEnabled extends IncomingCommandBase { command: DriverCommand.isStatisticsEnabled; } -interface IncomingCommandStartListeningLogs extends IncomingCommandBase { +interface IncomingCommandDriverStartListeningLogs extends IncomingCommandBase { command: DriverCommand.startListeningLogs; filter?: Partial; } -interface IncomingCommandStopListeningLogs extends IncomingCommandBase { +interface IncomingCommandDriverStopListeningLogs extends IncomingCommandBase { command: DriverCommand.stopListeningLogs; } -interface IncomingCommandCheckForConfigUpdates extends IncomingCommandBase { +interface IncomingCommandDriverCheckForConfigUpdates extends IncomingCommandBase { command: DriverCommand.checkForConfigUpdates; } -interface IncomingCommandInstallConfigUpdate extends IncomingCommandBase { +interface IncomingCommandDriverInstallConfigUpdate extends IncomingCommandBase { command: DriverCommand.installConfigUpdate; } -interface IncomingCommandSetPreferredScales extends IncomingCommandBase { +interface IncomingCommandDriverSetPreferredScales extends IncomingCommandBase { command: DriverCommand.setPreferredScales; scales: ZWaveOptions["preferences"]["scales"]; } -interface IncomingCommandEnableErrorReporting extends IncomingCommandBase { +interface IncomingCommandDriverEnableErrorReporting extends IncomingCommandBase { command: DriverCommand.enableErrorReporting; } -interface IncomingCommandSoftReset extends IncomingCommandBase { +interface IncomingCommandDriverSoftReset extends IncomingCommandBase { command: DriverCommand.softReset; } -interface IncomingCommandTrySoftReset extends IncomingCommandBase { +interface IncomingCommandDriverTrySoftReset extends IncomingCommandBase { command: DriverCommand.trySoftReset; } -interface IncomingCommandHardReset extends IncomingCommandBase { +interface IncomingCommandDriverHardReset extends IncomingCommandBase { command: DriverCommand.hardReset; } -interface IncomingCommandShutdown extends IncomingCommandBase { +interface IncomingCommandDriverShutdown extends IncomingCommandBase { command: DriverCommand.shutdown; } -interface IncomingCommandUpdateOptions extends IncomingCommandBase { +interface IncomingCommandDriverUpdateOptions extends IncomingCommandBase { command: DriverCommand.updateOptions; options: EditableZWaveOptions; } -interface IncomingCommandSendTestFrame extends IncomingCommandBase { +interface IncomingCommandDriverSendTestFrame extends IncomingCommandBase { command: DriverCommand.sendTestFrame; nodeId: number; powerlevel: Powerlevel; } -export type IncomingCommandFirmwareUpdateOTW = IncomingCommandBase & { +export type IncomingCommandDriverFirmwareUpdateOTW = IncomingCommandBase & { command: DriverCommand.firmwareUpdateOTW; } & ( | { @@ -105,31 +105,31 @@ export type IncomingCommandFirmwareUpdateOTW = IncomingCommandBase & { } ); -export interface IncomingCommandIsOTWFirmwareUpdateInProgress extends IncomingCommandBase { +export interface IncomingCommandDriverIsOTWFirmwareUpdateInProgress extends IncomingCommandBase { command: DriverCommand.isOTWFirmwareUpdateInProgress; } -interface IncomingCommandSoftResetAndRestart extends IncomingCommandBase { +interface IncomingCommandDriverSoftResetAndRestart extends IncomingCommandBase { command: DriverCommand.softResetAndRestart; } -interface IncomingCommandEnterBootloader extends IncomingCommandBase { +interface IncomingCommandDriverEnterBootloader extends IncomingCommandBase { command: DriverCommand.enterBootloader; } -interface IncomingCommandLeaveBootloader extends IncomingCommandBase { +interface IncomingCommandDriverLeaveBootloader extends IncomingCommandBase { command: DriverCommand.leaveBootloader; } // CC version queries -interface IncomingCommandGetSupportedCCVersion extends IncomingCommandBase { +interface IncomingCommandDriverGetSupportedCCVersion extends IncomingCommandBase { command: DriverCommand.getSupportedCCVersion; cc: CommandClasses; nodeId: number; endpointIndex?: number; } -interface IncomingCommandGetSafeCCVersion extends IncomingCommandBase { +interface IncomingCommandDriverGetSafeCCVersion extends IncomingCommandBase { command: DriverCommand.getSafeCCVersion; cc: CommandClasses; nodeId: number; @@ -137,47 +137,47 @@ interface IncomingCommandGetSafeCCVersion extends IncomingCommandBase { } // User agent -interface IncomingCommandUpdateUserAgent extends IncomingCommandBase { +interface IncomingCommandDriverUpdateUserAgent extends IncomingCommandBase { command: DriverCommand.updateUserAgent; components: Record; } // RSSI monitoring -interface IncomingCommandEnableFrequentRSSIMonitoring extends IncomingCommandBase { +interface IncomingCommandDriverEnableFrequentRSSIMonitoring extends IncomingCommandBase { command: DriverCommand.enableFrequentRSSIMonitoring; durationMs: number; } -interface IncomingCommandDisableFrequentRSSIMonitoring extends IncomingCommandBase { +interface IncomingCommandDriverDisableFrequentRSSIMonitoring extends IncomingCommandBase { command: DriverCommand.disableFrequentRSSIMonitoring; } export type IncomingMessageDriver = - | IncomingCommandGetConfig - | IncomingCommandUpdateLogConfig - | IncomingCommandGetLogConfig - | IncomingCommandDisableStatistics - | IncomingCommandEnableStatistics - | IncomingCommandIsStatisticsEnabled - | IncomingCommandStartListeningLogs - | IncomingCommandStopListeningLogs - | IncomingCommandCheckForConfigUpdates - | IncomingCommandInstallConfigUpdate - | IncomingCommandSetPreferredScales - | IncomingCommandEnableErrorReporting - | IncomingCommandSoftReset - | IncomingCommandTrySoftReset - | IncomingCommandHardReset - | IncomingCommandShutdown - | IncomingCommandUpdateOptions - | IncomingCommandSendTestFrame - | IncomingCommandFirmwareUpdateOTW - | IncomingCommandIsOTWFirmwareUpdateInProgress - | IncomingCommandSoftResetAndRestart - | IncomingCommandEnterBootloader - | IncomingCommandLeaveBootloader - | IncomingCommandGetSupportedCCVersion - | IncomingCommandGetSafeCCVersion - | IncomingCommandUpdateUserAgent - | IncomingCommandEnableFrequentRSSIMonitoring - | IncomingCommandDisableFrequentRSSIMonitoring; + | IncomingCommandDriverGetConfig + | IncomingCommandDriverUpdateLogConfig + | IncomingCommandDriverGetLogConfig + | IncomingCommandDriverDisableStatistics + | IncomingCommandDriverEnableStatistics + | IncomingCommandDriverIsStatisticsEnabled + | IncomingCommandDriverStartListeningLogs + | IncomingCommandDriverStopListeningLogs + | IncomingCommandDriverCheckForConfigUpdates + | IncomingCommandDriverInstallConfigUpdate + | IncomingCommandDriverSetPreferredScales + | IncomingCommandDriverEnableErrorReporting + | IncomingCommandDriverSoftReset + | IncomingCommandDriverTrySoftReset + | IncomingCommandDriverHardReset + | IncomingCommandDriverShutdown + | IncomingCommandDriverUpdateOptions + | IncomingCommandDriverSendTestFrame + | IncomingCommandDriverFirmwareUpdateOTW + | IncomingCommandDriverIsOTWFirmwareUpdateInProgress + | IncomingCommandDriverSoftResetAndRestart + | IncomingCommandDriverEnterBootloader + | IncomingCommandDriverLeaveBootloader + | IncomingCommandDriverGetSupportedCCVersion + | IncomingCommandDriverGetSafeCCVersion + | IncomingCommandDriverUpdateUserAgent + | IncomingCommandDriverEnableFrequentRSSIMonitoring + | IncomingCommandDriverDisableFrequentRSSIMonitoring; diff --git a/src/lib/incoming_message.ts b/src/lib/incoming_message.ts index d492f4b6a..ca2e4adb1 100644 --- a/src/lib/incoming_message.ts +++ b/src/lib/incoming_message.ts @@ -10,6 +10,7 @@ import { IncomingMessageEndpoint } from "./endpoint/incoming_message.js"; import { IncomingMessageUtils } from "./utils/incoming_message.js"; import { IncomingMessageConfigManager } from "./config_manager/incoming_message.js"; import { LogContexts } from "./logging.js"; +import { IncomingMessageIntrospect } from "./introspect/incoming_message.js"; import { IncomingMessageZniffer } from "./zniffer/incoming_message.js"; interface IncomingCommandStartListening extends IncomingCommandBase { @@ -59,6 +60,7 @@ export type IncomingMessage = | IncomingMessageUtils | IncomingMessageZniffer | IncomingMessageConfigManager + | IncomingMessageIntrospect | IncomingCommandInitialize | IncomingCommandStartListeningLogs | IncomingCommandStopListeningLogs; diff --git a/src/lib/instance.ts b/src/lib/instance.ts index 51aecb33e..dfa764ab2 100644 --- a/src/lib/instance.ts +++ b/src/lib/instance.ts @@ -4,6 +4,7 @@ export enum Instance { controller = "controller", driver = "driver", endpoint = "endpoint", + introspect = "introspect", multicast_group = "multicast_group", node = "node", utils = "utils", diff --git a/src/lib/introspect/command.ts b/src/lib/introspect/command.ts new file mode 100644 index 000000000..32ca90b99 --- /dev/null +++ b/src/lib/introspect/command.ts @@ -0,0 +1,3 @@ +export enum IntrospectCommand { + commands = "introspect.commands", +} diff --git a/src/lib/introspect/incoming_message.ts b/src/lib/introspect/incoming_message.ts new file mode 100644 index 000000000..a83bbc805 --- /dev/null +++ b/src/lib/introspect/incoming_message.ts @@ -0,0 +1,11 @@ +import { IncomingCommandBase } from "../incoming_message_base.js"; +import { IntrospectCommand } from "./command.js"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface IncomingCommandIntrospectBase extends IncomingCommandBase {} + +export interface IncomingCommandIntrospectCommands extends IncomingCommandIntrospectBase { + command: IntrospectCommand.commands; +} + +export type IncomingMessageIntrospect = IncomingCommandIntrospectCommands; diff --git a/src/lib/introspect/message_handler.ts b/src/lib/introspect/message_handler.ts new file mode 100644 index 000000000..f2dfba710 --- /dev/null +++ b/src/lib/introspect/message_handler.ts @@ -0,0 +1,21 @@ +import { UnknownCommandError } from "../error.js"; +import incomingMessageSchema from "../generated/incoming_message_schema.json" with { type: "json" }; +import { MessageHandler } from "../message_handler.js"; +import { IntrospectCommand } from "./command.js"; +import { IncomingMessageIntrospect } from "./incoming_message.js"; +import { IntrospectResultTypes } from "./outgoing_message.js"; + +export class IntrospectMessageHandler implements MessageHandler { + async handle( + message: IncomingMessageIntrospect, + ): Promise { + const { command } = message; + + switch (message.command) { + case IntrospectCommand.commands: + return incomingMessageSchema; + default: + throw new UnknownCommandError(command); + } + } +} diff --git a/src/lib/introspect/outgoing_message.ts b/src/lib/introspect/outgoing_message.ts new file mode 100644 index 000000000..096c02301 --- /dev/null +++ b/src/lib/introspect/outgoing_message.ts @@ -0,0 +1,5 @@ +import { IntrospectCommand } from "./command.js"; + +export interface IntrospectResultTypes { + [IntrospectCommand.commands]: Record; +} diff --git a/src/lib/multicast_group/incoming_message.ts b/src/lib/multicast_group/incoming_message.ts index 269c85044..8bcb47e95 100644 --- a/src/lib/multicast_group/incoming_message.ts +++ b/src/lib/multicast_group/incoming_message.ts @@ -44,7 +44,7 @@ export interface IncomingCommandMulticastGroupSupportsCCAPI extends IncomingComm commandClass: CommandClasses; } -export interface IncomingCommandBroadcastNodeGetDefinedValueIDs extends IncomingCommandMulticastGroupBase { +export interface IncomingCommandMulticastGroupGetDefinedValueIDs extends IncomingCommandMulticastGroupBase { command: MulticastGroupCommand.getDefinedValueIDs; } @@ -55,4 +55,4 @@ export type IncomingMessageMulticastGroup = | IncomingCommandMulticastGroupGetCCVersion | IncomingCommandMulticastGroupInvokeCCAPI | IncomingCommandMulticastGroupSupportsCCAPI - | IncomingCommandBroadcastNodeGetDefinedValueIDs; + | IncomingCommandMulticastGroupGetDefinedValueIDs; diff --git a/src/lib/node/incoming_message.ts b/src/lib/node/incoming_message.ts index 025f5c311..d00a523e2 100644 --- a/src/lib/node/incoming_message.ts +++ b/src/lib/node/incoming_message.ts @@ -63,11 +63,11 @@ export interface IncomingCommandNodeAbortFirmwareUpdate extends IncomingCommandN command: NodeCommand.abortFirmwareUpdate; } -export interface IncomingCommandGetFirmwareUpdateCapabilities extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetFirmwareUpdateCapabilities extends IncomingCommandNodeBase { command: NodeCommand.getFirmwareUpdateCapabilities; } -export interface IncomingCommandGetFirmwareUpdateCapabilitiesCached extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetFirmwareUpdateCapabilitiesCached extends IncomingCommandNodeBase { command: NodeCommand.getFirmwareUpdateCapabilitiesCached; } @@ -104,79 +104,79 @@ export interface IncomingCommandNodePing extends IncomingCommandNodeBase { command: NodeCommand.ping; } -export interface IncomingCommandHasSecurityClass extends IncomingCommandNodeBase { +export interface IncomingCommandNodeHasSecurityClass extends IncomingCommandNodeBase { command: NodeCommand.hasSecurityClass; securityClass: SecurityClass; } -export interface IncomingCommandGetHighestSecurityClass extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetHighestSecurityClass extends IncomingCommandNodeBase { command: NodeCommand.getHighestSecurityClass; } -export interface IncomingCommandTestPowerlevel extends IncomingCommandNodeBase { +export interface IncomingCommandNodeTestPowerlevel extends IncomingCommandNodeBase { command: NodeCommand.testPowerlevel; testNodeId: number; powerlevel: Powerlevel; testFrameCount: number; } -export interface IncomingCommandCheckLifelineHealth extends IncomingCommandNodeBase { +export interface IncomingCommandNodeCheckLifelineHealth extends IncomingCommandNodeBase { command: NodeCommand.checkLifelineHealth; rounds?: number; } -export interface IncomingCommandCheckRouteHealth extends IncomingCommandNodeBase { +export interface IncomingCommandNodeCheckRouteHealth extends IncomingCommandNodeBase { command: NodeCommand.checkRouteHealth; targetNodeId: number; rounds?: number; } -export interface IncomingCommandGetValue extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetValue extends IncomingCommandNodeBase { command: NodeCommand.getValue; valueId: ValueID; } -export interface IncomingCommandGetEndpointCount extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetEndpointCount extends IncomingCommandNodeBase { command: NodeCommand.getEndpointCount; } -export interface IncomingCommandInterviewCC extends IncomingCommandNodeBase { +export interface IncomingCommandNodeInterviewCC extends IncomingCommandNodeBase { command: NodeCommand.interviewCC; commandClass: CommandClasses; } -export interface IncomingCommandGetState extends IncomingCommandNodeBase { +export interface IncomingCommandNodeGetState extends IncomingCommandNodeBase { command: NodeCommand.getState; } -export interface IncomingCommandSetName extends IncomingCommandNodeBase { +export interface IncomingCommandNodeSetName extends IncomingCommandNodeBase { command: NodeCommand.setName; name: string; updateCC?: boolean; } -export interface IncomingCommandSetLocation extends IncomingCommandNodeBase { +export interface IncomingCommandNodeSetLocation extends IncomingCommandNodeBase { command: NodeCommand.setLocation; location: string; updateCC?: boolean; } -export interface IncomingCommandSetKeepAwake extends IncomingCommandNodeBase { +export interface IncomingCommandNodeSetKeepAwake extends IncomingCommandNodeBase { command: NodeCommand.setKeepAwake; keepAwake: boolean; } -export interface IncomingCommandIsFirmwareUpdateInProgress extends IncomingCommandNodeBase { +export interface IncomingCommandNodeIsFirmwareUpdateInProgress extends IncomingCommandNodeBase { command: | NodeCommand.isFirmwareUpdateInProgress | NodeCommand.getFirmwareUpdateProgress; } -export interface IncomingCommandWaitForWakeup extends IncomingCommandNodeBase { +export interface IncomingCommandNodeWaitForWakeup extends IncomingCommandNodeBase { command: NodeCommand.waitForWakeup; } -export interface IncomingCommandInterview extends IncomingCommandNodeBase { +export interface IncomingCommandNodeInterview extends IncomingCommandNodeBase { command: NodeCommand.interview; } @@ -244,29 +244,29 @@ export type IncomingMessageNode = | IncomingCommandNodeBeginFirmwareUpdate | IncomingCommandNodeUpdateFirmware | IncomingCommandNodeAbortFirmwareUpdate - | IncomingCommandGetFirmwareUpdateCapabilities - | IncomingCommandGetFirmwareUpdateCapabilitiesCached + | IncomingCommandNodeGetFirmwareUpdateCapabilities + | IncomingCommandNodeGetFirmwareUpdateCapabilitiesCached | IncomingCommandNodePollValue | IncomingCommandNodeSetRawConfigParameterValue | IncomingCommandNodeGetRawConfigParameterValue | IncomingCommandNodeRefreshValues | IncomingCommandNodeRefreshCCValues | IncomingCommandNodePing - | IncomingCommandHasSecurityClass - | IncomingCommandGetHighestSecurityClass - | IncomingCommandTestPowerlevel - | IncomingCommandCheckLifelineHealth - | IncomingCommandCheckRouteHealth - | IncomingCommandGetValue - | IncomingCommandGetEndpointCount - | IncomingCommandInterviewCC - | IncomingCommandGetState - | IncomingCommandSetName - | IncomingCommandSetLocation - | IncomingCommandSetKeepAwake - | IncomingCommandIsFirmwareUpdateInProgress - | IncomingCommandWaitForWakeup - | IncomingCommandInterview + | IncomingCommandNodeHasSecurityClass + | IncomingCommandNodeGetHighestSecurityClass + | IncomingCommandNodeTestPowerlevel + | IncomingCommandNodeCheckLifelineHealth + | IncomingCommandNodeCheckRouteHealth + | IncomingCommandNodeGetValue + | IncomingCommandNodeGetEndpointCount + | IncomingCommandNodeInterviewCC + | IncomingCommandNodeGetState + | IncomingCommandNodeSetName + | IncomingCommandNodeSetLocation + | IncomingCommandNodeSetKeepAwake + | IncomingCommandNodeIsFirmwareUpdateInProgress + | IncomingCommandNodeWaitForWakeup + | IncomingCommandNodeInterview | IncomingCommandNodeGetValueTimestamp | IncomingCommandNodeManuallyIdleNotificationValueMethod1 | IncomingCommandNodeManuallyIdleNotificationValueMethod2 diff --git a/src/lib/outgoing_message.ts b/src/lib/outgoing_message.ts index 13c24748d..37358bc28 100644 --- a/src/lib/outgoing_message.ts +++ b/src/lib/outgoing_message.ts @@ -10,6 +10,7 @@ import { MulticastGroupResultTypes } from "./multicast_group/outgoing_message.js import { EndpointResultTypes } from "./endpoint/outgoing_message.js"; import { UtilsResultTypes } from "./utils/outgoing_message.js"; import { ConfigManagerResultTypes } from "./config_manager/outgoing_message.js"; +import { IntrospectResultTypes } from "./introspect/outgoing_message.js"; import { ZnifferResultTypes } from "./zniffer/outgoing_message.js"; // https://github.com/microsoft/TypeScript/issues/1897#issuecomment-822032151 @@ -77,6 +78,7 @@ export type ResultTypes = ServerResultTypes & BroadcastNodeResultTypes & EndpointResultTypes & UtilsResultTypes & + IntrospectResultTypes & ZnifferResultTypes & ConfigManagerResultTypes; diff --git a/src/lib/server.ts b/src/lib/server.ts index 1645a36d5..fddc9c91e 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -38,6 +38,7 @@ import { import { Instance } from "./instance.js"; import { ServerCommand } from "./command.js"; import { DriverMessageHandler } from "./driver/message_handler.js"; +import { IntrospectMessageHandler } from "./introspect/message_handler.js"; import { LogContexts, LoggingEventForwarder } from "./logging.js"; import { BroadcastNodeMessageHandler } from "./broadcast_node/message_handler.js"; import { MulticastGroupMessageHandler } from "./multicast_group/message_handler.js"; @@ -113,6 +114,7 @@ export class Client { this, ), [Instance.endpoint]: new EndpointMessageHandler(this.driver, this), + [Instance.introspect]: new IntrospectMessageHandler(), [Instance.utils]: new UtilsMessageHandler(), [Instance.zniffer]: new ZnifferMessageHandler(driver, clientsController), }; From 0466008c74c3fd8d94991bae05b9a726026714c6 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:29:37 -0500 Subject: [PATCH 02/10] fix: ensure schema generation works on clean checkout - Add `mkdir -p` to create generated directory before schema generation - Add `--no-type-check` to avoid type errors when JSON doesn't exist yet - Compress introspect responses (schema payload is ~189KB) Co-Authored-By: Claude Opus 4.6 --- README.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1f3c83b47..593045a4e 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ interface { ### Introspection -The server supports introspection commands that allow clients to discover the API at runtime. These commands work **before `initialize`**, so clients can explore the API before negotiating a schema version. +The server supports introspection commands that allow clients to discover the API at runtime. These commands work both before and after `initialize`, so clients can explore the API at any point — including before negotiating a schema version. #### Get command schema diff --git a/package.json b/package.json index 2c04a09a9..ea43fbfa5 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lint": "eslint", "lint:fix": "eslint --fix && prettier -w .", "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", - "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --out src/lib/generated/incoming_message_schema.json", + "generate:schema": "mkdir -p src/lib/generated && ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/lib/generated/incoming_message_schema.json", "prebuild": "npm run generate:schema", "build": "tsc -p .", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", From 938eb47efe73bc43491637c8599c180c86f7aac7 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:12:50 -0500 Subject: [PATCH 03/10] fix: address PR review feedback - Enable compression for introspect responses (large schema payload) - Use cross-platform Node.js script instead of mkdir -p - Add integration test for pre-initialize introspect.commands Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/lib/server.ts | 1 + src/test/integration.ts | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ea43fbfa5..97fbf2524 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lint": "eslint", "lint:fix": "eslint --fix && prettier -w .", "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", - "generate:schema": "mkdir -p src/lib/generated && ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/lib/generated/incoming_message_schema.json", + "generate:schema": "node -e \"require('fs').mkdirSync('src/lib/generated',{recursive:true})\" && ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/lib/generated/incoming_message_schema.json", "prebuild": "npm run generate:schema", "build": "tsc -p .", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", diff --git a/src/lib/server.ts b/src/lib/server.ts index fddc9c91e..204d94f39 100644 --- a/src/lib/server.ts +++ b/src/lib/server.ts @@ -202,6 +202,7 @@ export class Client { return this.sendResultSuccess( msg.messageId, await this.instanceHandlers[instance].handle(msg), + instance === Instance.introspect, ); } diff --git a/src/test/integration.ts b/src/test/integration.ts index 878e1537c..ce304a171 100644 --- a/src/test/integration.ts +++ b/src/test/integration.ts @@ -56,6 +56,24 @@ const runTest = async () => { type: "version", }); + // Test introspect.commands works before initialize + socket.send( + JSON.stringify({ + command: "introspect.commands", + messageId: "introspect", + }), + ); + + const introspectResult = (await nextMessage()) as any; + assert.strictEqual(introspectResult.type, "result"); + assert.strictEqual(introspectResult.success, true); + assert.strictEqual(introspectResult.messageId, "introspect"); + assert.ok(introspectResult.result.definitions, "schema has definitions"); + assert.ok( + introspectResult.result.anyOf || introspectResult.result.$ref, + "schema has anyOf or $ref at root", + ); + socket.send( JSON.stringify({ command: "initialize", From d5eb561118ee69b4959f3815fa2e48f931dec3ae Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:19:53 -0500 Subject: [PATCH 04/10] Update package.json Co-authored-by: AlCalzone --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97fbf2524..7d61b2b67 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lint": "eslint", "lint:fix": "eslint --fix && prettier -w .", "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", - "generate:schema": "node -e \"require('fs').mkdirSync('src/lib/generated',{recursive:true})\" && ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/lib/generated/incoming_message_schema.json", + "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/lib/generated/incoming_message_schema.json", "prebuild": "npm run generate:schema", "build": "tsc -p .", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", From 1feec116f2717a51b4a45fa460609f63b14c8597 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:56:32 -0500 Subject: [PATCH 05/10] fix: use readFile instead of import attributes for JSON import Avoids Node version compatibility issues with `with { type: "json" }` (Node 22+) vs `assert { type: "json" }` (deprecated, errors on Node 22+). Uses readFile with caching for the 189KB schema payload. Adds a copy step to the build so the generated JSON lands in dist-esm/. Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/lib/introspect/message_handler.ts | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 7d61b2b67..e18c46020 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/lib/generated/incoming_message_schema.json", "prebuild": "npm run generate:schema", - "build": "tsc -p .", + "build": "tsc -p . && mkdir -p dist-esm/lib/generated && cp src/lib/generated/incoming_message_schema.json dist-esm/lib/generated/", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", "prepare": "npm run build", "prepublishOnly": "rm -rf dist-* && npm run build" diff --git a/src/lib/introspect/message_handler.ts b/src/lib/introspect/message_handler.ts index f2dfba710..c8175b45d 100644 --- a/src/lib/introspect/message_handler.ts +++ b/src/lib/introspect/message_handler.ts @@ -1,10 +1,19 @@ +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import { UnknownCommandError } from "../error.js"; -import incomingMessageSchema from "../generated/incoming_message_schema.json" with { type: "json" }; import { MessageHandler } from "../message_handler.js"; import { IntrospectCommand } from "./command.js"; import { IncomingMessageIntrospect } from "./incoming_message.js"; import { IntrospectResultTypes } from "./outgoing_message.js"; +const schemaPath = join( + dirname(fileURLToPath(import.meta.url)), + "../generated/incoming_message_schema.json", +); + +let cachedSchema: Record | undefined; + export class IntrospectMessageHandler implements MessageHandler { async handle( message: IncomingMessageIntrospect, @@ -13,7 +22,10 @@ export class IntrospectMessageHandler implements MessageHandler { switch (message.command) { case IntrospectCommand.commands: - return incomingMessageSchema; + if (!cachedSchema) { + cachedSchema = JSON.parse(await readFile(schemaPath, "utf-8")); + } + return cachedSchema!; default: throw new UnknownCommandError(command); } From c5db0891da58f1866e927e494bbf9fa49d84f581 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:08:02 -0500 Subject: [PATCH 06/10] fix: generate schema as TS module instead of using import attributes Instead of importing JSON with `with { type: "json" }` (Node 22+ only) or reading it at runtime with readFile, generate a .ts wrapper alongside the JSON so tsc handles it like any other module. No copy step needed, no runtime file I/O, no caching logic. Co-Authored-By: Claude Opus 4.6 --- package.json | 4 ++-- src/lib/introspect/message_handler.ts | 16 ++-------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index e18c46020..2a7bda390 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,9 @@ "lint": "eslint", "lint:fix": "eslint --fix && prettier -w .", "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", - "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/lib/generated/incoming_message_schema.json", + "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/lib/generated/incoming_message_schema.json && node -p \"'export default '+require('fs').readFileSync('src/lib/generated/incoming_message_schema.json','utf-8').trimEnd()+';'\" > src/lib/generated/incoming_message_schema.ts", "prebuild": "npm run generate:schema", - "build": "tsc -p . && mkdir -p dist-esm/lib/generated && cp src/lib/generated/incoming_message_schema.json dist-esm/lib/generated/", + "build": "tsc -p .", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", "prepare": "npm run build", "prepublishOnly": "rm -rf dist-* && npm run build" diff --git a/src/lib/introspect/message_handler.ts b/src/lib/introspect/message_handler.ts index c8175b45d..1d723fae4 100644 --- a/src/lib/introspect/message_handler.ts +++ b/src/lib/introspect/message_handler.ts @@ -1,19 +1,10 @@ -import { readFile } from "node:fs/promises"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; import { UnknownCommandError } from "../error.js"; +import incomingMessageSchema from "../generated/incoming_message_schema.js"; import { MessageHandler } from "../message_handler.js"; import { IntrospectCommand } from "./command.js"; import { IncomingMessageIntrospect } from "./incoming_message.js"; import { IntrospectResultTypes } from "./outgoing_message.js"; -const schemaPath = join( - dirname(fileURLToPath(import.meta.url)), - "../generated/incoming_message_schema.json", -); - -let cachedSchema: Record | undefined; - export class IntrospectMessageHandler implements MessageHandler { async handle( message: IncomingMessageIntrospect, @@ -22,10 +13,7 @@ export class IntrospectMessageHandler implements MessageHandler { switch (message.command) { case IntrospectCommand.commands: - if (!cachedSchema) { - cachedSchema = JSON.parse(await readFile(schemaPath, "utf-8")); - } - return cachedSchema!; + return incomingMessageSchema; default: throw new UnknownCommandError(command); } From 3fc44d0cd6f46a243f5b4f0b6c11037308c75e01 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:13:02 -0500 Subject: [PATCH 07/10] refactor: move generated directory to src/generated/ Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 +- package.json | 2 +- src/lib/introspect/message_handler.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2ef4fe209..1ac740f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ config.json *.tgz cache .claude -/src/lib/generated/ +/src/generated/ diff --git a/package.json b/package.json index 2a7bda390..7f18b42fe 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lint": "eslint", "lint:fix": "eslint --fix && prettier -w .", "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", - "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/lib/generated/incoming_message_schema.json && node -p \"'export default '+require('fs').readFileSync('src/lib/generated/incoming_message_schema.json','utf-8').trimEnd()+';'\" > src/lib/generated/incoming_message_schema.ts", + "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/generated/incoming_message_schema.json && node -p \"'export default '+require('fs').readFileSync('src/generated/incoming_message_schema.json','utf-8').trimEnd()+';'\" > src/generated/incoming_message_schema.ts", "prebuild": "npm run generate:schema", "build": "tsc -p .", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", diff --git a/src/lib/introspect/message_handler.ts b/src/lib/introspect/message_handler.ts index 1d723fae4..5f5b2ee4e 100644 --- a/src/lib/introspect/message_handler.ts +++ b/src/lib/introspect/message_handler.ts @@ -1,5 +1,5 @@ import { UnknownCommandError } from "../error.js"; -import incomingMessageSchema from "../generated/incoming_message_schema.js"; +import incomingMessageSchema from "../../generated/incoming_message_schema.js"; import { MessageHandler } from "../message_handler.js"; import { IntrospectCommand } from "./command.js"; import { IncomingMessageIntrospect } from "./incoming_message.js"; From 7caf4a85ab5546fabaa13a8bd58831ff6d4e9e5a Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:20:15 -0500 Subject: [PATCH 08/10] refactor: extract schema generation into reusable script Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- scripts/generate-schema.cjs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 scripts/generate-schema.cjs diff --git a/package.json b/package.json index 7f18b42fe..42fd85548 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lint": "eslint", "lint:fix": "eslint --fix && prettier -w .", "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", - "generate:schema": "ts-json-schema-generator --path src/lib/incoming_message.ts --type IncomingMessage --tsconfig tsconfig.json --no-type-check --out src/generated/incoming_message_schema.json && node -p \"'export default '+require('fs').readFileSync('src/generated/incoming_message_schema.json','utf-8').trimEnd()+';'\" > src/generated/incoming_message_schema.ts", + "generate:schema": "node scripts/generate-schema.cjs src/lib/incoming_message.ts IncomingMessage src/generated/incoming_message_schema", "prebuild": "npm run generate:schema", "build": "tsc -p .", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", diff --git a/scripts/generate-schema.cjs b/scripts/generate-schema.cjs new file mode 100644 index 000000000..7d1da5bd2 --- /dev/null +++ b/scripts/generate-schema.cjs @@ -0,0 +1,35 @@ +// Usage: node scripts/generate-schema.cjs +// Generates a JSON schema from a TypeScript type, then wraps it as a TS module. +// Output: .json and .ts +const { execFileSync } = require("child_process"); +const { mkdirSync, readFileSync, writeFileSync } = require("fs"); +const { dirname } = require("path"); + +const [sourcePath, typeName, outputBase] = process.argv.slice(2); +if (!sourcePath || !typeName || !outputBase) { + console.error( + "Usage: node scripts/generate-schema.cjs ", + ); + process.exit(1); +} + +mkdirSync(dirname(outputBase), { recursive: true }); + +execFileSync( + "ts-json-schema-generator", + [ + "--path", + sourcePath, + "--type", + typeName, + "--tsconfig", + "tsconfig.json", + "--no-type-check", + "--out", + `${outputBase}.json`, + ], + { stdio: "inherit" }, +); + +const json = readFileSync(`${outputBase}.json`, "utf-8").trimEnd(); +writeFileSync(`${outputBase}.ts`, `export default ${json};\n`); From 65e51b2dfd105eaf9cd0925d5b1bd051a116a45d Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:21:58 -0500 Subject: [PATCH 09/10] fix: support extra flags in generate-schema script Co-Authored-By: Claude Opus 4.6 --- scripts/generate-schema.cjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/generate-schema.cjs b/scripts/generate-schema.cjs index 7d1da5bd2..9518b3437 100644 --- a/scripts/generate-schema.cjs +++ b/scripts/generate-schema.cjs @@ -1,14 +1,15 @@ -// Usage: node scripts/generate-schema.cjs +// Usage: node scripts/generate-schema.cjs [...flags] // Generates a JSON schema from a TypeScript type, then wraps it as a TS module. // Output: .json and .ts +// Extra flags are passed through to ts-json-schema-generator. const { execFileSync } = require("child_process"); const { mkdirSync, readFileSync, writeFileSync } = require("fs"); const { dirname } = require("path"); -const [sourcePath, typeName, outputBase] = process.argv.slice(2); +const [sourcePath, typeName, outputBase, ...extraFlags] = process.argv.slice(2); if (!sourcePath || !typeName || !outputBase) { console.error( - "Usage: node scripts/generate-schema.cjs ", + "Usage: node scripts/generate-schema.cjs [...flags]", ); process.exit(1); } @@ -27,6 +28,7 @@ execFileSync( "--no-type-check", "--out", `${outputBase}.json`, + ...extraFlags, ], { stdio: "inherit" }, ); From c2d5ecccf1269dd02e3d7f90f58d0e6010790f30 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Thu, 5 Mar 2026 02:07:02 -0500 Subject: [PATCH 10/10] refactor: split schema generation and TS wrapping into separate scripts generate-schema.cjs produces JSON only, wrap-json-module.cjs wraps any JSON file as a TS default export. This supports generating multiple schemas before wrapping the final output as a module. Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- scripts/generate-schema.cjs | 24 ++++++++++-------------- scripts/wrap-json-module.cjs | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 scripts/wrap-json-module.cjs diff --git a/package.json b/package.json index 42fd85548..95c279722 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lint": "eslint", "lint:fix": "eslint --fix && prettier -w .", "test": "npm run generate:schema && prettier --check src && tsc --noEmit && npm run lint && tsx src/test/integration.ts", - "generate:schema": "node scripts/generate-schema.cjs src/lib/incoming_message.ts IncomingMessage src/generated/incoming_message_schema", + "generate:schema": "node scripts/generate-schema.cjs src/lib/incoming_message.ts IncomingMessage src/generated/incoming_message_schema.json && node scripts/wrap-json-module.cjs src/generated/incoming_message_schema.json src/generated/incoming_message_schema.ts", "prebuild": "npm run generate:schema", "build": "tsc -p .", "postbuild": "esm2cjs --in dist-esm --out dist-cjs -l error -t node20", diff --git a/scripts/generate-schema.cjs b/scripts/generate-schema.cjs index 9518b3437..8c152666b 100644 --- a/scripts/generate-schema.cjs +++ b/scripts/generate-schema.cjs @@ -1,23 +1,22 @@ -// Usage: node scripts/generate-schema.cjs [...flags] -// Generates a JSON schema from a TypeScript type, then wraps it as a TS module. -// Output: .json and .ts +// Usage: node scripts/generate-schema.cjs [...flags] +// Generates a JSON schema from a TypeScript type. // Extra flags are passed through to ts-json-schema-generator. const { execFileSync } = require("child_process"); -const { mkdirSync, readFileSync, writeFileSync } = require("fs"); -const { dirname } = require("path"); +const { mkdirSync } = require("fs"); +const { dirname, join } = require("path"); -const [sourcePath, typeName, outputBase, ...extraFlags] = process.argv.slice(2); -if (!sourcePath || !typeName || !outputBase) { +const [sourcePath, typeName, output, ...extraFlags] = process.argv.slice(2); +if (!sourcePath || !typeName || !output) { console.error( - "Usage: node scripts/generate-schema.cjs [...flags]", + "Usage: node scripts/generate-schema.cjs [...flags]", ); process.exit(1); } -mkdirSync(dirname(outputBase), { recursive: true }); +mkdirSync(dirname(output), { recursive: true }); execFileSync( - "ts-json-schema-generator", + join(__dirname, "../node_modules/.bin/ts-json-schema-generator"), [ "--path", sourcePath, @@ -27,11 +26,8 @@ execFileSync( "tsconfig.json", "--no-type-check", "--out", - `${outputBase}.json`, + output, ...extraFlags, ], { stdio: "inherit" }, ); - -const json = readFileSync(`${outputBase}.json`, "utf-8").trimEnd(); -writeFileSync(`${outputBase}.ts`, `export default ${json};\n`); diff --git a/scripts/wrap-json-module.cjs b/scripts/wrap-json-module.cjs new file mode 100644 index 000000000..9baae2fdf --- /dev/null +++ b/scripts/wrap-json-module.cjs @@ -0,0 +1,14 @@ +// Usage: node scripts/wrap-json-module.cjs +// Wraps a JSON file as a TypeScript module with a default export. +const { readFileSync, writeFileSync } = require("fs"); + +const [input, output] = process.argv.slice(2); +if (!input || !output) { + console.error( + "Usage: node scripts/wrap-json-module.cjs ", + ); + process.exit(1); +} + +const json = readFileSync(input, "utf-8").trimEnd(); +writeFileSync(output, `export default ${json};\n`);