Skip to content

feat: add introspect module with commands endpoint#1525

Draft
raman325 wants to merge 10 commits intomasterfrom
feat/introspect-endpoint
Draft

feat: add introspect module with commands endpoint#1525
raman325 wants to merge 10 commits intomasterfrom
feat/introspect-endpoint

Conversation

@raman325
Copy link
Collaborator

@raman325 raman325 commented Mar 4, 2026

Summary

  • Adds a new introspect command module following the existing module pattern (node, controller, etc.)
  • introspect.commands returns the full JSON Schema (Draft 7) describing all incoming message types
  • Works before initialize, enabling API discovery pre-negotiation
  • Schema is generated at build time via ts-json-schema-generator and served at runtime
  • Interfaces renamed for $ref reuse in the generated schema (189KB vs 307KB)

Changes

  • src/lib/introspect/ — new module with command, incoming_message, outgoing_message, message_handler
  • src/lib/instance.ts — register introspect instance
  • src/lib/command.ts — export IntrospectCommand
  • src/lib/incoming_message.ts — add IncomingMessageIntrospect to union
  • src/lib/outgoing_message.ts — add IntrospectResultTypes to result types
  • src/lib/server.ts — wire IntrospectMessageHandler into instance handlers
  • Interface renames in node, driver, controller, multicast_group incoming messages for $ref reuse
  • package.json — add ts-json-schema-generator, generate:schema script, prebuild hook
  • README.md — add introspection section with tool recommendations

Test plan

  • npm test passes (schema generation, prettier, tsc, lint, integration tests)
  • Send {"messageId": "1", "command": "introspect.commands"} before initialize — returns valid JSON Schema
  • Returned schema validates actual command messages correctly

🤖 Generated with Claude Code

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 <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an introspect module to enable API discovery by exposing a build-time generated JSON Schema for all incoming message types, accessible before initialize.

Changes:

  • Introduces src/lib/introspect/* (command, message types, handler) and registers it as a new instance in the server routing.
  • Extends incoming/outgoing type unions and exports to include introspection and renames several incoming-message interfaces to improve $ref reuse in the generated schema.
  • Adds ts-json-schema-generator with generate:schema/prebuild hooks, updates docs, and git-ignores the generated schema output directory.

Reviewed changes

Copilot reviewed 15 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/lib/server.ts Registers the new introspect instance handler in the per-client instance handler map.
src/lib/outgoing_message.ts Adds IntrospectResultTypes into the global result type union.
src/lib/node/incoming_message.ts Renames several node incoming-message interfaces for improved schema $ref reuse and updates the union.
src/lib/multicast_group/incoming_message.ts Renames the multicast group getDefinedValueIDs incoming-message interface and updates the union.
src/lib/introspect/outgoing_message.ts Defines result types for introspect.commands.
src/lib/introspect/message_handler.ts Implements the handler that returns the generated incoming-message JSON schema.
src/lib/introspect/incoming_message.ts Defines incoming message type(s) for the new introspect module.
src/lib/introspect/command.ts Defines the new IntrospectCommand enum.
src/lib/instance.ts Adds introspect to the Instance enum for routing.
src/lib/incoming_message.ts Adds IncomingMessageIntrospect to the top-level incoming message union.
src/lib/driver/incoming_message.ts Renames driver incoming-message interfaces for schema $ref reuse and updates the union.
src/lib/controller/incoming_message.ts Renames the controller firmware update “in progress” incoming-message interface and updates the union.
src/lib/command.ts Exports IntrospectCommand (and ZnifferCommand) from the central command export module.
package.json Adds schema generation tooling and hooks (generate:schema, prebuild) and runs generation in npm test.
package-lock.json Locks the new ts-json-schema-generator dependency tree.
README.md Documents the introspection capability and usage guidance.
.gitignore Ignores /src/lib/generated/ where the schema is written.
Comments suppressed due to low confidence (1)

package.json:44

  • IntrospectMessageHandler imports ../generated/incoming_message_schema.json, but the build pipeline (tsc + esm2cjs) does not copy JSON assets into dist-esm/dist-cjs. Since the published package only includes dist-* (see files), this will likely fail at runtime with a missing module. Consider adding a postbuild step to copy src/lib/generated/incoming_message_schema.json into dist-esm/lib/generated/ (and the CJS output), or generate the schema directly into the dist folders as part of the build.
    "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",
    "prepublishOnly": "rm -rf dist-* && npm run build"

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -0,0 +1,21 @@
import { UnknownCommandError } from "../error.js";
import incomingMessageSchema from "../generated/incoming_message_schema.json" with { type: "json" };
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON import uses the newer import-attributes syntax (with { type: "json" }). Node 20 commonly supports JSON imports via assert { type: "json" }, but may not support the with form depending on the exact runtime version, which could make the server fail to start. Consider switching to the widely-supported assert { type: "json" } syntax for Node 20, or loading the JSON via fs/createRequire to avoid reliance on import attributes syntax evolution.

Suggested change
import incomingMessageSchema from "../generated/incoming_message_schema.json" with { type: "json" };
import incomingMessageSchema from "../generated/incoming_message_schema.json" assert { type: "json" };

Copilot uses AI. Check for mistakes.
Copy link
Member

@AlCalzone AlCalzone Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raman325 The comment from Copilot here is a valid concern. If we want to use the with syntax (which is the standard way forward), we need to bump to Node.js 22 minimum.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we bump to 22 here, we'd probably need to do the same in zwave-js-ui since it creates a server instance in code - is that preferred or should we stick with the deprecated syntax for now?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK assert will be a SyntaxError on Node 22+. So I think there's no way around bumping and making a major version.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, can we just use node:fs/promises and await readFile instead?

Copy link
Collaborator Author

@raman325 raman325 Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to readFile with caching per AlCalzone's suggestion in 1feec11. Also added a build copy step so the generated JSON lands in dist-esm/lib/generated/ (tsc doesn't emit it on its own). The existing createRequire usages in const.ts and server.ts are fine — they load package.json from the project root and user config paths, which are always present at runtime.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it simpler - convert the json to TS and then just import it. We keep the JSON in case it's useful for development workflows

@raman325 raman325 force-pushed the feat/introspect-endpoint branch 2 times, most recently from 4b700ac to b74b4cc Compare March 4, 2026 01:41
- 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 <noreply@anthropic.com>
@raman325 raman325 force-pushed the feat/introspect-endpoint branch from b74b4cc to 0466008 Compare March 4, 2026 01:42
Copy link
Member

@AlCalzone AlCalzone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, how does this feature take the minimum schema version for some commands into account?

@@ -0,0 +1,21 @@
import { UnknownCommandError } from "../error.js";
import incomingMessageSchema from "../generated/incoming_message_schema.json" with { type: "json" };
Copy link
Member

@AlCalzone AlCalzone Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raman325 The comment from Copilot here is a valid concern. If we want to use the with syntax (which is the standard way forward), we need to bump to Node.js 22 minimum.

@raman325
Copy link
Collaborator Author

raman325 commented Mar 4, 2026

Curious, how does this feature take the minimum schema version for some commands into account?

right now it doesn't and we haven't really encoded schema versioning into commands. We don't actually gate commands by schema, the only thing we do is add logic to make returns backwards compatible if something changes in the API.

Realistically, I am not sure how valuable being able to filter by schema version for commands is as helpful as e.g. seeing events and states for a given schema version. I was planning to add it for states because it's easy enough, we have all the types defined. Will hvae to work through how to do it for events and how it could be implemented for commands

- 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 <noreply@anthropic.com>
Copy link
Member

@AlCalzone AlCalzone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing.

raman325 and others added 2 commits March 5, 2026 00:19
Co-authored-by: AlCalzone <dominic.griesel@nabucasa.com>
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 <noreply@anthropic.com>
@raman325 raman325 force-pushed the feat/introspect-endpoint branch from fb4e567 to 1feec11 Compare March 5, 2026 06:02
raman325 and others added 5 commits March 5, 2026 01:08
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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@raman325 raman325 marked this pull request as draft March 5, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants