@superdoc/sdk 2.26.0 → 2.27.0-next.19

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.
Files changed (45) hide show
  1. package/README.md +236 -29
  2. package/dist/agent/actions.cjs +62 -24
  3. package/dist/agent/actions.js +62 -24
  4. package/dist/agent/catalog.d.ts +0 -4
  5. package/dist/agent/catalog.js +0 -4
  6. package/dist/embedded-prompts.generated.cjs +1 -1
  7. package/dist/embedded-prompts.generated.js +1 -1
  8. package/dist/embedded-tools.generated.cjs +5 -5
  9. package/dist/embedded-tools.generated.js +5 -5
  10. package/dist/generated/client.cjs +18 -1
  11. package/dist/generated/client.d.ts +3250 -1019
  12. package/dist/generated/client.js +18 -1
  13. package/dist/generated/contract.cjs +1 -2
  14. package/dist/generated/contract.d.ts +1 -0
  15. package/dist/generated/contract.js +1 -2
  16. package/dist/index.cjs +3 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.js +3 -0
  19. package/dist/presets/full.cjs +4 -0
  20. package/dist/presets/full.js +4 -0
  21. package/dist/prompts/full-addendum.md +1 -0
  22. package/dist/runtime/host.cjs +36 -3
  23. package/dist/runtime/host.d.ts +5 -0
  24. package/dist/runtime/host.js +36 -4
  25. package/dist/runtime/notifications.cjs +124 -0
  26. package/dist/runtime/notifications.d.ts +35 -0
  27. package/dist/runtime/notifications.js +116 -0
  28. package/dist/runtime/process.cjs +4 -1
  29. package/dist/runtime/process.d.ts +2 -0
  30. package/dist/runtime/process.js +4 -1
  31. package/dist/runtime/sdk-policy.d.ts +1 -0
  32. package/dist/runtime/sdk-version.generated.cjs +1 -1
  33. package/dist/runtime/sdk-version.generated.d.ts +1 -1
  34. package/dist/runtime/sdk-version.generated.js +1 -1
  35. package/dist/runtime/transport-common.cjs +9 -1
  36. package/dist/runtime/transport-common.d.ts +3 -0
  37. package/dist/runtime/transport-common.js +8 -1
  38. package/package.json +7 -7
  39. package/tools/catalog.json +42 -2
  40. package/tools/generation-receipt.json +60 -22
  41. package/tools/tools-policy.json +1 -1
  42. package/tools/tools.anthropic.json +42 -2
  43. package/tools/tools.generic.json +42 -2
  44. package/tools/tools.openai.json +42 -2
  45. package/tools/tools.vercel.json +42 -2
package/dist/index.cjs CHANGED
@@ -152,6 +152,9 @@ class SuperDocClient {
152
152
  async connect() {
153
153
  await this.runtime.connect();
154
154
  }
155
+ subscribe(options = {}) {
156
+ return this.runtime.subscribe(options);
157
+ }
155
158
  /**
156
159
  * Open a document and return a bound document handle.
157
160
  *
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
@@ -140,6 +140,9 @@ export class SuperDocClient {
140
140
  async connect() {
141
141
  await this.runtime.connect();
142
142
  }
143
+ subscribe(options = {}) {
144
+ return this.runtime.subscribe(options);
145
+ }
143
146
  /**
144
147
  * Open a document and return a bound document handle.
145
148
  *
@@ -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',
@@ -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).
@@ -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;
@@ -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;
@@ -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
+ }