Skip to content

Commit 8a5b87a

Browse files
authored
chore: cli improvements (#2570)
* chore: cli improvements * fix: cli inline-special inserts at structural document end * fix: cli and sdk validation
1 parent 9c3acff commit 8a5b87a

34 files changed

Lines changed: 1262 additions & 121 deletions

apps/cli/scripts/export-sdk-contract.ts

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
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';
1515
import { resolve, dirname } from 'node:path';
16-
import { createHash } from 'node:crypto';
1716
import { tmpdir } from 'node:os';
1817

1918
import { COMMAND_CATALOG, INTENT_GROUP_META } from '@superdoc/document-api';
19+
import { buildContractSnapshot } from '@superdoc/document-api/scripts/lib/contract-snapshot.ts';
2020

2121
import { CLI_OPERATION_METADATA } from '../src/cli/operation-params';
2222
import {
@@ -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';
3131
import { CLI_ONLY_OPERATION_DEFINITIONS } from '../src/cli/cli-only-operation-definitions';
3232
import { RESPONSE_ENVELOPE_KEY } from '../src/cli/operation-hints';
3333
import { 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

5596
const ROOT = resolve(import.meta.dir, '../../..');
5697
const CLI_DIR = resolve(ROOT, 'apps/cli');
57-
const CONTRACT_JSON_PATH = resolve(ROOT, 'packages/document-api/generated/schemas/document-api-contract.json');
5898
const OUTPUT_PATH = resolve(CLI_DIR, 'generated/sdk-contract.json');
5999
const 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-
74105
function 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

83114
function 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,

apps/cli/skill/SKILL.md

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
name: editing-docx
2+
name: superdoc-edit-docx
33
description: Edit, query, and transform Word documents with the SuperDoc CLI v1 operation surface. Use when the user asks to read, search, modify, comment, or review changes in .docx files.
44
---
55

@@ -24,7 +24,7 @@ Use `describe command` for per-command args and constraints.
2424

2525
## Preferred Workflows
2626

27-
### 1) Stateful multi-step edits (recommended)
27+
### 1) Edit an existing document (recommended for targeted changes)
2828

2929
```bash
3030
superdoc open ./contract.docx
@@ -34,20 +34,59 @@ superdoc save --in-place
3434
superdoc close
3535
```
3636

37-
- Always use `query match` (not `find`) to discover mutation targets — it returns exact addresses with cardinality guarantees.
37+
- Use `query match` when you are modifying existing content and need an exact mutation target.
3838
- After `open`, commands run against the active/default session when `<doc>` is omitted.
3939
- Use `superdoc session list|set-default|save|close` for explicit session control.
4040
- `close` on dirty state requires `--discard` or a prior `save`.
4141

42-
### 2) Stateless one-off reads
42+
### 2) Generate or seed a document body (recommended for synthetic/probe docs)
43+
44+
Use `open --content-override` when you want to create a new body from Markdown, HTML, or plain text in one step.
45+
46+
```bash
47+
superdoc open --content-override "# Probe Title\n\nALPHA01" --override-type markdown
48+
superdoc save --out ./probe.docx
49+
superdoc close
50+
```
51+
52+
```bash
53+
superdoc open template.docx \
54+
--content-override '<p>ALPHA01 <strong>BRAVO02</strong><br/>CHARLIE03</p>' \
55+
--override-type html
56+
superdoc save --out ./probe.docx
57+
superdoc close
58+
```
59+
60+
- `--content-override` is the fastest way to seed paragraphs, headings, lists, and `<br/>` line breaks.
61+
- Use `--override-type markdown|html|text` explicitly. `open` rejects `--content-override` without it.
62+
- For generation, do not start with `query match` unless you are modifying content that already exists.
63+
64+
### 3) Generate incrementally, then reuse the insert receipt target
65+
66+
When you need deterministic inline formatting after seeding text, insert first, then reuse the returned target block/range.
67+
68+
```bash
69+
superdoc open
70+
superdoc insert --value "ALPHA01 BRAVO02 CHARLIE03"
71+
superdoc format apply --block-id <from-insert-receipt> --start 8 --end 15 --inline-json '{"fontSize":16,"fontFamily":"Times New Roman"}'
72+
superdoc format apply --block-id <from-insert-receipt> --start 16 --end 25 --inline-json '{"fontSize":10,"fontFamily":"Arial"}'
73+
superdoc save --out ./probe.docx
74+
superdoc close
75+
```
76+
77+
- The insert receipt contains the resolved target under `receipt.resolution.target`.
78+
- For a simple one-paragraph synthetic doc, direct `--block-id --start --end` formatting is usually shorter than re-querying.
79+
- Use `query match` again only if later steps need to rediscover content by meaning, not by the range you just created.
80+
81+
### 4) Stateless one-off reads
4382

4483
```bash
4584
superdoc get-text ./proposal.docx
4685
superdoc get-markdown ./proposal.docx
4786
superdoc info ./proposal.docx
4887
```
4988

50-
### 3) Stateless one-off mutations
89+
### 5) Stateless one-off mutations
5190

5291
```bash
5392
superdoc replace ./proposal.docx \
@@ -58,6 +97,20 @@ superdoc replace ./proposal.docx \
5897

5998
- In stateless mode (`<doc>` provided), mutating commands require `--out` unless using `--dry-run`.
6099

100+
### 6) Inline special nodes: tabs vs line breaks
101+
102+
- `insert line-break` inserts a real Word line break node inside the current paragraph.
103+
- `insert tab` inserts a real Word tab node inside the current paragraph.
104+
- Paragraph tab stops are different. Tab stops control layout positions; tab nodes are inline content characters that advance to the next tab stop.
105+
106+
```bash
107+
superdoc insert line-break --block-id p1 --offset 12
108+
superdoc insert tab --block-id p1 --offset 12
109+
```
110+
111+
- Use `format paragraph set-tab-stop` / related paragraph formatting commands when you need the tab stop definitions themselves.
112+
- Use the inline insert commands when you need actual `w:br` or `w:tab` content in exported DOCX.
113+
61114
### Safety: preview before apply
62115

63116
- Use `--dry-run` to preview any mutation without applying it.
@@ -76,6 +129,7 @@ superdoc replace ./proposal.docx \
76129

77130
- Replace text: `replace --target-json '{...}' --text "..."`
78131
- Insert inline text: `insert --block-id <id> --offset <n> --value "..."`
132+
- Insert inline tab/line break nodes: `insert tab`, `insert line-break`
79133
- Delete text/node: `delete --target-json '{...}'`
80134
- Delete blocks: `blocks delete`, `blocks delete-range`
81135
- Batch mutations: `mutations apply --steps-json '[...]' --atomic true --change-mode direct`
@@ -131,8 +185,10 @@ Always supported alongside their `-json` counterpart (use one, not both):
131185
## Output and Global Flags
132186

133187
- Default output is JSON envelope.
188+
- In JSON mode, command results are returned as a JSON envelope.
134189
- Use `--pretty` for human-readable output (not supported by `call`).
135-
- Global flags: `--output <json|pretty>`, `--session <id>`, `--timeout-ms <n>`.
190+
- Use `--quiet` to suppress non-essential warnings in pretty mode.
191+
- Global flags: `--output <json|pretty>`, `--session <id>`, `--timeout-ms <n>`, `--quiet`.
136192
- `<doc>` can be `-` to read DOCX bytes from stdin.
137193

138194
## Legacy Compatibility (Use Sparingly)

0 commit comments

Comments
 (0)