@nifrajs/cli 1.3.1 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/cli",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "The nifra CLI — zero-config dev/build/start for a nifra app (`nifra dev`, `nifra build`, `nifra start`).",
5
5
  "keywords": [
6
6
  "bun",
@@ -41,12 +41,13 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@nifrajs/client": "1.3.1",
45
- "@nifrajs/mcp": "1.3.1",
46
- "@nifrajs/runner": "1.3.1",
47
- "@nifrajs/schema": "1.3.1",
48
- "@nifrajs/web": "1.3.1",
49
- "create-nifra": "1.3.1"
44
+ "@nifrajs/client": "1.4.0",
45
+ "@nifrajs/core": "1.4.0",
46
+ "@nifrajs/mcp": "1.4.0",
47
+ "@nifrajs/runner": "1.4.0",
48
+ "@nifrajs/schema": "1.4.0",
49
+ "@nifrajs/web": "1.4.0",
50
+ "create-nifra": "1.4.0"
50
51
  },
51
52
  "repository": {
52
53
  "type": "git",
package/src/cli.ts CHANGED
@@ -82,7 +82,7 @@ Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with
82
82
  \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`
83
83
 
84
84
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
85
- const CLI_VERSION = "1.3.1"
85
+ const CLI_VERSION = "1.4.0"
86
86
 
87
87
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
88
88
  const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
package/src/introspect.ts CHANGED
@@ -5,38 +5,14 @@
5
5
  * *this* project's actual surface, not just generic docs.
6
6
  */
7
7
 
8
+ import { type ReflectedRoute, reflectRoutes, type SchemaReflection } from "@nifrajs/core/reflection"
8
9
  import type { Manifest } from "@nifrajs/web"
9
10
  import { discoverRoutes } from "@nifrajs/web/fs"
10
11
  import type { LoadedApp } from "./load.ts"
11
12
 
12
- /** Structural view of a `@nifrajs/core` route descriptor — declared locally so the CLI needs no type
13
- * dependency on core. `app.routes()` returns these. */
14
- interface RouteDesc {
15
- readonly method: string
16
- readonly path: string
17
- readonly schema?: unknown
18
- }
19
-
20
13
  /** Read the backend's registered routes, if it's a `server()` with a `.routes()` method. */
21
- export function backendRoutes(backend: unknown): RouteDesc[] {
22
- if (backend && typeof (backend as { routes?: unknown }).routes === "function") {
23
- try {
24
- return (backend as { routes(): RouteDesc[] }).routes()
25
- } catch {
26
- return []
27
- }
28
- }
29
- return []
30
- }
31
-
32
- /** The JSON Schema a `t`/Standard Schema carries (nifra's `t` is TypeBox-backed and exposes `.jsonSchema`),
33
- * or `undefined` for a validator that doesn't expose one. This is the exact field-level contract. */
34
- function jsonSchemaOf(schema: unknown): unknown {
35
- if (schema && typeof schema === "object" && "jsonSchema" in schema) {
36
- const js = (schema as { jsonSchema?: unknown }).jsonSchema
37
- if (js !== undefined && js !== null) return js
38
- }
39
- return undefined
14
+ export function backendRoutes(backend: unknown): ReflectedRoute[] {
15
+ return [...reflectRoutes(backend)]
40
16
  }
41
17
 
42
18
  interface JsonSchemaNode {
@@ -99,16 +75,16 @@ export function tsTypeOf(schema: unknown, depth = 0): string {
99
75
 
100
76
  /** One route's request/response field shapes, indented under its line. Reads the declared schemas off the
101
77
  * descriptor so an agent gets the exact input + output contract — not just "this route is validated". */
102
- function schemaLines(schema: unknown): string[] {
103
- const s = schema as { body?: unknown; query?: unknown; response?: unknown } | undefined
78
+ function schemaLines(schema: ReflectedRoute["schema"]): string[] {
79
+ const s = schema
104
80
  if (!s) return []
105
81
  const out: string[] = []
106
82
  for (const key of ["query", "body", "response"] as const) {
107
83
  const declared = s[key]
108
84
  if (!declared) continue
109
- const js = jsonSchemaOf(declared)
85
+ const js = declared.jsonSchema
110
86
  out.push(
111
- js
87
+ js !== undefined
112
88
  ? ` - ${key}: \`${tsTypeOf(js)}\``
113
89
  : ` - ${key}: _(validated; shape not introspectable from this validator)_`,
114
90
  )
@@ -153,7 +129,7 @@ export function clientCall(method: string, path: string, schema: unknown): strin
153
129
  }
154
130
 
155
131
  /** Markdown section listing the backend's API routes with their request + response field shapes. */
156
- export function apiRoutesSection(routes: readonly RouteDesc[]): string {
132
+ export function apiRoutesSection(routes: readonly ReflectedRoute[]): string {
157
133
  if (routes.length === 0) {
158
134
  return "## API routes\n\nNo `backend.ts` server routes found (this app may be frontend-only)."
159
135
  }
@@ -171,7 +147,7 @@ export function apiRoutesSection(routes: readonly RouteDesc[]): string {
171
147
  * body/query/response shapes or the per-route `call` line. Bounds the first-call payload on a big backend:
172
148
  * an agent gets the full route list (so it knows what's mounted) plus a pointer to fetch any route's full
173
149
  * contract via the `path`/`kind` slice, instead of every schema up front. */
174
- export function apiRoutesIndexSection(routes: readonly RouteDesc[]): string {
150
+ export function apiRoutesIndexSection(routes: readonly ReflectedRoute[]): string {
175
151
  if (routes.length === 0) {
176
152
  return "## API routes (backend.ts)\n\nNo `backend.ts` server routes found (this app may be frontend-only)."
177
153
  }
@@ -287,8 +263,10 @@ export interface RouteJson {
287
263
  export function routesToJson(app: LoadedApp, pathPrefix?: string): RouteJson[] {
288
264
  const shape = (v: unknown): string | undefined => {
289
265
  if (!v) return undefined
290
- const js = jsonSchemaOf(v)
291
- return js ? tsTypeOf(js) : "(validated; shape not introspectable from this validator)"
266
+ const js = (v as SchemaReflection).jsonSchema
267
+ return js !== undefined
268
+ ? tsTypeOf(js)
269
+ : "(validated; shape not introspectable from this validator)"
292
270
  }
293
271
  let routes = backendRoutes(app.backend)
294
272
  if (pathPrefix !== undefined && pathPrefix !== "") {
@@ -297,7 +275,7 @@ export function routesToJson(app: LoadedApp, pathPrefix?: string): RouteJson[] {
297
275
  return [...routes]
298
276
  .sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method))
299
277
  .map((r) => {
300
- const s = r.schema as { body?: unknown; query?: unknown; response?: unknown } | undefined
278
+ const s = r.schema
301
279
  const body = shape(s?.body)
302
280
  const query = shape(s?.query)
303
281
  const response = shape(s?.response)
@@ -353,7 +331,7 @@ export interface PageRouteInput {
353
331
 
354
332
  export interface RouteTableInput {
355
333
  readonly pages: readonly PageRouteInput[]
356
- readonly api: readonly RouteDesc[]
334
+ readonly api: ReadonlyArray<Pick<ReflectedRoute, "method" | "path">>
357
335
  /** The page router's `apiPrefix` (default `/api`); an API route at/under it is auto-mounted. `""`
358
336
  * disables the auto-mount, so no API route is marked auto-mounted. */
359
337
  readonly apiPrefix?: string
package/src/mcp-http.ts CHANGED
@@ -26,7 +26,7 @@ export { docsTools } from "./mcp-docs-tools.ts"
26
26
  export type { TypeEntry } from "./types-search.ts"
27
27
 
28
28
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
29
- const VERSION = "1.3.1"
29
+ const VERSION = "1.4.0"
30
30
  const SERVER_INFO = { name: "nifra-docs", version: VERSION }
31
31
  const DOCS_HEALTH =
32
32
  "nifra docs MCP — POST JSON-RPC 2.0 here (methods: initialize, tools/list, tools/call). Tools: nifra_docs, nifra_example."
package/src/mcp.ts CHANGED
@@ -26,6 +26,7 @@
26
26
  import { readFile, stat } from "node:fs/promises"
27
27
  import { basename, resolve, sep } from "node:path"
28
28
  import { fileURLToPath } from "node:url"
29
+ import { type ReflectedRoute, reflectRoutes } from "@nifrajs/core/reflection"
29
30
  import { Glob } from "bun"
30
31
  import { loadDocsCorpus } from "./docs-search.ts"
31
32
  import { loadExamplesCorpus } from "./examples.ts"
@@ -978,17 +979,8 @@ export function projectTools(
978
979
  ]
979
980
  }
980
981
 
981
- /** A `.tool()`-registered route as seen through the backend's `routes()` introspection. */
982
- type ToolRoute = {
983
- readonly schema?: { readonly body?: unknown; readonly response?: unknown }
984
- readonly tool?: {
985
- readonly name: string
986
- readonly description: string
987
- readonly annotations?: McpTool["annotations"]
988
- }
989
- }
990
982
  type ToolBackend = {
991
- readonly routes?: () => ToolRoute[]
983
+ readonly routes?: () => readonly unknown[]
992
984
  readonly fetch: (req: Request) => Promise<Response>
993
985
  }
994
986
 
@@ -997,28 +989,15 @@ type ToolBackend = {
997
989
  export function extractBackendTools(backend: unknown): McpTool[] {
998
990
  const b = backend as ToolBackend | null
999
991
  if (!b || typeof b.routes !== "function") return []
1000
- let routes: ToolRoute[] = []
1001
- try {
1002
- routes = b.routes()
1003
- } catch {
1004
- return []
1005
- }
1006
-
1007
- const jsonSchemaOf = (schema: unknown): unknown => {
1008
- if (schema && typeof schema === "object" && "jsonSchema" in schema) {
1009
- const js = (schema as { jsonSchema?: unknown }).jsonSchema
1010
- if (js !== undefined && js !== null) return js
1011
- }
1012
- return undefined
1013
- }
992
+ const routes = reflectRoutes(b)
1014
993
 
1015
994
  return routes
1016
995
  .filter(
1017
996
  (
1018
997
  r,
1019
- ): r is ToolRoute & {
1020
- schema: NonNullable<ToolRoute["schema"]>
1021
- tool: NonNullable<ToolRoute["tool"]>
998
+ ): r is ReflectedRoute & {
999
+ schema: NonNullable<ReflectedRoute["schema"]>
1000
+ tool: NonNullable<ReflectedRoute["tool"]>
1022
1001
  } => r.schema !== undefined && r.tool !== undefined,
1023
1002
  )
1024
1003
  .map((r) => {
@@ -1027,7 +1006,7 @@ export function extractBackendTools(backend: unknown): McpTool[] {
1027
1006
  return {
1028
1007
  name: toolInfo.name,
1029
1008
  description: toolInfo.description,
1030
- inputSchema: (jsonSchemaOf(s.body) ?? {
1009
+ inputSchema: (s.body?.jsonSchema ?? {
1031
1010
  type: "object",
1032
1011
  properties: {},
1033
1012
  }) as McpTool["inputSchema"],