1111 * Check: bun run apps/cli/scripts/export-sdk-contract.ts --check
1212 */
1313
14- import { readFileSync , writeFileSync , mkdirSync } from 'node:fs' ;
14+ import { writeFileSync , mkdirSync , readFileSync } from 'node:fs' ;
1515import { resolve , dirname } from 'node:path' ;
16- import { createHash } from 'node:crypto' ;
1716import { tmpdir } from 'node:os' ;
1817
1918import { COMMAND_CATALOG , INTENT_GROUP_META } from '@superdoc/document-api' ;
19+ import { buildContractSnapshot } from '@superdoc/document-api/scripts/lib/contract-snapshot.ts' ;
2020
2121import { CLI_OPERATION_METADATA } from '../src/cli/operation-params' ;
2222import {
@@ -27,7 +27,7 @@ import {
2727 cliRequiresDocumentContext ,
2828 toDocApiId ,
2929} from '../src/cli/operation-set' ;
30- import type { CliOnlyOperation } from '../src/cli/types' ;
30+ import type { CliOnlyOperation , CliOperationParamSpec , CliTypeSpec } from '../src/cli/types' ;
3131import { CLI_ONLY_OPERATION_DEFINITIONS } from '../src/cli/cli-only-operation-definitions' ;
3232import { RESPONSE_ENVELOPE_KEY } from '../src/cli/operation-hints' ;
3333import { HOST_PROTOCOL_VERSION , HOST_PROTOCOL_FEATURES , HOST_PROTOCOL_NOTIFICATIONS } from '../src/host/protocol' ;
@@ -48,29 +48,60 @@ function classifySdkSurface(operationId: string): SdkSurface {
4848 return 'document' ;
4949}
5050
51+ function buildParamSchema ( param : CliOperationParamSpec ) : Record < string , unknown > {
52+ let schema : Record < string , unknown > ;
53+
54+ if ( param . type === 'string' && param . schema ) schema = { type : 'string' , ...( param . schema as CliTypeSpec ) } ;
55+ else if ( param . type === 'string' ) schema = { type : 'string' } ;
56+ else if ( param . type === 'number' ) schema = { type : 'number' } ;
57+ else if ( param . type === 'boolean' ) schema = { type : 'boolean' } ;
58+ else if ( param . type === 'string[]' ) schema = { type : 'array' , items : { type : 'string' } } ;
59+ else if ( param . type === 'json' && param . schema && ( param . schema as CliTypeSpec ) . type !== 'json' ) {
60+ schema = { ...( param . schema as CliTypeSpec ) } ;
61+ } else {
62+ schema = { type : 'object' } ;
63+ }
64+
65+ if ( param . description ) schema . description = param . description ;
66+ return schema ;
67+ }
68+
69+ function buildCliOnlyInputSchema (
70+ params : readonly CliOperationParamSpec [ ] ,
71+ sdkSurface : SdkSurface ,
72+ ) : Record < string , unknown > {
73+ const properties : Record < string , Record < string , unknown > > = { } ;
74+ const required : string [ ] = [ ] ;
75+
76+ for ( const param of params ) {
77+ if ( param . agentVisible === false ) continue ;
78+ if ( sdkSurface === 'document' && ( param . name === 'doc' || param . name === 'sessionId' ) ) continue ;
79+
80+ properties [ param . name ] = buildParamSchema ( param ) ;
81+ if ( param . required ) required . push ( param . name ) ;
82+ }
83+
84+ return {
85+ type : 'object' ,
86+ properties,
87+ ...( required . length > 0 ? { required } : { } ) ,
88+ additionalProperties : false ,
89+ } ;
90+ }
91+
5192// ---------------------------------------------------------------------------
5293// Paths
5394// ---------------------------------------------------------------------------
5495
5596const ROOT = resolve ( import . meta. dir , '../../..' ) ;
5697const CLI_DIR = resolve ( ROOT , 'apps/cli' ) ;
57- const CONTRACT_JSON_PATH = resolve ( ROOT , 'packages/document-api/generated/schemas/document-api-contract.json' ) ;
5898const OUTPUT_PATH = resolve ( CLI_DIR , 'generated/sdk-contract.json' ) ;
5999const CLI_PKG_PATH = resolve ( CLI_DIR , 'package.json' ) ;
60100
61101// ---------------------------------------------------------------------------
62102// Load inputs
63103// ---------------------------------------------------------------------------
64104
65- function loadDocApiContract ( ) : {
66- contractVersion : string ;
67- $defs ?: Record < string , unknown > ;
68- operations : Record < string , Record < string , unknown > > ;
69- } {
70- const raw = readFileSync ( CONTRACT_JSON_PATH , 'utf-8' ) ;
71- return JSON . parse ( raw ) ;
72- }
73-
74105function loadCliPackage ( ) : { name : string ; version : string } {
75106 const raw = readFileSync ( CLI_PKG_PATH , 'utf-8' ) ;
76107 return JSON . parse ( raw ) ;
@@ -81,10 +112,14 @@ function loadCliPackage(): { name: string; version: string } {
81112// ---------------------------------------------------------------------------
82113
83114function buildSdkContract ( ) {
84- const docApiContract = loadDocApiContract ( ) ;
115+ // Read the live document-api source snapshot instead of the generated JSON
116+ // artifact. This keeps SDK export resilient when developers add operations
117+ // before refreshing packages/document-api/generated/.
118+ const docApiContract = buildContractSnapshot ( ) ;
85119 const cliPkg = loadCliPackage ( ) ;
86-
87- const sourceHash = createHash ( 'sha256' ) . update ( JSON . stringify ( docApiContract ) ) . digest ( 'hex' ) . slice ( 0 , 16 ) ;
120+ const docApiOperations = Object . fromEntries (
121+ docApiContract . operations . map ( ( operation ) => [ operation . operationId , operation ] ) ,
122+ ) ;
88123
89124 const operations : Record < string , unknown > = { } ;
90125
@@ -94,11 +129,12 @@ function buildSdkContract() {
94129 const stripped = cliOpId . slice ( 4 ) as CliOnlyOperation ;
95130
96131 const cliOnlyDef = docApiId ? null : CLI_ONLY_OPERATION_DEFINITIONS [ stripped ] ;
132+ const sdkSurface = classifySdkSurface ( cliOpId ) ;
97133
98134 // Base fields shared by all operations
99135 const entry : Record < string , unknown > = {
100136 operationId : cliOpId ,
101- sdkSurface : classifySdkSurface ( cliOpId ) ,
137+ sdkSurface,
102138 command : metadata . command ,
103139 commandTokens : [ ...cliCommandTokens ( cliOpId ) ] ,
104140 category : cliCategory ( cliOpId ) ,
@@ -135,21 +171,22 @@ function buildSdkContract() {
135171 entry . supportsTrackedMode = catalog . supportsTrackedMode ;
136172 entry . supportsDryRun = catalog . supportsDryRun ;
137173
138- // Schema plane from document-api-contract.json
139- const docOp = docApiContract . operations [ docApiId ] ;
174+ // Schema plane from the source snapshot.
175+ const docOp = docApiOperations [ docApiId ] ;
140176 if ( ! docOp ) {
141- throw new Error ( `Missing document-api contract entry for ${ docApiId } ` ) ;
177+ throw new Error ( `CLI operation ${ cliOpId } maps to missing document-api source entry ${ docApiId } . ` ) ;
142178 }
143- entry . inputSchema = docOp . inputSchema ;
144- entry . outputSchema = docOp . outputSchema ;
145- if ( docOp . successSchema ) entry . successSchema = docOp . successSchema ;
146- if ( docOp . failureSchema ) entry . failureSchema = docOp . failureSchema ;
179+ entry . inputSchema = docOp . schemas . input ;
180+ entry . outputSchema = docOp . schemas . output ;
181+ if ( docOp . schemas . success ) entry . successSchema = docOp . schemas . success ;
182+ if ( docOp . schemas . failure ) entry . failureSchema = docOp . schemas . failure ;
147183 if ( docOp . skipAsATool ) entry . skipAsATool = true ;
148184 if ( docOp . intentGroup ) entry . intentGroup = docOp . intentGroup ;
149185 if ( docOp . intentAction ) entry . intentAction = docOp . intentAction ;
150186 } else {
151187 // CLI-only operation — metadata from canonical definitions
152188 const def = cliOnlyDef ! ;
189+ entry . inputSchema = buildCliOnlyInputSchema ( metadata . params , sdkSurface ) ;
153190 entry . mutates = def . sdkMetadata . mutates ;
154191 entry . idempotency = def . sdkMetadata . idempotency ;
155192 entry . supportsTrackedMode = def . sdkMetadata . supportsTrackedMode ;
@@ -168,7 +205,7 @@ function buildSdkContract() {
168205
169206 return {
170207 contractVersion : docApiContract . contractVersion ,
171- sourceHash,
208+ sourceHash : docApiContract . sourceHash ,
172209 ...( docApiContract . $defs ? { $defs : docApiContract . $defs } : { } ) ,
173210 cli : {
174211 package : cliPkg . name ,
0 commit comments