@superdoc/sdk 2.13.0-next.9 → 2.13.1-next.1

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/dist/index.cjs CHANGED
@@ -10,6 +10,7 @@ var skills = require('./skills.cjs');
10
10
  var tools = require('./tools.cjs');
11
11
  var intentDispatch_generated = require('./generated/intent-dispatch.generated.cjs');
12
12
  var v2PresetCompat = require('./agent/v2-preset-compat.cjs');
13
+ var actions = require('./agent/actions.cjs');
13
14
  var define = require('./actions/define.cjs');
14
15
  var presets = require('./presets.cjs');
15
16
 
@@ -216,6 +217,7 @@ exports.getToolCatalog = tools.getToolCatalog;
216
217
  exports.listTools = tools.listTools;
217
218
  exports.dispatchIntentTool = intentDispatch_generated.dispatchIntentTool;
218
219
  exports.wrapV2PresetCompat = v2PresetCompat.wrapV2PresetCompat;
220
+ exports.getActionDispositions = actions.getActionDispositions;
219
221
  exports.defineAction = define.defineAction;
220
222
  exports.DEFAULT_PRESET = presets.DEFAULT_PRESET;
221
223
  exports.getPreset = presets.getPreset;
package/dist/index.d.ts CHANGED
@@ -93,7 +93,8 @@ export type { AnthropicSystemPrompt, CacheStrategy, SystemPromptForProviderResul
93
93
  export { dispatchIntentTool } from './generated/intent-dispatch.generated.js';
94
94
  export { wrapV2PresetCompat } from './agent/v2-preset-compat.js';
95
95
  export type { BoundDocApi } from './generated/client.js';
96
- export type { ActionName } from './agent/actions.js';
96
+ export type { ActionName, ActionDisposition } from './agent/actions.js';
97
+ export { getActionDispositions } from './agent/actions.js';
97
98
  export type { AgentReceipt, AgentApplyArgs, AgentVerifyArgs, AgentInspectResult } from './agent/runtime.js';
98
99
  export type { GetSystemPromptOptions, GetToolsOptions, GetToolsResult, PresetDescriptor } from './presets.js';
99
100
  export { createAgentToolkit } from './tools.js';
package/dist/index.js CHANGED
@@ -196,6 +196,7 @@ export { getSkill, installSkill, listSkills } from './skills.js';
196
196
  export { chooseTools, dispatchSuperDocTool, getMcpPrompt, getSystemPrompt, getSystemPromptForProvider, getToolCatalog, listTools, DEFAULT_PRESET, getPreset, listPresets, registerPreset, unregisterPreset, } from './tools.js';
197
197
  export { dispatchIntentTool } from './generated/intent-dispatch.generated.js';
198
198
  export { wrapV2PresetCompat } from './agent/v2-preset-compat.js';
199
+ export { getActionDispositions } from './agent/actions.js';
199
200
  export { createAgentToolkit } from './tools.js';
200
201
  // Custom-action authoring kit: `defineAction` builds an ActionSpec (steps or
201
202
  // run tier); hand the specs to `createAgentToolkit({ actions })`.
@@ -370,8 +370,15 @@ function applyCacheMarkers(tools, provider, cacheRequested) {
370
370
  // ---------------------------------------------------------------------------
371
371
  // PresetDescriptor surface
372
372
  // ---------------------------------------------------------------------------
373
+ function policyOptionsOf(options) {
374
+ return {
375
+ excludeActions: options?.excludeActions,
376
+ enforceChangeMode: options?.enforceChangeMode,
377
+ allowDirectActions: options?.allowDirectActions,
378
+ };
379
+ }
373
380
  async function coreGetTools(provider, options) {
374
- const tools = catalog.listAgentTools(provider, { excludeActions: options?.excludeActions });
381
+ const tools = catalog.listAgentTools(provider, policyOptionsOf(options));
375
382
  return applyCacheMarkers(tools, provider, options?.cache === true);
376
383
  }
377
384
  async function coreGetCatalog() {
@@ -389,6 +396,7 @@ async function coreGetCatalog() {
389
396
  generatedAt: null,
390
397
  toolCount: tools.length,
391
398
  tools,
399
+ dispositions: { ...actions.ACTION_DISPOSITIONS },
392
400
  };
393
401
  }
394
402
  /**
@@ -467,7 +475,7 @@ function stripExcludedActionLines(prompt, excludedActions) {
467
475
  .join('\n');
468
476
  }
469
477
  async function coreGetSystemPrompt(options) {
470
- const { excludedActions } = catalog.validateExclusions({ excludeActions: options?.excludeActions });
478
+ const { excludedActions } = catalog.resolveActionPolicy(policyOptionsOf(options));
471
479
  // The core preset is ACTIONS-ONLY: superdoc_execute_code is WIP and not
472
480
  // advertised, and the prompt documents only the action surface (this exact
473
481
  // prompt is what the eval suite validates). Code-execution guidance will be
@@ -483,6 +491,17 @@ async function coreGetMcpPrompt() {
483
491
  }
484
492
  return _mcpPromptCache;
485
493
  }
494
+ /**
495
+ * Dispatchable-but-unadvertised tools that route around the action layer and
496
+ * therefore around the tracked-changes policy. Refused outright while
497
+ * enforcement is on; a tracked-aware agent_apply may lift this later.
498
+ */
499
+ const ENFORCEMENT_SIDE_DOORS = new Set([
500
+ 'agent_apply',
501
+ 'agent_operation',
502
+ 'superdoc_execute_code',
503
+ ]);
504
+ const POLICY_OPTION_KEYS = ['excludeActions', 'enforceChangeMode', 'allowDirectActions'];
486
505
  async function coreDispatch(documentHandle, toolName, args, invokeOptions) {
487
506
  if (!isRecord(args)) {
488
507
  throw new errors.SuperDocCliError(`Tool arguments for ${toolName} must be an object.`, {
@@ -497,26 +516,69 @@ async function coreDispatch(documentHandle, toolName, args, invokeOptions) {
497
516
  details: { toolName },
498
517
  });
499
518
  }
500
- // Defense-in-depth for exclusion configs: a host that narrowed the advertised
501
- // surface (getTools excludeActions) can pass the same list here so a
502
- // guessed/injected call to an excluded action is refused, not run.
503
- const exclusionOptions = invokeOptions;
504
- if (toolName === 'superdoc_perform_action' &&
505
- typeof sanitizedArgs.action === 'string' &&
506
- exclusionOptions?.excludeActions?.includes(sanitizedArgs.action)) {
507
- throw new errors.SuperDocCliError(`Action ${sanitizedArgs.action} is excluded by configuration.`, {
519
+ // Defense-in-depth for narrowed surfaces: a host that narrowed the advertised
520
+ // tools (excludeActions / enforceChangeMode) passes the same options here so
521
+ // a guessed or injected call to a hidden action is refused, not run.
522
+ const policyOptions = invokeOptions;
523
+ // Unknown names are tolerated here: a preset that extends core (custom
524
+ // actions) forwards one merged exclusion list, and its own action names are
525
+ // not core's to validate. getTools/getSystemPrompt still validate.
526
+ const policy = catalog.resolveActionPolicy(policyOptionsOf(policyOptions), { ignoreUnknownActions: true });
527
+ const enforcing = policy.enforceChangeMode === 'tracked';
528
+ if (enforcing && ENFORCEMENT_SIDE_DOORS.has(toolName)) {
529
+ throw new errors.SuperDocCliError(`${toolName} bypasses the tracked-changes policy and is refused under enforceChangeMode: "tracked".`, {
508
530
  code: 'INVALID_ARGUMENT',
509
- details: { toolName, action: sanitizedArgs.action, excluded: true },
531
+ details: { toolName, reason: 'enforce-change-mode' },
510
532
  });
511
533
  }
534
+ let dispatchArgs = sanitizedArgs;
535
+ let changeModeForced = false;
536
+ if (toolName === 'superdoc_perform_action' && typeof sanitizedArgs.action === 'string') {
537
+ const action = sanitizedArgs.action;
538
+ if (policy.configuredExclusions.has(action)) {
539
+ throw new errors.SuperDocCliError(`Action ${action} is excluded by configuration.`, {
540
+ code: 'INVALID_ARGUMENT',
541
+ details: { toolName, action, excluded: true, reason: 'excluded' },
542
+ });
543
+ }
544
+ if (policy.directOnlyExclusions.has(action)) {
545
+ throw new errors.SuperDocCliError(`Action ${action} edits the document directly and is refused under enforceChangeMode: "tracked". Pass it in allowDirectActions to permit it.`, {
546
+ code: 'INVALID_ARGUMENT',
547
+ details: { toolName, action, excluded: true, reason: 'direct-only' },
548
+ });
549
+ }
550
+ if (enforcing && actions.isActionName(action)) {
551
+ if (actions.ACTION_DISPOSITIONS[action] === 'tracked') {
552
+ // Omitted or explicit "direct" both upgrade: the policy decides the
553
+ // mode, the model only picks the verb.
554
+ changeModeForced = sanitizedArgs.changeMode !== 'tracked';
555
+ dispatchArgs = { ...sanitizedArgs, changeMode: 'tracked' };
556
+ }
557
+ else {
558
+ // An allowlisted direct-only action runs direct by definition. A stray
559
+ // changeMode:"tracked" from the model would make move_range fail (it
560
+ // refuses tracked) and the receipt lie, so normalize it away.
561
+ changeModeForced = sanitizedArgs.changeMode === 'tracked';
562
+ const { changeMode: _requested, ...rest } = sanitizedArgs;
563
+ dispatchArgs = rest;
564
+ }
565
+ }
566
+ }
512
567
  let forwardOptions = invokeOptions;
513
- if (exclusionOptions && 'excludeActions' in exclusionOptions) {
514
- const { excludeActions: _ea, ...rest } = exclusionOptions;
568
+ if (policyOptions && POLICY_OPTION_KEYS.some((key) => key in policyOptions)) {
569
+ const { excludeActions: _ea, enforceChangeMode: _ecm, allowDirectActions: _ada, ...rest } = policyOptions;
515
570
  forwardOptions = rest;
516
571
  }
517
572
  if (catalog.isAgentToolName(toolName)) {
518
- validateAgentToolArgs(toolName, sanitizedArgs);
519
- return dispatchAgentTool(documentHandle, toolName, sanitizedArgs, forwardOptions);
573
+ validateAgentToolArgs(toolName, dispatchArgs);
574
+ const result = await dispatchAgentTool(documentHandle, toolName, dispatchArgs, forwardOptions);
575
+ if (enforcing && toolName === 'superdoc_perform_action' && isRecord(result)) {
576
+ // Receipts under enforcement state the mode that actually ran, so a host
577
+ // can audit "was this edit tracked?" without re-deriving the policy.
578
+ const changeMode = dispatchArgs.changeMode === 'tracked' ? 'tracked' : 'direct';
579
+ return { ...result, changeMode, changeModeForced };
580
+ }
581
+ return result;
520
582
  }
521
583
  throw new errors.SuperDocCliError(`Unknown tool: ${toolName}`, {
522
584
  code: 'TOOL_DISPATCH_NOT_FOUND',
@@ -18,9 +18,9 @@ import path from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { SuperDocCliError } from '../runtime/errors.js';
20
20
  import { writeSdkDebugTrace } from '../runtime/debug-trace.js';
21
- import { AGENT_TOOL_DEFINITIONS, isAgentToolName, listAgentTools, validateExclusions, PUBLIC_AGENT_TOOL_NAMES, } from '../agent/catalog.js';
21
+ import { AGENT_TOOL_DEFINITIONS, isAgentToolName, listAgentTools, resolveActionPolicy, PUBLIC_AGENT_TOOL_NAMES, } from '../agent/catalog.js';
22
22
  import { agentApply, agentInspect, agentOperation, agentVerify } from '../agent/runtime.js';
23
- import { superdocPerformAction } from '../agent/actions.js';
23
+ import { ACTION_DISPOSITIONS, isActionName, superdocPerformAction } from '../agent/actions.js';
24
24
  import { EMBEDDED_PROMPTS } from '../embedded-prompts.generated.js';
25
25
  // Prompts ship as <dist>/prompts/*.md. Resolve relative to the compiled
26
26
  // module, but the module's depth differs by build: in the SDK package this
@@ -366,8 +366,15 @@ function applyCacheMarkers(tools, provider, cacheRequested) {
366
366
  // ---------------------------------------------------------------------------
367
367
  // PresetDescriptor surface
368
368
  // ---------------------------------------------------------------------------
369
+ function policyOptionsOf(options) {
370
+ return {
371
+ excludeActions: options?.excludeActions,
372
+ enforceChangeMode: options?.enforceChangeMode,
373
+ allowDirectActions: options?.allowDirectActions,
374
+ };
375
+ }
369
376
  async function coreGetTools(provider, options) {
370
- const tools = listAgentTools(provider, { excludeActions: options?.excludeActions });
377
+ const tools = listAgentTools(provider, policyOptionsOf(options));
371
378
  return applyCacheMarkers(tools, provider, options?.cache === true);
372
379
  }
373
380
  async function coreGetCatalog() {
@@ -385,6 +392,7 @@ async function coreGetCatalog() {
385
392
  generatedAt: null,
386
393
  toolCount: tools.length,
387
394
  tools,
395
+ dispositions: { ...ACTION_DISPOSITIONS },
388
396
  };
389
397
  }
390
398
  /**
@@ -463,7 +471,7 @@ function stripExcludedActionLines(prompt, excludedActions) {
463
471
  .join('\n');
464
472
  }
465
473
  async function coreGetSystemPrompt(options) {
466
- const { excludedActions } = validateExclusions({ excludeActions: options?.excludeActions });
474
+ const { excludedActions } = resolveActionPolicy(policyOptionsOf(options));
467
475
  // The core preset is ACTIONS-ONLY: superdoc_execute_code is WIP and not
468
476
  // advertised, and the prompt documents only the action surface (this exact
469
477
  // prompt is what the eval suite validates). Code-execution guidance will be
@@ -479,6 +487,17 @@ async function coreGetMcpPrompt() {
479
487
  }
480
488
  return _mcpPromptCache;
481
489
  }
490
+ /**
491
+ * Dispatchable-but-unadvertised tools that route around the action layer and
492
+ * therefore around the tracked-changes policy. Refused outright while
493
+ * enforcement is on; a tracked-aware agent_apply may lift this later.
494
+ */
495
+ const ENFORCEMENT_SIDE_DOORS = new Set([
496
+ 'agent_apply',
497
+ 'agent_operation',
498
+ 'superdoc_execute_code',
499
+ ]);
500
+ const POLICY_OPTION_KEYS = ['excludeActions', 'enforceChangeMode', 'allowDirectActions'];
482
501
  async function coreDispatch(documentHandle, toolName, args, invokeOptions) {
483
502
  if (!isRecord(args)) {
484
503
  throw new SuperDocCliError(`Tool arguments for ${toolName} must be an object.`, {
@@ -493,26 +512,69 @@ async function coreDispatch(documentHandle, toolName, args, invokeOptions) {
493
512
  details: { toolName },
494
513
  });
495
514
  }
496
- // Defense-in-depth for exclusion configs: a host that narrowed the advertised
497
- // surface (getTools excludeActions) can pass the same list here so a
498
- // guessed/injected call to an excluded action is refused, not run.
499
- const exclusionOptions = invokeOptions;
500
- if (toolName === 'superdoc_perform_action' &&
501
- typeof sanitizedArgs.action === 'string' &&
502
- exclusionOptions?.excludeActions?.includes(sanitizedArgs.action)) {
503
- throw new SuperDocCliError(`Action ${sanitizedArgs.action} is excluded by configuration.`, {
515
+ // Defense-in-depth for narrowed surfaces: a host that narrowed the advertised
516
+ // tools (excludeActions / enforceChangeMode) passes the same options here so
517
+ // a guessed or injected call to a hidden action is refused, not run.
518
+ const policyOptions = invokeOptions;
519
+ // Unknown names are tolerated here: a preset that extends core (custom
520
+ // actions) forwards one merged exclusion list, and its own action names are
521
+ // not core's to validate. getTools/getSystemPrompt still validate.
522
+ const policy = resolveActionPolicy(policyOptionsOf(policyOptions), { ignoreUnknownActions: true });
523
+ const enforcing = policy.enforceChangeMode === 'tracked';
524
+ if (enforcing && ENFORCEMENT_SIDE_DOORS.has(toolName)) {
525
+ throw new SuperDocCliError(`${toolName} bypasses the tracked-changes policy and is refused under enforceChangeMode: "tracked".`, {
504
526
  code: 'INVALID_ARGUMENT',
505
- details: { toolName, action: sanitizedArgs.action, excluded: true },
527
+ details: { toolName, reason: 'enforce-change-mode' },
506
528
  });
507
529
  }
530
+ let dispatchArgs = sanitizedArgs;
531
+ let changeModeForced = false;
532
+ if (toolName === 'superdoc_perform_action' && typeof sanitizedArgs.action === 'string') {
533
+ const action = sanitizedArgs.action;
534
+ if (policy.configuredExclusions.has(action)) {
535
+ throw new SuperDocCliError(`Action ${action} is excluded by configuration.`, {
536
+ code: 'INVALID_ARGUMENT',
537
+ details: { toolName, action, excluded: true, reason: 'excluded' },
538
+ });
539
+ }
540
+ if (policy.directOnlyExclusions.has(action)) {
541
+ throw new SuperDocCliError(`Action ${action} edits the document directly and is refused under enforceChangeMode: "tracked". Pass it in allowDirectActions to permit it.`, {
542
+ code: 'INVALID_ARGUMENT',
543
+ details: { toolName, action, excluded: true, reason: 'direct-only' },
544
+ });
545
+ }
546
+ if (enforcing && isActionName(action)) {
547
+ if (ACTION_DISPOSITIONS[action] === 'tracked') {
548
+ // Omitted or explicit "direct" both upgrade: the policy decides the
549
+ // mode, the model only picks the verb.
550
+ changeModeForced = sanitizedArgs.changeMode !== 'tracked';
551
+ dispatchArgs = { ...sanitizedArgs, changeMode: 'tracked' };
552
+ }
553
+ else {
554
+ // An allowlisted direct-only action runs direct by definition. A stray
555
+ // changeMode:"tracked" from the model would make move_range fail (it
556
+ // refuses tracked) and the receipt lie, so normalize it away.
557
+ changeModeForced = sanitizedArgs.changeMode === 'tracked';
558
+ const { changeMode: _requested, ...rest } = sanitizedArgs;
559
+ dispatchArgs = rest;
560
+ }
561
+ }
562
+ }
508
563
  let forwardOptions = invokeOptions;
509
- if (exclusionOptions && 'excludeActions' in exclusionOptions) {
510
- const { excludeActions: _ea, ...rest } = exclusionOptions;
564
+ if (policyOptions && POLICY_OPTION_KEYS.some((key) => key in policyOptions)) {
565
+ const { excludeActions: _ea, enforceChangeMode: _ecm, allowDirectActions: _ada, ...rest } = policyOptions;
511
566
  forwardOptions = rest;
512
567
  }
513
568
  if (isAgentToolName(toolName)) {
514
- validateAgentToolArgs(toolName, sanitizedArgs);
515
- return dispatchAgentTool(documentHandle, toolName, sanitizedArgs, forwardOptions);
569
+ validateAgentToolArgs(toolName, dispatchArgs);
570
+ const result = await dispatchAgentTool(documentHandle, toolName, dispatchArgs, forwardOptions);
571
+ if (enforcing && toolName === 'superdoc_perform_action' && isRecord(result)) {
572
+ // Receipts under enforcement state the mode that actually ran, so a host
573
+ // can audit "was this edit tracked?" without re-deriving the policy.
574
+ const changeMode = dispatchArgs.changeMode === 'tracked' ? 'tracked' : 'direct';
575
+ return { ...result, changeMode, changeModeForced };
576
+ }
577
+ return result;
516
578
  }
517
579
  throw new SuperDocCliError(`Unknown tool: ${toolName}`, {
518
580
  code: 'TOOL_DISPATCH_NOT_FOUND',
package/dist/presets.d.ts CHANGED
@@ -72,6 +72,12 @@ export type ToolCatalog = {
72
72
  generatedAt: string | null;
73
73
  toolCount: number;
74
74
  tools: ToolCatalogEntry[];
75
+ /**
76
+ * Per-action `changeMode` disposition (core preset only): `tracked` actions
77
+ * honor `changeMode: "tracked"`; `direct` actions always edit directly and
78
+ * are hidden under `enforceChangeMode` unless allowlisted.
79
+ */
80
+ dispositions?: Record<string, 'tracked' | 'direct'>;
75
81
  };
76
82
  export interface GetToolsOptions {
77
83
  /**
@@ -87,6 +93,19 @@ export interface GetToolsOptions {
87
93
  * ignore this option.
88
94
  */
89
95
  excludeActions?: readonly string[];
96
+ /**
97
+ * Tracked-changes-only policy (core preset). `'tracked'` removes every
98
+ * direct-only action from the surface (unless allowlisted) exactly like
99
+ * `excludeActions`; dispatch then upgrades tracked-capable actions to
100
+ * `changeMode: "tracked"`. Only `'tracked'` is accepted.
101
+ */
102
+ enforceChangeMode?: 'tracked';
103
+ /**
104
+ * Direct-only actions to keep callable under `enforceChangeMode` (`['*']` =
105
+ * all of them). Requires `enforceChangeMode`; unknown names throw;
106
+ * tracked-capable names are ignored.
107
+ */
108
+ allowDirectActions?: readonly string[];
90
109
  }
91
110
  /**
92
111
  * Options for {@link PresetDescriptor.getSystemPrompt}. Mirrors the exclusion
@@ -97,6 +116,10 @@ export interface GetToolsOptions {
97
116
  export interface GetSystemPromptOptions {
98
117
  /** Drop the per-action documentation lines for these actions (core preset). */
99
118
  excludeActions?: readonly string[];
119
+ /** Same policy as {@link GetToolsOptions.enforceChangeMode}: direct-only actions leave the prompt too. */
120
+ enforceChangeMode?: 'tracked';
121
+ /** Same allowlist as {@link GetToolsOptions.allowDirectActions}. */
122
+ allowDirectActions?: readonly string[];
100
123
  }
101
124
  export interface GetToolsResult {
102
125
  tools: unknown[];
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  var node_child_process = require('node:child_process');
4
- var node_readline = require('node:readline');
5
4
  var transportCommon = require('./transport-common.cjs');
6
5
  var errors = require('./errors.cjs');
7
6
  var documentEvidence = require('./document-evidence.cjs');
@@ -96,7 +95,8 @@ class HostTransport {
96
95
  user;
97
96
  documentRpcEnabled;
98
97
  child = null;
99
- stdoutReader = null;
98
+ stdoutBuffer = '';
99
+ stdoutDataListener = null;
100
100
  pending = new Map();
101
101
  nextRequestId = 1;
102
102
  connecting = null;
@@ -418,14 +418,12 @@ class HostTransport {
418
418
  stdio: ['pipe', 'pipe', 'pipe'],
419
419
  });
420
420
  this.child = child;
421
- const stdoutReader = node_readline.createInterface({
422
- input: child.stdout,
423
- crlfDelay: Number.POSITIVE_INFINITY,
424
- });
425
- this.stdoutReader = stdoutReader;
426
- stdoutReader.on('line', (line) => {
427
- this.onStdoutLine(line);
428
- });
421
+ // JSON-RPC is framed by ASCII LF; Node 24 readline also splits on a raw
422
+ // U+2028 inside otherwise valid JSON string data.
423
+ child.stdout.setEncoding('utf8');
424
+ const stdoutDataListener = (chunk) => this.onStdoutChunk(chunk);
425
+ this.stdoutDataListener = stdoutDataListener;
426
+ child.stdout.on('data', stdoutDataListener);
429
427
  child.stderr.on('data', (chunk) => {
430
428
  if (!FORWARD_HOST_STDERR) {
431
429
  return;
@@ -672,6 +670,18 @@ class HostTransport {
672
670
  }
673
671
  pending.resolve(record.result);
674
672
  }
673
+ onStdoutChunk(chunk) {
674
+ this.stdoutBuffer += chunk;
675
+ let newlineIndex = this.stdoutBuffer.indexOf('\n');
676
+ while (newlineIndex !== -1) {
677
+ let line = this.stdoutBuffer.slice(0, newlineIndex);
678
+ this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
679
+ if (line.endsWith('\r'))
680
+ line = line.slice(0, -1);
681
+ this.onStdoutLine(line);
682
+ newlineIndex = this.stdoutBuffer.indexOf('\n');
683
+ }
684
+ }
675
685
  mapJsonRpcError(rawError) {
676
686
  if (typeof rawError !== 'object' || rawError == null || Array.isArray(rawError)) {
677
687
  return new errors.SuperDocCliError('Host returned an unknown JSON-RPC error.', {
@@ -730,15 +740,14 @@ class HostTransport {
730
740
  this.indeterminateDiscardCloses.clear();
731
741
  const child = this.child;
732
742
  if (child) {
743
+ if (this.stdoutDataListener)
744
+ child.stdout.off('data', this.stdoutDataListener);
733
745
  child.removeAllListeners();
734
746
  child.kill('SIGKILL');
735
747
  }
736
748
  this.child = null;
737
- if (this.stdoutReader) {
738
- this.stdoutReader.removeAllListeners();
739
- this.stdoutReader.close();
740
- this.stdoutReader = null;
741
- }
749
+ this.stdoutBuffer = '';
750
+ this.stdoutDataListener = null;
742
751
  const pendingEntries = Array.from(this.pending.values());
743
752
  this.pending.clear();
744
753
  const rejection = error ??
@@ -43,7 +43,8 @@ export declare class HostTransport {
43
43
  private readonly user?;
44
44
  private readonly documentRpcEnabled;
45
45
  private child;
46
- private stdoutReader;
46
+ private stdoutBuffer;
47
+ private stdoutDataListener;
47
48
  private readonly pending;
48
49
  private nextRequestId;
49
50
  private connecting;
@@ -82,6 +83,7 @@ export declare class HostTransport {
82
83
  private reconcileIndeterminateDiscardCloses;
83
84
  private sendJsonRpcRequest;
84
85
  private onStdoutLine;
86
+ private onStdoutChunk;
85
87
  private mapJsonRpcError;
86
88
  private handleDisconnect;
87
89
  private cleanupProcess;
@@ -1,5 +1,4 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { createInterface } from 'node:readline';
3
2
  import { buildOperationArgv, normalizeCollaborationAuth, resolveInvocation, } from './transport-common.js';
4
3
  import { SuperDocCliError } from './errors.js';
5
4
  import { registerMutationRevision } from './document-evidence.js';
@@ -93,7 +92,8 @@ export class HostTransport {
93
92
  user;
94
93
  documentRpcEnabled;
95
94
  child = null;
96
- stdoutReader = null;
95
+ stdoutBuffer = '';
96
+ stdoutDataListener = null;
97
97
  pending = new Map();
98
98
  nextRequestId = 1;
99
99
  connecting = null;
@@ -415,14 +415,12 @@ export class HostTransport {
415
415
  stdio: ['pipe', 'pipe', 'pipe'],
416
416
  });
417
417
  this.child = child;
418
- const stdoutReader = createInterface({
419
- input: child.stdout,
420
- crlfDelay: Number.POSITIVE_INFINITY,
421
- });
422
- this.stdoutReader = stdoutReader;
423
- stdoutReader.on('line', (line) => {
424
- this.onStdoutLine(line);
425
- });
418
+ // JSON-RPC is framed by ASCII LF; Node 24 readline also splits on a raw
419
+ // U+2028 inside otherwise valid JSON string data.
420
+ child.stdout.setEncoding('utf8');
421
+ const stdoutDataListener = (chunk) => this.onStdoutChunk(chunk);
422
+ this.stdoutDataListener = stdoutDataListener;
423
+ child.stdout.on('data', stdoutDataListener);
426
424
  child.stderr.on('data', (chunk) => {
427
425
  if (!FORWARD_HOST_STDERR) {
428
426
  return;
@@ -669,6 +667,18 @@ export class HostTransport {
669
667
  }
670
668
  pending.resolve(record.result);
671
669
  }
670
+ onStdoutChunk(chunk) {
671
+ this.stdoutBuffer += chunk;
672
+ let newlineIndex = this.stdoutBuffer.indexOf('\n');
673
+ while (newlineIndex !== -1) {
674
+ let line = this.stdoutBuffer.slice(0, newlineIndex);
675
+ this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
676
+ if (line.endsWith('\r'))
677
+ line = line.slice(0, -1);
678
+ this.onStdoutLine(line);
679
+ newlineIndex = this.stdoutBuffer.indexOf('\n');
680
+ }
681
+ }
672
682
  mapJsonRpcError(rawError) {
673
683
  if (typeof rawError !== 'object' || rawError == null || Array.isArray(rawError)) {
674
684
  return new SuperDocCliError('Host returned an unknown JSON-RPC error.', {
@@ -727,15 +737,14 @@ export class HostTransport {
727
737
  this.indeterminateDiscardCloses.clear();
728
738
  const child = this.child;
729
739
  if (child) {
740
+ if (this.stdoutDataListener)
741
+ child.stdout.off('data', this.stdoutDataListener);
730
742
  child.removeAllListeners();
731
743
  child.kill('SIGKILL');
732
744
  }
733
745
  this.child = null;
734
- if (this.stdoutReader) {
735
- this.stdoutReader.removeAllListeners();
736
- this.stdoutReader.close();
737
- this.stdoutReader = null;
738
- }
746
+ this.stdoutBuffer = '';
747
+ this.stdoutDataListener = null;
739
748
  const pendingEntries = Array.from(this.pending.values());
740
749
  this.pending.clear();
741
750
  const rejection = error ??
@@ -3,6 +3,6 @@
3
3
  // AUTO-GENERATED by scripts/embed-version.mjs — DO NOT EDIT.
4
4
  // Source of truth: package.json. Regenerated on every SDK build so the
5
5
  // SDK retains its own version identity when bundled into another package.
6
- const SDK_VERSION = '2.13.0-next.9';
6
+ const SDK_VERSION = '2.13.1-next.1';
7
7
 
8
8
  exports.SDK_VERSION = SDK_VERSION;
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "2.13.0-next.9";
1
+ export declare const SDK_VERSION = "2.13.1-next.1";
@@ -1,4 +1,4 @@
1
1
  // AUTO-GENERATED by scripts/embed-version.mjs — DO NOT EDIT.
2
2
  // Source of truth: package.json. Regenerated on every SDK build so the
3
3
  // SDK retains its own version identity when bundled into another package.
4
- export const SDK_VERSION = '2.13.0-next.9';
4
+ export const SDK_VERSION = '2.13.1-next.1';
package/dist/tools.cjs CHANGED
@@ -34,6 +34,8 @@ async function chooseTools(input) {
34
34
  const { tools, cacheStrategy } = await preset.getTools(input.provider, {
35
35
  cache: input.cache === true,
36
36
  excludeActions: input.excludeActions,
37
+ enforceChangeMode: input.enforceChangeMode,
38
+ allowDirectActions: input.allowDirectActions,
37
39
  });
38
40
  return {
39
41
  tools,
@@ -102,16 +104,25 @@ const CUSTOM_TOOLKIT_PRESET_ID = 'custom_superdoc_preset';
102
104
  const TOOL_PROVIDERS = new Set(['openai', 'anthropic', 'vercel', 'generic']);
103
105
  /**
104
106
  * One-call agent surface: tools, system prompt, and a pre-bound dispatcher
105
- * that are coherent BY CONSTRUCTION — the same preset and `excludeActions`
106
- * apply to all three, so an action can never linger in the prompt after
107
- * being excluded from the tool array (or vice versa).
107
+ * that are coherent BY CONSTRUCTION — the same preset, `excludeActions`,
108
+ * `enforceChangeMode`, and `allowDirectActions` apply to all three, so an
109
+ * action can never linger in the prompt after being excluded from the tool
110
+ * array (or vice versa).
108
111
  *
109
112
  * The legacy preset ignores exclusion options everywhere (it has no action
110
113
  * surface); passing `excludeActions` with `preset: 'legacy'` is a no-op,
111
114
  * matching the standalone functions.
112
115
  */
113
116
  async function createAgentToolkit(input) {
114
- const excludeActions = input.excludeActions ? [...input.excludeActions] : undefined;
117
+ // Copy the lists so later caller mutation cannot desync dispatch from the
118
+ // tools and prompt that were generated here.
119
+ const policy = {
120
+ ...(input.excludeActions ? { excludeActions: [...input.excludeActions] } : {}),
121
+ ...(input.enforceChangeMode !== undefined ? { enforceChangeMode: input.enforceChangeMode } : {}),
122
+ ...(input.allowDirectActions ? { allowDirectActions: [...input.allowDirectActions] } : {}),
123
+ };
124
+ const hasPolicy = Object.keys(policy).length > 0;
125
+ const excludeActions = policy.excludeActions;
115
126
  // One-call custom-actions path: define your actions, hand them here as
116
127
  // `customActions`, use the
117
128
  // returned toolkit. We build an ephemeral extended/composed preset over
@@ -122,6 +133,16 @@ async function createAgentToolkit(input) {
122
133
  // otherwise `{ base: 'core', customActions: [] }` would silently fall through to
123
134
  // the default (legacy) preset, a different tool family than the one named.
124
135
  if ((input.customActions && input.customActions.length > 0) || input.includeActions != null || input.base != null) {
136
+ // A custom action runs its own step list through runCustomAction, which
137
+ // never passes the core dispatch guard, so the tracked-only policy cannot
138
+ // cover it. Enforcing built-ins while custom actions edit directly would be
139
+ // a policy that silently does not hold — refuse the combination instead.
140
+ if (input.enforceChangeMode !== undefined) {
141
+ throw new errors.SuperDocCliError('enforceChangeMode is not supported together with custom actions; a custom action bypasses the tracked-changes guard.', {
142
+ code: 'INVALID_ARGUMENT',
143
+ details: { enforceChangeMode: input.enforceChangeMode, reason: 'custom-actions-unsupported' },
144
+ });
145
+ }
125
146
  // This path builds tools directly (no chooseTools), so it must validate
126
147
  // the provider itself; an unknown value would otherwise yield a silently
127
148
  // mixed-dialect tool list. Mirrors the Python `create_agent_toolkit` guard.
@@ -156,12 +177,12 @@ async function createAgentToolkit(input) {
156
177
  };
157
178
  }
158
179
  const presetId = input.preset ?? presets.DEFAULT_PRESET;
159
- const { tools, meta } = await chooseTools({ ...input, preset: presetId, excludeActions });
160
- const systemPrompt = await getSystemPrompt(presetId, excludeActions ? { excludeActions } : undefined);
180
+ const { tools, meta } = await chooseTools({ ...input, preset: presetId, ...policy });
181
+ const systemPrompt = await getSystemPrompt(presetId, hasPolicy ? policy : undefined);
161
182
  const dispatch = (documentHandle, toolName, args = {}, invokeOptions) => dispatchSuperDocTool(documentHandle, toolName, args, {
162
183
  ...invokeOptions,
163
184
  preset: presetId,
164
- ...(excludeActions ? { excludeActions } : {}),
185
+ ...policy,
165
186
  });
166
187
  return { tools, meta, systemPrompt, dispatch };
167
188
  }
@@ -202,7 +223,12 @@ async function getMcpPrompt(preset) {
202
223
  * ```
203
224
  */
204
225
  async function getSystemPromptForProvider(input) {
205
- const text = await getSystemPrompt(input.preset, input.excludeActions ? { excludeActions: input.excludeActions } : undefined);
226
+ const promptOptions = {
227
+ ...(input.excludeActions ? { excludeActions: input.excludeActions } : {}),
228
+ ...(input.enforceChangeMode !== undefined ? { enforceChangeMode: input.enforceChangeMode } : {}),
229
+ ...(input.allowDirectActions ? { allowDirectActions: input.allowDirectActions } : {}),
230
+ };
231
+ const text = await getSystemPrompt(input.preset, Object.keys(promptOptions).length > 0 ? promptOptions : undefined);
206
232
  const cacheRequested = input.cache === true;
207
233
  if (input.provider === 'anthropic') {
208
234
  const block = { type: 'text', text };