@superdoc/sdk 2.26.0 → 2.27.0-next.9
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/README.md +236 -29
- package/dist/agent/actions.cjs +62 -24
- package/dist/agent/actions.js +62 -24
- package/dist/agent/catalog.d.ts +0 -4
- package/dist/agent/catalog.js +0 -4
- package/dist/embedded-prompts.generated.cjs +1 -1
- package/dist/embedded-prompts.generated.js +1 -1
- package/dist/embedded-tools.generated.cjs +5 -5
- package/dist/embedded-tools.generated.js +5 -5
- package/dist/generated/client.cjs +18 -1
- package/dist/generated/client.d.ts +3248 -1019
- package/dist/generated/client.js +18 -1
- package/dist/generated/contract.cjs +1 -2
- package/dist/generated/contract.d.ts +1 -0
- package/dist/generated/contract.js +1 -2
- package/dist/index.cjs +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/presets/full.cjs +4 -0
- package/dist/presets/full.js +4 -0
- package/dist/prompts/full-addendum.md +1 -0
- package/dist/runtime/host.cjs +36 -3
- package/dist/runtime/host.d.ts +5 -0
- package/dist/runtime/host.js +36 -4
- package/dist/runtime/notifications.cjs +124 -0
- package/dist/runtime/notifications.d.ts +35 -0
- package/dist/runtime/notifications.js +116 -0
- package/dist/runtime/process.cjs +4 -1
- package/dist/runtime/process.d.ts +2 -0
- package/dist/runtime/process.js +4 -1
- package/dist/runtime/sdk-policy.d.ts +1 -0
- package/dist/runtime/sdk-version.generated.cjs +1 -1
- package/dist/runtime/sdk-version.generated.d.ts +1 -1
- package/dist/runtime/sdk-version.generated.js +1 -1
- package/dist/runtime/transport-common.cjs +9 -1
- package/dist/runtime/transport-common.d.ts +3 -0
- package/dist/runtime/transport-common.js +8 -1
- package/package.json +7 -7
- package/tools/catalog.json +42 -2
- package/tools/generation-receipt.json +60 -22
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +42 -2
- package/tools/tools.generic.json +42 -2
- package/tools/tools.openai.json +42 -2
- package/tools/tools.vercel.json +42 -2
package/dist/index.cjs
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type BoundDocApi, type DocCloseBoundParams, type DocCloseResult, type DocFormatApplyBoundParams, type DocFormatApplyResult, type DocOpenParams as GeneratedDocOpenParams, type DocOpenResult, type DocSaveBoundParams, type DocSaveResult } from './generated/client.js';
|
|
2
2
|
import { SuperDocRuntime, type SuperDocClientOptions, type DocOpenOptions, type InvokeOptions, type OperationSpec, type ReplaceFileOptions, type ReplaceFileResult, type ReplaceFileSource, type RuntimeInvoker } from './runtime/process.js';
|
|
3
|
+
import type { HostNotifications, SubscribeOptions } from './runtime/notifications.js';
|
|
3
4
|
/**
|
|
4
5
|
* Wraps a raw runtime and injects a fixed sessionId into every invoke call.
|
|
5
6
|
* Implements RuntimeInvoker so generated code can use it directly.
|
|
@@ -73,6 +74,7 @@ export declare class SuperDocClient {
|
|
|
73
74
|
private readonly pendingSessionIds;
|
|
74
75
|
constructor(options?: SuperDocClientOptions);
|
|
75
76
|
connect(): Promise<void>;
|
|
77
|
+
subscribe(options?: SubscribeOptions): HostNotifications;
|
|
76
78
|
/**
|
|
77
79
|
* Open a document and return a bound document handle.
|
|
78
80
|
*
|
|
@@ -104,6 +106,7 @@ export type { AgentToolkit, CreateAgentToolkitInput } from './tools.js';
|
|
|
104
106
|
export { defineAction } from './actions/define.js';
|
|
105
107
|
export type { ActionSpec, ActionStep, DefineActionInput, JSONSchemaObject } from './actions/define.js';
|
|
106
108
|
export { SuperDocCliError } from './runtime/errors.js';
|
|
109
|
+
export type { HostNotification, HostNotifications, SubscribeOptions } from './runtime/notifications.js';
|
|
107
110
|
export { runTrackedComparisonReview } from './tracked-review-route.js';
|
|
108
111
|
export type { TrackedReviewSession, TrackedComparisonReviewInput } from './tracked-review-route.js';
|
|
109
112
|
export type { CollaborationAuth, DocOpenOptions, InvokeOptions, OperationSpec, OperationParamSpec, RuntimeInvoker, SuperDocClientOptions, } from './runtime/process.js';
|
package/dist/index.js
CHANGED
package/dist/presets/full.cjs
CHANGED
|
@@ -66,6 +66,7 @@ Where things live (ids for superdoc_api_call):
|
|
|
66
66
|
- Page layout: doc.sections.list → sectionId, then doc.sections.setPageMargins / setPageSetup / setColumns / setPageNumbering / setTitlePage / setPageBorders; doc.create.sectionBreak.
|
|
67
67
|
- Header / footer text: doc.sections.list → doc.headerFooters.list → (if the slot has no explicit part) doc.headerFooters.parts.create {kind} → doc.headerFooters.refs.set {target:{kind:"headerFooterSlot", section:{kind:"section", sectionId}, headerFooterKind, variant}, refId}. A new part already holds ONE empty paragraph: write into it (doc.blocks.list {in:<story>} for its blockId, then doc.insert {value, type:"text", in:<story>, blockId, offset:0}); do not add a second paragraph. Header/footer doc.insert and doc.replace refuse block targets: use target:{kind:"selection", start:{kind:"text", blockId, offset}, end:{kind:"text", blockId, offset}} plus in:<story>. Story = {kind:"story", storyType:"headerFooterSlot", section:{kind:"section", sectionId}, headerFooterKind, variant}. Page number: doc.fields.insert {mode:"raw", instruction:"PAGE", at:{kind:"text", story:<same story>, segments:[{blockId, range:{start:N, end:N}}]}} after the text "Page " (N = its length); "Page N of M" adds " of " and a NUMPAGES field. Alignment: doc.format.paragraph.setAlignment with the story on the target. Odd/even variants need doc.sections.setOddEvenHeadersFooters {enabled:true} first, then one part per variant.
|
|
68
68
|
- Section breaks: doc.create.sectionBreak {at:{kind:"after", target:{kind:"block", nodeType, nodeId:<last paragraph of the closing section>}}, representation:"attachToPreviousParagraph", breakType:"nextPage"} closes the section on that paragraph, as Word does; {at:{kind:"before", target:<first paragraph of the new section>}} is the same boundary; {kind:"documentEnd"} appends a new final section. Then doc.sections.list for the new sectionId and doc.sections.setPageNumbering {start:1} to restart numbering. Add one only when the task needs a new section; never delete the paragraph hosting a break to remove it.
|
|
69
|
+
- Table designs: doc.styles.getTheme reads the document palette; doc.tables.listPresets {source:"word"} returns IDs and appearance (follow nextOffset). Choose by header treatment, bandedRows.effect and fill/themeColor, and borders.layout. Accent slots are document-dependent; resolve every role through colorSchemeMapping (text/background aliases use t1/t2/bg1/bg2 keys) and apply hex tint/shade. Missing style properties inherit; direct cell formatting overrides them. doc.tables.applyPreset {nodeId, preset:<returned id>, styleOptions:{headerRow:true, bandedRows:true, bandedColumns:false}} applies a design while preserving direct formatting; choose other flags to match the task. Border-only banding does not create alternating fills. Check requested constraints and preconditions before mutating.
|
|
69
70
|
- Tables: doc.tables.setBorders / applyBorderPreset / setShading (table or cell background) / setCellProperties / setColumnWidth / setRowHeight / mergeCells / splitCell / sort / setStyle / setLayout; cell text formatting via doc.format.apply on the cell paragraph (new cells must match their siblings' run formatting — read a sibling with superdoc_inspect includeTableCellRuns:true). A NEW table (create_table, markdown) gets 1pt black borders: when the document already has tables, copy their convention (width, border colour and weight: superdoc_inspect / doc.tables.get a sibling) with doc.tables.setLayout and setBorders. A border weight the request leaves open is Word's 0.5pt. Table background = setShading on the table node, not shade_cells per cell.
|
|
70
71
|
- Formatting: doc.format.apply {target|ref, inline:{bold, italic, underline, color, fontSize, fontFamily, highlight}}; paragraph: doc.format.paragraph.setAlignment / setIndentation / setSpacing / setBorder / setShading / setKeepOptions / setTabStop / setNumbering. Units: colours are bare hex ("0000FF"), fontSize and letterSpacing in points, paragraph indentation and spacing in twips (20 = 1pt, 1440 = 1 inch), setBorder size in eighths of a point (1pt = 8) and space in points (Word's default is 1), table widths in twips where the schema says dxa. Box border: ONE batch of four setBorder {target, side:top|bottom|left|right, style:"single", size, color}; no paragraph setBorders exists.
|
|
71
72
|
- Whole-body font/size: ONE doc.format.apply with target {kind:"selection", start:{kind:"text", blockId:<first paragraph>, offset:0}, end:{kind:"text", blockId:<last paragraph>, offset:<length>}}, not set_font_family or per-paragraph calls; headers, footers and notes are separate story calls.
|
|
@@ -102,6 +103,7 @@ const QUICK_REFERENCE_IDS = [
|
|
|
102
103
|
'doc.format.paragraph.setIndentation',
|
|
103
104
|
'doc.format.paragraph.setSpacing',
|
|
104
105
|
'doc.format.paragraph.setKeepOptions',
|
|
106
|
+
'doc.styles.getTheme',
|
|
105
107
|
'doc.styles.getCatalog',
|
|
106
108
|
'doc.styles.create',
|
|
107
109
|
'doc.comments.list',
|
|
@@ -112,6 +114,8 @@ const QUICK_REFERENCE_IDS = [
|
|
|
112
114
|
'doc.trackChanges.list',
|
|
113
115
|
'doc.trackChanges.get',
|
|
114
116
|
'doc.trackChanges.decide',
|
|
117
|
+
'doc.tables.listPresets',
|
|
118
|
+
'doc.tables.applyPreset',
|
|
115
119
|
'doc.tables.get',
|
|
116
120
|
'doc.tables.getProperties',
|
|
117
121
|
'doc.tables.getCells',
|
package/dist/presets/full.js
CHANGED
|
@@ -63,6 +63,7 @@ Where things live (ids for superdoc_api_call):
|
|
|
63
63
|
- Page layout: doc.sections.list → sectionId, then doc.sections.setPageMargins / setPageSetup / setColumns / setPageNumbering / setTitlePage / setPageBorders; doc.create.sectionBreak.
|
|
64
64
|
- Header / footer text: doc.sections.list → doc.headerFooters.list → (if the slot has no explicit part) doc.headerFooters.parts.create {kind} → doc.headerFooters.refs.set {target:{kind:"headerFooterSlot", section:{kind:"section", sectionId}, headerFooterKind, variant}, refId}. A new part already holds ONE empty paragraph: write into it (doc.blocks.list {in:<story>} for its blockId, then doc.insert {value, type:"text", in:<story>, blockId, offset:0}); do not add a second paragraph. Header/footer doc.insert and doc.replace refuse block targets: use target:{kind:"selection", start:{kind:"text", blockId, offset}, end:{kind:"text", blockId, offset}} plus in:<story>. Story = {kind:"story", storyType:"headerFooterSlot", section:{kind:"section", sectionId}, headerFooterKind, variant}. Page number: doc.fields.insert {mode:"raw", instruction:"PAGE", at:{kind:"text", story:<same story>, segments:[{blockId, range:{start:N, end:N}}]}} after the text "Page " (N = its length); "Page N of M" adds " of " and a NUMPAGES field. Alignment: doc.format.paragraph.setAlignment with the story on the target. Odd/even variants need doc.sections.setOddEvenHeadersFooters {enabled:true} first, then one part per variant.
|
|
65
65
|
- Section breaks: doc.create.sectionBreak {at:{kind:"after", target:{kind:"block", nodeType, nodeId:<last paragraph of the closing section>}}, representation:"attachToPreviousParagraph", breakType:"nextPage"} closes the section on that paragraph, as Word does; {at:{kind:"before", target:<first paragraph of the new section>}} is the same boundary; {kind:"documentEnd"} appends a new final section. Then doc.sections.list for the new sectionId and doc.sections.setPageNumbering {start:1} to restart numbering. Add one only when the task needs a new section; never delete the paragraph hosting a break to remove it.
|
|
66
|
+
- Table designs: doc.styles.getTheme reads the document palette; doc.tables.listPresets {source:"word"} returns IDs and appearance (follow nextOffset). Choose by header treatment, bandedRows.effect and fill/themeColor, and borders.layout. Accent slots are document-dependent; resolve every role through colorSchemeMapping (text/background aliases use t1/t2/bg1/bg2 keys) and apply hex tint/shade. Missing style properties inherit; direct cell formatting overrides them. doc.tables.applyPreset {nodeId, preset:<returned id>, styleOptions:{headerRow:true, bandedRows:true, bandedColumns:false}} applies a design while preserving direct formatting; choose other flags to match the task. Border-only banding does not create alternating fills. Check requested constraints and preconditions before mutating.
|
|
66
67
|
- Tables: doc.tables.setBorders / applyBorderPreset / setShading (table or cell background) / setCellProperties / setColumnWidth / setRowHeight / mergeCells / splitCell / sort / setStyle / setLayout; cell text formatting via doc.format.apply on the cell paragraph (new cells must match their siblings' run formatting — read a sibling with superdoc_inspect includeTableCellRuns:true). A NEW table (create_table, markdown) gets 1pt black borders: when the document already has tables, copy their convention (width, border colour and weight: superdoc_inspect / doc.tables.get a sibling) with doc.tables.setLayout and setBorders. A border weight the request leaves open is Word's 0.5pt. Table background = setShading on the table node, not shade_cells per cell.
|
|
67
68
|
- Formatting: doc.format.apply {target|ref, inline:{bold, italic, underline, color, fontSize, fontFamily, highlight}}; paragraph: doc.format.paragraph.setAlignment / setIndentation / setSpacing / setBorder / setShading / setKeepOptions / setTabStop / setNumbering. Units: colours are bare hex ("0000FF"), fontSize and letterSpacing in points, paragraph indentation and spacing in twips (20 = 1pt, 1440 = 1 inch), setBorder size in eighths of a point (1pt = 8) and space in points (Word's default is 1), table widths in twips where the schema says dxa. Box border: ONE batch of four setBorder {target, side:top|bottom|left|right, style:"single", size, color}; no paragraph setBorders exists.
|
|
68
69
|
- Whole-body font/size: ONE doc.format.apply with target {kind:"selection", start:{kind:"text", blockId:<first paragraph>, offset:0}, end:{kind:"text", blockId:<last paragraph>, offset:<length>}}, not set_font_family or per-paragraph calls; headers, footers and notes are separate story calls.
|
|
@@ -99,6 +100,7 @@ const QUICK_REFERENCE_IDS = [
|
|
|
99
100
|
'doc.format.paragraph.setIndentation',
|
|
100
101
|
'doc.format.paragraph.setSpacing',
|
|
101
102
|
'doc.format.paragraph.setKeepOptions',
|
|
103
|
+
'doc.styles.getTheme',
|
|
102
104
|
'doc.styles.getCatalog',
|
|
103
105
|
'doc.styles.create',
|
|
104
106
|
'doc.comments.list',
|
|
@@ -109,6 +111,8 @@ const QUICK_REFERENCE_IDS = [
|
|
|
109
111
|
'doc.trackChanges.list',
|
|
110
112
|
'doc.trackChanges.get',
|
|
111
113
|
'doc.trackChanges.decide',
|
|
114
|
+
'doc.tables.listPresets',
|
|
115
|
+
'doc.tables.applyPreset',
|
|
112
116
|
'doc.tables.get',
|
|
113
117
|
'doc.tables.getProperties',
|
|
114
118
|
'doc.tables.getCells',
|
|
@@ -13,6 +13,7 @@ Recipes (ids for superdoc_api_call unless a helper is named):
|
|
|
13
13
|
- Footnote / endnote: superdoc_perform_action {action:"insert_footnote", anchorText:"Confidential Information", occurrence:1, content:"…", noteType:"footnote"}. The mark lands right after the phrase. Raw form: doc.footnotes.insert {type, content, at:{kind:"text", segments:[{blockId, range:{start,end}}]}}.
|
|
14
14
|
- Picture: superdoc_perform_action {action:"insert_image", src:"eval-logo.png", position:"after", anchorText:"…", caption:"Figure 1 — …"} (file path, URL, or data URI; omit position/anchorText for document end). Centre it afterwards with format_paragraph on the image's paragraph and the caption if asked. Fine-tuning: doc.images.setSize / setWrapType / setPosition / setAltText.
|
|
15
15
|
- Shade a header row / column / table: superdoc_perform_action {action:"shade_cells", tableOrdinal:1, rowIndex:0, color:"D9D9D9"}. Borders: doc.tables.setBorders / applyBorderPreset; cell props: doc.tables.setCellProperties (cell target only); widths/heights: setColumnWidth / setRowHeight / distributeRows / distributeColumns; merge/split: mergeCells / splitCell; sort; setStyle / applyStyle / setLayout.
|
|
16
|
+
- Built-in Word table style (Grid Table 4 – Accent 1, List Table 3, Plain Table 2, …): doc.tables.listPresets {source:"word", query:"Grid Table 4"} returns preset IDs (follow nextOffset for more), then doc.tables.applyPreset {nodeId, preset:<returned id>, styleOptions:{headerRow, bandedRows, firstColumn, …}}. Read doc.styles.getTheme to resolve appearance color refs against this document, including colorSchemeMapping for all roles and hex tint/shade; never assume an accent slot is green. Select header fill, shading banding (not border-only bands), and border layout from appearance. Missing properties inherit and direct formatting overrides style cues. Use the "-accent-N" preset for theme colours, never accentColor. Direct edit only. doc.tables.getStyles lists styles already in the document; applyStyle references one of those IDs.
|
|
16
17
|
- New table cells that must match neighbours: after insert_table_row / setCellText, read a sibling cell's run properties (superdoc_inspect with table runs) and apply them with doc.format.apply {target:{kind:"selection", start:{kind:"text", blockId:<cell paragraph>, offset:0}, end:{kind:"text", blockId, offset:<text length>}}, inline:{fontSize, fontFamily, bold, …}}. A new cell with default formatting next to 10pt siblings is a bug.
|
|
17
18
|
- Page layout: doc.sections.list first (sectionId), then doc.sections.setPageMargins / setPageSetup (size, orientation) / setColumns / setPageNumbering / setTitlePage / setOddEvenHeadersFooters / setPageBorders / setBreakType; doc.create.sectionBreak for a new section.
|
|
18
19
|
- Header / footer text: (1) doc.sections.list → sectionId; (2) doc.headerFooters.list → which slots exist and whether the slot is explicit; (3) if the slot has no explicit part: doc.headerFooters.parts.create {kind:"header"|"footer"} → refId, then doc.headerFooters.refs.set {target:{kind:"headerFooterSlot", section:{kind:"section", sectionId}, headerFooterKind, variant:"default"|"first"|"even"}, refId}; (4) write text with doc.create.paragraph {text, in:{kind:"story", storyType:"headerFooterSlot", section:{kind:"section", sectionId}, headerFooterKind, variant}, at:{kind:"documentEnd"}}; (5) page number: doc.fields.insert {mode:"raw", instruction:"PAGE", at:{kind:"text", story:<that same story>, segments:[{blockId:<the paragraph you created>, range:{start:N, end:N}}]}} where N is the offset inside that paragraph. Repeat for variant "first"/"even" when the document uses different first/even pages (headerFooters.list tells you).
|
package/dist/runtime/host.cjs
CHANGED
|
@@ -4,6 +4,7 @@ var node_child_process = require('node:child_process');
|
|
|
4
4
|
var transportCommon = require('./transport-common.cjs');
|
|
5
5
|
var errors = require('./errors.cjs');
|
|
6
6
|
var stateDirectory = require('./state-directory.cjs');
|
|
7
|
+
var notifications = require('./notifications.cjs');
|
|
7
8
|
var documentEvidence = require('./document-evidence.cjs');
|
|
8
9
|
var contract = require('../generated/contract.cjs');
|
|
9
10
|
var sdkPolicy = require('./sdk-policy.cjs');
|
|
@@ -11,6 +12,7 @@ var sdkPolicy = require('./sdk-policy.cjs');
|
|
|
11
12
|
const HOST_PROTOCOL_VERSION = sdkPolicy.SDK_POLICY.protocolVersion;
|
|
12
13
|
const CLI_HOST_REQUIRED_FEATURES = sdkPolicy.SDK_POLICY.cliRequiredFeatures;
|
|
13
14
|
const COLLABORATION_AUTH_PER_OPEN_FEATURE = sdkPolicy.SDK_POLICY.features.collaborationAuthPerOpen;
|
|
15
|
+
const IDENTITY_CLAIM_PER_OPEN_FEATURE = sdkPolicy.SDK_POLICY.features.identityClaimPerOpen;
|
|
14
16
|
const HOST_REQUEST_TIMEOUT_FEATURE = sdkPolicy.SDK_POLICY.features.requestTimeout;
|
|
15
17
|
const CHANGE_MODES = ['direct', 'tracked'];
|
|
16
18
|
const FORWARD_HOST_STDERR = typeof process !== 'undefined' && typeof process.env?.SUPERDOC_DEBUG_TEXT_REWRITE === 'string'
|
|
@@ -125,6 +127,7 @@ class HostTransport {
|
|
|
125
127
|
activeCalls = new Set();
|
|
126
128
|
hostFeatures = new Set();
|
|
127
129
|
cliLocalSessions = new Set();
|
|
130
|
+
notifications = new notifications.HostNotificationHub();
|
|
128
131
|
constructor(options) {
|
|
129
132
|
this.stateDirectory = new stateDirectory.SdkStateDirectory(options.stateDir);
|
|
130
133
|
this.hostBin = options.hostBin;
|
|
@@ -146,6 +149,9 @@ class HostTransport {
|
|
|
146
149
|
async connect() {
|
|
147
150
|
await this.runWhileActive(() => this.ensureConnected());
|
|
148
151
|
}
|
|
152
|
+
subscribe(options = {}) {
|
|
153
|
+
return this.notifications.subscribe(options);
|
|
154
|
+
}
|
|
149
155
|
supportsDocumentFacts(sessionId) {
|
|
150
156
|
return this.usesCliDocumentFacts(sessionId);
|
|
151
157
|
}
|
|
@@ -171,14 +177,18 @@ class HostTransport {
|
|
|
171
177
|
async dispose() {
|
|
172
178
|
if (this.disposePromise)
|
|
173
179
|
return this.disposePromise;
|
|
174
|
-
if (this.terminalDisposalError)
|
|
180
|
+
if (this.terminalDisposalError) {
|
|
181
|
+
this.notifications.close();
|
|
175
182
|
throw this.terminalDisposalError;
|
|
183
|
+
}
|
|
176
184
|
if (!this.child &&
|
|
177
185
|
!this.connecting &&
|
|
178
186
|
this.activeCalls.size === 0 &&
|
|
179
187
|
!this.stateDirectory.ownsRoot &&
|
|
180
|
-
this.stoppedChildren.size === 0)
|
|
188
|
+
this.stoppedChildren.size === 0) {
|
|
189
|
+
this.notifications.close();
|
|
181
190
|
return;
|
|
191
|
+
}
|
|
182
192
|
this.stopping = true;
|
|
183
193
|
const disposal = this.disposeAfterActiveCalls();
|
|
184
194
|
this.disposePromise = disposal;
|
|
@@ -192,6 +202,14 @@ class HostTransport {
|
|
|
192
202
|
}
|
|
193
203
|
}
|
|
194
204
|
async disposeAfterActiveCalls() {
|
|
205
|
+
try {
|
|
206
|
+
await this.shutdownAfterActiveCalls();
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
this.notifications.close();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async shutdownAfterActiveCalls() {
|
|
195
213
|
await Promise.allSettled(Array.from(this.activeCalls));
|
|
196
214
|
if (this.terminalDisposalError)
|
|
197
215
|
throw this.terminalDisposalError;
|
|
@@ -244,6 +262,12 @@ class HostTransport {
|
|
|
244
262
|
});
|
|
245
263
|
}
|
|
246
264
|
}
|
|
265
|
+
const identityClaim = options.identityClaim === undefined ? undefined : transportCommon.normalizeIdentityClaim(options.identityClaim);
|
|
266
|
+
if (identityClaim !== undefined && operation.operationId !== 'doc.open') {
|
|
267
|
+
throw new errors.SuperDocCliError('identityClaim is supported only for doc.open requests.', {
|
|
268
|
+
code: 'INVALID_ARGUMENT',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
247
271
|
await this.ensureConnected();
|
|
248
272
|
if (collaborationAuth !== undefined && !this.hostFeatures.has(COLLABORATION_AUTH_PER_OPEN_FEATURE)) {
|
|
249
273
|
throw new errors.SuperDocCliError('The connected CLI host does not support per-open collaboration authentication.', {
|
|
@@ -251,6 +275,12 @@ class HostTransport {
|
|
|
251
275
|
details: { feature: COLLABORATION_AUTH_PER_OPEN_FEATURE },
|
|
252
276
|
});
|
|
253
277
|
}
|
|
278
|
+
if (identityClaim !== undefined && !this.hostFeatures.has(IDENTITY_CLAIM_PER_OPEN_FEATURE)) {
|
|
279
|
+
throw new errors.SuperDocCliError('The connected CLI host does not support a per-open identity claim.', {
|
|
280
|
+
code: 'CAPABILITY_UNSUPPORTED',
|
|
281
|
+
details: { feature: IDENTITY_CLAIM_PER_OPEN_FEATURE },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
254
284
|
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : undefined;
|
|
255
285
|
const supportsRequestTimeoutMetadata = this.hostFeatures.has(HOST_REQUEST_TIMEOUT_FEATURE);
|
|
256
286
|
const cliHostRequestTimeoutMs = this.requestTimeoutMs ?? HOST_DEFAULT_REQUEST_TIMEOUT_MS;
|
|
@@ -288,6 +318,7 @@ class HostTransport {
|
|
|
288
318
|
}),
|
|
289
319
|
...(includeRevision ? { includeRevision: true } : {}),
|
|
290
320
|
...(collaborationAuth === undefined ? {} : { collaborationAuth }),
|
|
321
|
+
...(identityClaim === undefined ? {} : { identityClaim }),
|
|
291
322
|
}, watchdogTimeout, {
|
|
292
323
|
requestTimeoutMs: supportsRequestTimeoutMetadata ? options.timeoutMs : undefined,
|
|
293
324
|
});
|
|
@@ -511,8 +542,8 @@ class HostTransport {
|
|
|
511
542
|
if (record.jsonrpc !== '2.0') {
|
|
512
543
|
return;
|
|
513
544
|
}
|
|
514
|
-
// Notification (no id) — reserved for future eventing
|
|
515
545
|
if ('method' in record && !('id' in record)) {
|
|
546
|
+
this.notifications.publish(record);
|
|
516
547
|
return;
|
|
517
548
|
}
|
|
518
549
|
const idRaw = record.id;
|
|
@@ -616,6 +647,7 @@ class HostTransport {
|
|
|
616
647
|
}
|
|
617
648
|
handleDisconnect(error) {
|
|
618
649
|
this.cleanupProcess(error);
|
|
650
|
+
this.notifications.disconnect(error);
|
|
619
651
|
}
|
|
620
652
|
cleanupProcess(error) {
|
|
621
653
|
this.hostFeatures.clear();
|
|
@@ -644,6 +676,7 @@ class HostTransport {
|
|
|
644
676
|
|
|
645
677
|
exports.COLLABORATION_AUTH_PER_OPEN_FEATURE = COLLABORATION_AUTH_PER_OPEN_FEATURE;
|
|
646
678
|
exports.HostTransport = HostTransport;
|
|
679
|
+
exports.IDENTITY_CLAIM_PER_OPEN_FEATURE = IDENTITY_CLAIM_PER_OPEN_FEATURE;
|
|
647
680
|
exports.buildHostSpawnArgs = buildHostSpawnArgs;
|
|
648
681
|
exports.mapCliInvocationResult = mapCliInvocationResult;
|
|
649
682
|
exports.mapDocumentReplaceFileResult = mapDocumentReplaceFileResult;
|
package/dist/runtime/host.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type OperationSpec, type SuperDocClientOptions, type TransportInvokeOptions } from './transport-common.js';
|
|
2
|
+
import { type HostNotifications, type SubscribeOptions } from './notifications.js';
|
|
2
3
|
export declare const COLLABORATION_AUTH_PER_OPEN_FEATURE: string;
|
|
4
|
+
export declare const IDENTITY_CLAIM_PER_OPEN_FEATURE: string;
|
|
3
5
|
export declare function mapCliInvocationResult(operation: OperationSpec, value: unknown): unknown;
|
|
4
6
|
export declare function mapDocumentReplaceFileResult(response: unknown, sessionId: string): Record<string, unknown>;
|
|
5
7
|
/**
|
|
@@ -53,15 +55,18 @@ export declare class HostTransport {
|
|
|
53
55
|
private readonly activeCalls;
|
|
54
56
|
private readonly hostFeatures;
|
|
55
57
|
private readonly cliLocalSessions;
|
|
58
|
+
private readonly notifications;
|
|
56
59
|
constructor(options: {
|
|
57
60
|
hostBin: string;
|
|
58
61
|
} & SuperDocClientOptions);
|
|
59
62
|
connect(): Promise<void>;
|
|
63
|
+
subscribe(options?: SubscribeOptions): HostNotifications;
|
|
60
64
|
supportsDocumentFacts(sessionId: string): boolean;
|
|
61
65
|
usesCliDocumentFacts(sessionId: string): boolean;
|
|
62
66
|
readDocumentRevision(sessionId: string): Promise<string | undefined>;
|
|
63
67
|
dispose(): Promise<void>;
|
|
64
68
|
private disposeAfterActiveCalls;
|
|
69
|
+
private shutdownAfterActiveCalls;
|
|
65
70
|
invoke<TData = unknown>(operation: OperationSpec, params?: Record<string, unknown>, options?: TransportInvokeOptions): Promise<TData>;
|
|
66
71
|
replaceFile(sessionId: string, path: string, options?: TransportInvokeOptions): Promise<Record<string, unknown>>;
|
|
67
72
|
private invokeWhileActive;
|
package/dist/runtime/host.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { buildOperationArgv, captureMutationPlanAuthor, buildStructuredReadInput, normalizeCollaborationAuth, resolveInvocation, } from './transport-common.js';
|
|
2
|
+
import { buildOperationArgv, captureMutationPlanAuthor, buildStructuredReadInput, normalizeCollaborationAuth, normalizeIdentityClaim, resolveInvocation, } from './transport-common.js';
|
|
3
3
|
import { SuperDocCliError } from './errors.js';
|
|
4
4
|
import { SdkStateDirectory } from './state-directory.js';
|
|
5
|
+
import { HostNotificationHub } from './notifications.js';
|
|
5
6
|
import { registerMutationRevision } from './document-evidence.js';
|
|
6
7
|
import { CONTRACT } from '../generated/contract.js';
|
|
7
8
|
import { SDK_POLICY } from './sdk-policy.js';
|
|
8
9
|
const HOST_PROTOCOL_VERSION = SDK_POLICY.protocolVersion;
|
|
9
10
|
const CLI_HOST_REQUIRED_FEATURES = SDK_POLICY.cliRequiredFeatures;
|
|
10
11
|
export const COLLABORATION_AUTH_PER_OPEN_FEATURE = SDK_POLICY.features.collaborationAuthPerOpen;
|
|
12
|
+
export const IDENTITY_CLAIM_PER_OPEN_FEATURE = SDK_POLICY.features.identityClaimPerOpen;
|
|
11
13
|
const HOST_REQUEST_TIMEOUT_FEATURE = SDK_POLICY.features.requestTimeout;
|
|
12
14
|
const CHANGE_MODES = ['direct', 'tracked'];
|
|
13
15
|
const FORWARD_HOST_STDERR = typeof process !== 'undefined' && typeof process.env?.SUPERDOC_DEBUG_TEXT_REWRITE === 'string'
|
|
@@ -122,6 +124,7 @@ export class HostTransport {
|
|
|
122
124
|
activeCalls = new Set();
|
|
123
125
|
hostFeatures = new Set();
|
|
124
126
|
cliLocalSessions = new Set();
|
|
127
|
+
notifications = new HostNotificationHub();
|
|
125
128
|
constructor(options) {
|
|
126
129
|
this.stateDirectory = new SdkStateDirectory(options.stateDir);
|
|
127
130
|
this.hostBin = options.hostBin;
|
|
@@ -143,6 +146,9 @@ export class HostTransport {
|
|
|
143
146
|
async connect() {
|
|
144
147
|
await this.runWhileActive(() => this.ensureConnected());
|
|
145
148
|
}
|
|
149
|
+
subscribe(options = {}) {
|
|
150
|
+
return this.notifications.subscribe(options);
|
|
151
|
+
}
|
|
146
152
|
supportsDocumentFacts(sessionId) {
|
|
147
153
|
return this.usesCliDocumentFacts(sessionId);
|
|
148
154
|
}
|
|
@@ -168,14 +174,18 @@ export class HostTransport {
|
|
|
168
174
|
async dispose() {
|
|
169
175
|
if (this.disposePromise)
|
|
170
176
|
return this.disposePromise;
|
|
171
|
-
if (this.terminalDisposalError)
|
|
177
|
+
if (this.terminalDisposalError) {
|
|
178
|
+
this.notifications.close();
|
|
172
179
|
throw this.terminalDisposalError;
|
|
180
|
+
}
|
|
173
181
|
if (!this.child &&
|
|
174
182
|
!this.connecting &&
|
|
175
183
|
this.activeCalls.size === 0 &&
|
|
176
184
|
!this.stateDirectory.ownsRoot &&
|
|
177
|
-
this.stoppedChildren.size === 0)
|
|
185
|
+
this.stoppedChildren.size === 0) {
|
|
186
|
+
this.notifications.close();
|
|
178
187
|
return;
|
|
188
|
+
}
|
|
179
189
|
this.stopping = true;
|
|
180
190
|
const disposal = this.disposeAfterActiveCalls();
|
|
181
191
|
this.disposePromise = disposal;
|
|
@@ -189,6 +199,14 @@ export class HostTransport {
|
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
201
|
async disposeAfterActiveCalls() {
|
|
202
|
+
try {
|
|
203
|
+
await this.shutdownAfterActiveCalls();
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
this.notifications.close();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async shutdownAfterActiveCalls() {
|
|
192
210
|
await Promise.allSettled(Array.from(this.activeCalls));
|
|
193
211
|
if (this.terminalDisposalError)
|
|
194
212
|
throw this.terminalDisposalError;
|
|
@@ -241,6 +259,12 @@ export class HostTransport {
|
|
|
241
259
|
});
|
|
242
260
|
}
|
|
243
261
|
}
|
|
262
|
+
const identityClaim = options.identityClaim === undefined ? undefined : normalizeIdentityClaim(options.identityClaim);
|
|
263
|
+
if (identityClaim !== undefined && operation.operationId !== 'doc.open') {
|
|
264
|
+
throw new SuperDocCliError('identityClaim is supported only for doc.open requests.', {
|
|
265
|
+
code: 'INVALID_ARGUMENT',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
244
268
|
await this.ensureConnected();
|
|
245
269
|
if (collaborationAuth !== undefined && !this.hostFeatures.has(COLLABORATION_AUTH_PER_OPEN_FEATURE)) {
|
|
246
270
|
throw new SuperDocCliError('The connected CLI host does not support per-open collaboration authentication.', {
|
|
@@ -248,6 +272,12 @@ export class HostTransport {
|
|
|
248
272
|
details: { feature: COLLABORATION_AUTH_PER_OPEN_FEATURE },
|
|
249
273
|
});
|
|
250
274
|
}
|
|
275
|
+
if (identityClaim !== undefined && !this.hostFeatures.has(IDENTITY_CLAIM_PER_OPEN_FEATURE)) {
|
|
276
|
+
throw new SuperDocCliError('The connected CLI host does not support a per-open identity claim.', {
|
|
277
|
+
code: 'CAPABILITY_UNSUPPORTED',
|
|
278
|
+
details: { feature: IDENTITY_CLAIM_PER_OPEN_FEATURE },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
251
281
|
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : undefined;
|
|
252
282
|
const supportsRequestTimeoutMetadata = this.hostFeatures.has(HOST_REQUEST_TIMEOUT_FEATURE);
|
|
253
283
|
const cliHostRequestTimeoutMs = this.requestTimeoutMs ?? HOST_DEFAULT_REQUEST_TIMEOUT_MS;
|
|
@@ -285,6 +315,7 @@ export class HostTransport {
|
|
|
285
315
|
}),
|
|
286
316
|
...(includeRevision ? { includeRevision: true } : {}),
|
|
287
317
|
...(collaborationAuth === undefined ? {} : { collaborationAuth }),
|
|
318
|
+
...(identityClaim === undefined ? {} : { identityClaim }),
|
|
288
319
|
}, watchdogTimeout, {
|
|
289
320
|
requestTimeoutMs: supportsRequestTimeoutMetadata ? options.timeoutMs : undefined,
|
|
290
321
|
});
|
|
@@ -508,8 +539,8 @@ export class HostTransport {
|
|
|
508
539
|
if (record.jsonrpc !== '2.0') {
|
|
509
540
|
return;
|
|
510
541
|
}
|
|
511
|
-
// Notification (no id) — reserved for future eventing
|
|
512
542
|
if ('method' in record && !('id' in record)) {
|
|
543
|
+
this.notifications.publish(record);
|
|
513
544
|
return;
|
|
514
545
|
}
|
|
515
546
|
const idRaw = record.id;
|
|
@@ -613,6 +644,7 @@ export class HostTransport {
|
|
|
613
644
|
}
|
|
614
645
|
handleDisconnect(error) {
|
|
615
646
|
this.cleanupProcess(error);
|
|
647
|
+
this.notifications.disconnect(error);
|
|
616
648
|
}
|
|
617
649
|
cleanupProcess(error) {
|
|
618
650
|
this.hostFeatures.clear();
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var errors = require('./errors.cjs');
|
|
4
|
+
|
|
5
|
+
const NOTIFICATIONS_LAGGED = 'NOTIFICATIONS_LAGGED';
|
|
6
|
+
const DEFAULT_NOTIFICATION_CAPACITY = 64;
|
|
7
|
+
const LAGGED_MESSAGE = 'resynchronize document state before using notification-derived caches';
|
|
8
|
+
const RESYNC_HINT = 'Call changes.list with the cursor of the last change record you processed.';
|
|
9
|
+
class HostNotifications {
|
|
10
|
+
capacity;
|
|
11
|
+
detach;
|
|
12
|
+
queue = [];
|
|
13
|
+
waiters = [];
|
|
14
|
+
skipped = 0;
|
|
15
|
+
failure = null;
|
|
16
|
+
closed = false;
|
|
17
|
+
constructor(capacity, detach) {
|
|
18
|
+
this.capacity = capacity;
|
|
19
|
+
this.detach = detach;
|
|
20
|
+
}
|
|
21
|
+
next() {
|
|
22
|
+
if (this.skipped > 0) {
|
|
23
|
+
const skipped = this.skipped;
|
|
24
|
+
this.skipped = 0;
|
|
25
|
+
return Promise.reject(new errors.SuperDocCliError(LAGGED_MESSAGE, {
|
|
26
|
+
code: NOTIFICATIONS_LAGGED,
|
|
27
|
+
details: { skipped, resync: RESYNC_HINT },
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
const notification = this.queue.shift();
|
|
31
|
+
if (notification)
|
|
32
|
+
return Promise.resolve({ done: false, value: notification });
|
|
33
|
+
if (this.failure) {
|
|
34
|
+
const failure = this.failure;
|
|
35
|
+
this.failure = null;
|
|
36
|
+
return Promise.reject(failure);
|
|
37
|
+
}
|
|
38
|
+
if (this.closed)
|
|
39
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
40
|
+
return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
|
|
41
|
+
}
|
|
42
|
+
return() {
|
|
43
|
+
this.close();
|
|
44
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
45
|
+
}
|
|
46
|
+
close() {
|
|
47
|
+
if (this.closed)
|
|
48
|
+
return;
|
|
49
|
+
this.closed = true;
|
|
50
|
+
this.queue.length = 0;
|
|
51
|
+
this.skipped = 0;
|
|
52
|
+
this.failure = null;
|
|
53
|
+
this.detach(this);
|
|
54
|
+
for (const waiter of this.waiters.splice(0))
|
|
55
|
+
waiter.resolve({ done: true, value: undefined });
|
|
56
|
+
}
|
|
57
|
+
[Symbol.asyncIterator]() {
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
get isClosed() {
|
|
61
|
+
return this.closed;
|
|
62
|
+
}
|
|
63
|
+
deliver(notification) {
|
|
64
|
+
if (this.closed)
|
|
65
|
+
return;
|
|
66
|
+
const waiter = this.waiters.shift();
|
|
67
|
+
if (waiter) {
|
|
68
|
+
waiter.resolve({ done: false, value: notification });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (this.queue.length >= this.capacity) {
|
|
72
|
+
this.queue.shift();
|
|
73
|
+
this.skipped += 1;
|
|
74
|
+
}
|
|
75
|
+
this.queue.push(notification);
|
|
76
|
+
}
|
|
77
|
+
fail(error) {
|
|
78
|
+
if (this.closed)
|
|
79
|
+
return;
|
|
80
|
+
const waiter = this.waiters.shift();
|
|
81
|
+
if (waiter) {
|
|
82
|
+
waiter.reject(error);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.failure = error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
class HostNotificationHub {
|
|
89
|
+
subscriptions = new Set();
|
|
90
|
+
subscribe(options = {}) {
|
|
91
|
+
const capacity = options.capacity ?? DEFAULT_NOTIFICATION_CAPACITY;
|
|
92
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1) {
|
|
93
|
+
throw new errors.SuperDocCliError('Notification capacity must be a positive integer.', {
|
|
94
|
+
code: 'INVALID_ARGUMENT',
|
|
95
|
+
details: { capacity },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const subscription = new HostNotifications(capacity, (closed) => this.subscriptions.delete(closed));
|
|
99
|
+
this.subscriptions.add(subscription);
|
|
100
|
+
return subscription;
|
|
101
|
+
}
|
|
102
|
+
publish(frame) {
|
|
103
|
+
if (this.subscriptions.size === 0 || typeof frame.method !== 'string')
|
|
104
|
+
return;
|
|
105
|
+
const notification = frame.params === undefined
|
|
106
|
+
? { jsonrpc: '2.0', method: frame.method }
|
|
107
|
+
: { jsonrpc: '2.0', method: frame.method, params: frame.params };
|
|
108
|
+
for (const subscription of this.subscriptions)
|
|
109
|
+
subscription.deliver(notification);
|
|
110
|
+
}
|
|
111
|
+
disconnect(error) {
|
|
112
|
+
for (const subscription of this.subscriptions)
|
|
113
|
+
subscription.fail(error);
|
|
114
|
+
}
|
|
115
|
+
close() {
|
|
116
|
+
for (const subscription of [...this.subscriptions])
|
|
117
|
+
subscription.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
exports.DEFAULT_NOTIFICATION_CAPACITY = DEFAULT_NOTIFICATION_CAPACITY;
|
|
122
|
+
exports.HostNotificationHub = HostNotificationHub;
|
|
123
|
+
exports.HostNotifications = HostNotifications;
|
|
124
|
+
exports.NOTIFICATIONS_LAGGED = NOTIFICATIONS_LAGGED;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SuperDocCliError } from './errors.js';
|
|
2
|
+
export declare const NOTIFICATIONS_LAGGED = "NOTIFICATIONS_LAGGED";
|
|
3
|
+
export declare const DEFAULT_NOTIFICATION_CAPACITY = 64;
|
|
4
|
+
export interface HostNotification {
|
|
5
|
+
readonly jsonrpc: '2.0';
|
|
6
|
+
readonly method: string;
|
|
7
|
+
readonly params?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface SubscribeOptions {
|
|
10
|
+
readonly capacity?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class HostNotifications implements AsyncIterableIterator<HostNotification> {
|
|
13
|
+
private readonly capacity;
|
|
14
|
+
private readonly detach;
|
|
15
|
+
private readonly queue;
|
|
16
|
+
private readonly waiters;
|
|
17
|
+
private skipped;
|
|
18
|
+
private failure;
|
|
19
|
+
private closed;
|
|
20
|
+
constructor(capacity: number, detach: (subscription: HostNotifications) => void);
|
|
21
|
+
next(): Promise<IteratorResult<HostNotification, undefined>>;
|
|
22
|
+
return(): Promise<IteratorResult<HostNotification, undefined>>;
|
|
23
|
+
close(): void;
|
|
24
|
+
[Symbol.asyncIterator](): this;
|
|
25
|
+
get isClosed(): boolean;
|
|
26
|
+
deliver(notification: HostNotification): void;
|
|
27
|
+
fail(error: SuperDocCliError): void;
|
|
28
|
+
}
|
|
29
|
+
export declare class HostNotificationHub {
|
|
30
|
+
private readonly subscriptions;
|
|
31
|
+
subscribe(options?: SubscribeOptions): HostNotifications;
|
|
32
|
+
publish(frame: Record<string, unknown>): void;
|
|
33
|
+
disconnect(error: SuperDocCliError): void;
|
|
34
|
+
close(): void;
|
|
35
|
+
}
|