@superdoc-dev/sdk 1.0.0-alpha.1 → 1.0.0-alpha.3

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/src/index.ts CHANGED
@@ -1,29 +1,76 @@
1
1
  import { createDocApi } from './generated/client';
2
2
  import { SuperDocRuntime, type SuperDocClientOptions } from './runtime/process';
3
3
 
4
+ /**
5
+ * High-level client for interacting with SuperDoc documents via the CLI.
6
+ *
7
+ * Provides a typed `doc` API for opening, querying, and mutating documents.
8
+ * Call {@link connect} before operations and {@link dispose} when finished
9
+ * to manage the host process lifecycle.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const client = new SuperDocClient();
14
+ * await client.connect();
15
+ * const result = await client.doc.find({ doc: 'report.docx', type: 'text', pattern: 'hello' });
16
+ * await client.dispose();
17
+ * ```
18
+ */
4
19
  export class SuperDocClient {
5
20
  private readonly runtime: SuperDocRuntime;
6
21
  readonly doc: ReturnType<typeof createDocApi>;
7
22
 
23
+ /**
24
+ * @param options - Client configuration including environment overrides.
25
+ */
8
26
  constructor(options: SuperDocClientOptions = {}) {
9
27
  this.runtime = new SuperDocRuntime(options);
10
28
  this.doc = createDocApi(this.runtime);
11
29
  }
12
30
 
31
+ /**
32
+ * Establish the connection to the CLI host process.
33
+ * Can be called eagerly; otherwise the host starts lazily on the first command.
34
+ */
13
35
  async connect(): Promise<void> {
14
36
  await this.runtime.connect();
15
37
  }
16
38
 
39
+ /**
40
+ * Shut down the CLI host process and release resources.
41
+ */
17
42
  async dispose(): Promise<void> {
18
43
  await this.runtime.dispose();
19
44
  }
20
45
  }
21
46
 
47
+ /**
48
+ * Create a new {@link SuperDocClient} instance.
49
+ *
50
+ * @param options - Client configuration.
51
+ * @returns A configured client ready for document operations.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const client = createSuperDocClient();
56
+ * await client.connect();
57
+ * const info = await client.doc.info({ doc: 'report.docx' });
58
+ * await client.dispose();
59
+ * ```
60
+ */
22
61
  export function createSuperDocClient(options: SuperDocClientOptions = {}): SuperDocClient {
23
62
  return new SuperDocClient(options);
24
63
  }
25
64
 
26
- export { getSkill, listSkills } from './skills';
65
+ export { getSkill, installSkill, listSkills } from './skills';
66
+ export {
67
+ chooseTools,
68
+ dispatchSuperDocTool,
69
+ getToolCatalog,
70
+ inferDocumentFeatures,
71
+ listTools,
72
+ resolveToolOperation,
73
+ } from './tools';
27
74
  export { SuperDocCliError } from './runtime/errors';
28
75
  export type {
29
76
  InvokeOptions,
@@ -31,3 +78,10 @@ export type {
31
78
  OperationParamSpec,
32
79
  SuperDocClientOptions,
33
80
  } from './runtime/process';
81
+ export type {
82
+ DocumentFeatures,
83
+ ToolChooserInput,
84
+ ToolPhase,
85
+ ToolProfile,
86
+ ToolProvider,
87
+ } from './tools';
@@ -0,0 +1,38 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test';
2
+ import { SuperDocRuntime } from '../process';
3
+
4
+ const ORIGINAL_SUPERDOC_CLI_BIN = process.env.SUPERDOC_CLI_BIN;
5
+
6
+ function runtimeCliBin(runtime: SuperDocRuntime): string {
7
+ return (runtime as unknown as { transport: { cliBin: string } }).transport.cliBin;
8
+ }
9
+
10
+ afterEach(() => {
11
+ if (ORIGINAL_SUPERDOC_CLI_BIN == null) {
12
+ delete process.env.SUPERDOC_CLI_BIN;
13
+ return;
14
+ }
15
+ process.env.SUPERDOC_CLI_BIN = ORIGINAL_SUPERDOC_CLI_BIN;
16
+ });
17
+
18
+ describe('SuperDocRuntime', () => {
19
+ test('prefers SUPERDOC_CLI_BIN from client options env over process env', () => {
20
+ process.env.SUPERDOC_CLI_BIN = '/process/env/superdoc';
21
+
22
+ const runtime = new SuperDocRuntime({
23
+ env: {
24
+ SUPERDOC_CLI_BIN: '/options/env/superdoc',
25
+ },
26
+ });
27
+
28
+ expect(runtimeCliBin(runtime)).toBe('/options/env/superdoc');
29
+ });
30
+
31
+ test('uses process env SUPERDOC_CLI_BIN when client options env does not provide one', () => {
32
+ process.env.SUPERDOC_CLI_BIN = '/process/env/superdoc';
33
+
34
+ const runtime = new SuperDocRuntime();
35
+
36
+ expect(runtimeCliBin(runtime)).toBe('/process/env/superdoc');
37
+ });
38
+ });
@@ -0,0 +1,174 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { buildOperationArgv, resolveInvocation, type OperationSpec } from '../transport-common';
3
+
4
+ describe('resolveInvocation', () => {
5
+ test('resolves .js files via node', () => {
6
+ const result = resolveInvocation('/path/to/cli.js');
7
+ expect(result).toEqual({ command: 'node', prefixArgs: ['/path/to/cli.js'] });
8
+ });
9
+
10
+ test('resolves .ts files via bun', () => {
11
+ const result = resolveInvocation('/path/to/cli.ts');
12
+ expect(result).toEqual({ command: 'bun', prefixArgs: ['/path/to/cli.ts'] });
13
+ });
14
+
15
+ test('resolves plain binaries directly', () => {
16
+ const result = resolveInvocation('/usr/bin/superdoc');
17
+ expect(result).toEqual({ command: '/usr/bin/superdoc', prefixArgs: [] });
18
+ });
19
+
20
+ test('extension check is case-insensitive', () => {
21
+ expect(resolveInvocation('/path/CLI.JS').command).toBe('node');
22
+ expect(resolveInvocation('/path/CLI.TS').command).toBe('bun');
23
+ });
24
+ });
25
+
26
+ describe('buildOperationArgv', () => {
27
+ const baseOperation: OperationSpec = {
28
+ id: 'test.op',
29
+ command: ['test', 'run'],
30
+ params: [],
31
+ };
32
+
33
+ test('starts with command segments', () => {
34
+ const args = buildOperationArgv(baseOperation, {}, {}, undefined, false, undefined);
35
+ expect(args).toEqual(['test', 'run']);
36
+ });
37
+
38
+ test('appends --output json when includeOutputFlag is true', () => {
39
+ const args = buildOperationArgv(baseOperation, {}, {}, undefined, true, undefined);
40
+ expect(args).toEqual(['test', 'run', '--output', 'json']);
41
+ });
42
+
43
+ test('encodes string flag parameters', () => {
44
+ const op: OperationSpec = {
45
+ ...baseOperation,
46
+ params: [{ name: 'session', kind: 'flag', flag: 'session', type: 'string' }],
47
+ };
48
+ const args = buildOperationArgv(op, { session: 'my-session' }, {}, undefined, false, undefined);
49
+ expect(args).toEqual(['test', 'run', '--session', 'my-session']);
50
+ });
51
+
52
+ test('encodes boolean flag parameters as presence-only', () => {
53
+ const op: OperationSpec = {
54
+ ...baseOperation,
55
+ params: [{ name: 'force', kind: 'flag', flag: 'force', type: 'boolean' }],
56
+ };
57
+ const trueArgs = buildOperationArgv(op, { force: true }, {}, undefined, false, undefined);
58
+ expect(trueArgs).toEqual(['test', 'run', '--force']);
59
+
60
+ const falseArgs = buildOperationArgv(op, { force: false }, {}, undefined, false, undefined);
61
+ expect(falseArgs).toEqual(['test', 'run']);
62
+ });
63
+
64
+ test('encodes number flag parameters', () => {
65
+ const op: OperationSpec = {
66
+ ...baseOperation,
67
+ params: [{ name: 'limit', kind: 'flag', flag: 'limit', type: 'number' }],
68
+ };
69
+ const args = buildOperationArgv(op, { limit: 10 }, {}, undefined, false, undefined);
70
+ expect(args).toEqual(['test', 'run', '--limit', '10']);
71
+ });
72
+
73
+ test('encodes json flag parameters as JSON strings', () => {
74
+ const op: OperationSpec = {
75
+ ...baseOperation,
76
+ params: [{ name: 'query', kind: 'jsonFlag', flag: 'query-json', type: 'json' }],
77
+ };
78
+ const args = buildOperationArgv(op, { query: { type: 'text' } }, {}, undefined, false, undefined);
79
+ expect(args).toEqual(['test', 'run', '--query-json', '{"type":"text"}']);
80
+ });
81
+
82
+ test('encodes doc positional parameters', () => {
83
+ const op: OperationSpec = {
84
+ ...baseOperation,
85
+ params: [{ name: 'doc', kind: 'doc', type: 'string' }],
86
+ };
87
+ const args = buildOperationArgv(op, { doc: '/path/to/file.docx' }, {}, undefined, false, undefined);
88
+ expect(args).toEqual(['test', 'run', '/path/to/file.docx']);
89
+ });
90
+
91
+ test('encodes string[] flag parameters as repeated flags', () => {
92
+ const op: OperationSpec = {
93
+ ...baseOperation,
94
+ params: [{ name: 'include', kind: 'flag', flag: 'include', type: 'string[]' }],
95
+ };
96
+ const args = buildOperationArgv(op, { include: ['a', 'b'] }, {}, undefined, false, undefined);
97
+ expect(args).toEqual(['test', 'run', '--include', 'a', '--include', 'b']);
98
+ });
99
+
100
+ test('skips null/undefined optional parameters', () => {
101
+ const op: OperationSpec = {
102
+ ...baseOperation,
103
+ params: [{ name: 'session', kind: 'flag', flag: 'session', type: 'string' }],
104
+ };
105
+ const args = buildOperationArgv(op, {}, {}, undefined, false, undefined);
106
+ expect(args).toEqual(['test', 'run']);
107
+ });
108
+
109
+ test('throws on missing required parameters', () => {
110
+ const op: OperationSpec = {
111
+ ...baseOperation,
112
+ params: [{ name: 'doc', kind: 'doc', type: 'string', required: true }],
113
+ };
114
+ expect(() => buildOperationArgv(op, {}, {}, undefined, false, undefined)).toThrow('Missing required parameter: doc');
115
+ });
116
+
117
+ test('throws on non-array value for string[] parameter', () => {
118
+ const op: OperationSpec = {
119
+ ...baseOperation,
120
+ params: [{ name: 'include', kind: 'flag', flag: 'include', type: 'string[]' }],
121
+ };
122
+ expect(() => buildOperationArgv(op, { include: 'not-an-array' }, {}, undefined, false, undefined)).toThrow(
123
+ 'must be an array',
124
+ );
125
+ });
126
+
127
+ test('appends timeout from invoke options', () => {
128
+ const args = buildOperationArgv(baseOperation, {}, { timeoutMs: 5000 }, undefined, false, undefined);
129
+ expect(args).toEqual(['test', 'run', '--timeout-ms', '5000']);
130
+ });
131
+
132
+ test('invoke timeout takes precedence over runtime timeout', () => {
133
+ const args = buildOperationArgv(baseOperation, {}, { timeoutMs: 3000 }, 10000, false, undefined);
134
+ expect(args).toContain('3000');
135
+ expect(args).not.toContain('10000');
136
+ });
137
+
138
+ test('falls back to runtime timeout when invoke timeout is absent', () => {
139
+ const args = buildOperationArgv(baseOperation, {}, {}, 7000, false, undefined);
140
+ expect(args).toEqual(['test', 'run', '--timeout-ms', '7000']);
141
+ });
142
+
143
+ test('uses flag name as fallback when flag property is undefined', () => {
144
+ const op: OperationSpec = {
145
+ ...baseOperation,
146
+ params: [{ name: 'mode', kind: 'flag', type: 'string' }],
147
+ };
148
+ const args = buildOperationArgv(op, { mode: 'fast' }, {}, undefined, false, undefined);
149
+ expect(args).toEqual(['test', 'run', '--mode', 'fast']);
150
+ });
151
+
152
+ test('injects default change mode when operation supports changeMode and params omit it', () => {
153
+ const op: OperationSpec = {
154
+ ...baseOperation,
155
+ params: [{ name: 'changeMode', kind: 'flag', flag: 'change-mode', type: 'string' }],
156
+ };
157
+ const args = buildOperationArgv(op, {}, {}, undefined, false, 'tracked');
158
+ expect(args).toEqual(['test', 'run', '--change-mode', 'tracked']);
159
+ });
160
+
161
+ test('does not inject default change mode when operation does not support changeMode', () => {
162
+ const args = buildOperationArgv(baseOperation, {}, {}, undefined, false, 'tracked');
163
+ expect(args).toEqual(['test', 'run']);
164
+ });
165
+
166
+ test('explicit changeMode parameter overrides default change mode', () => {
167
+ const op: OperationSpec = {
168
+ ...baseOperation,
169
+ params: [{ name: 'changeMode', kind: 'flag', flag: 'change-mode', type: 'string' }],
170
+ };
171
+ const args = buildOperationArgv(op, { changeMode: 'direct' }, {}, undefined, false, 'tracked');
172
+ expect(args).toEqual(['test', 'run', '--change-mode', 'direct']);
173
+ });
174
+ });
@@ -69,6 +69,16 @@ function resolveFromWorkspaceFallback(target: SupportedTarget): string | null {
69
69
  return filePath;
70
70
  }
71
71
 
72
+ /**
73
+ * Resolve the path to the embedded SuperDoc CLI binary for the current platform.
74
+ *
75
+ * Checks platform-specific npm packages first, then falls back to a workspace
76
+ * `platforms/` directory. Ensures the binary is executable before returning.
77
+ *
78
+ * @returns Absolute path to the CLI binary.
79
+ * @throws {SuperDocCliError} With code `UNSUPPORTED_PLATFORM` if the current OS/arch is not supported.
80
+ * @throws {SuperDocCliError} With code `CLI_BINARY_MISSING` if no binary is found.
81
+ */
72
82
  export function resolveEmbeddedCliBinary(): string {
73
83
  const target = resolveTarget();
74
84
  if (!target) {
@@ -1,3 +1,20 @@
1
+ /**
2
+ * Error thrown by the SuperDoc SDK when a CLI operation fails.
3
+ *
4
+ * Includes a machine-readable `code` for programmatic error handling
5
+ * and optional `details` with structured diagnostic context.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * try {
10
+ * await client.doc.open({ doc: 'missing.docx' });
11
+ * } catch (error) {
12
+ * if (error instanceof SuperDocCliError) {
13
+ * console.error(error.code, error.message);
14
+ * }
15
+ * }
16
+ * ```
17
+ */
1
18
  export class SuperDocCliError extends Error {
2
19
  readonly code: string;
3
20
  readonly details?: unknown;
@@ -1,6 +1,13 @@
1
1
  import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
2
2
  import { createInterface, type Interface as ReadlineInterface } from 'node:readline';
3
- import { buildOperationArgv, resolveInvocation, type InvokeOptions, type OperationSpec, type SuperDocHostOptions } from './transport-common';
3
+ import {
4
+ buildOperationArgv,
5
+ resolveInvocation,
6
+ type ChangeMode,
7
+ type InvokeOptions,
8
+ type OperationSpec,
9
+ type SuperDocClientOptions,
10
+ } from './transport-common';
4
11
  import { SuperDocCliError } from './errors';
5
12
 
6
13
  type PendingRequest = {
@@ -24,9 +31,16 @@ type JsonRpcError = {
24
31
 
25
32
  const HOST_PROTOCOL_VERSION = '1.0';
26
33
  const REQUIRED_FEATURES = ['cli.invoke', 'host.shutdown'];
34
+ const CHANGE_MODES = ['direct', 'tracked'] as const;
27
35
 
28
36
  const JSON_RPC_TIMEOUT_CODE = -32011;
29
37
 
38
+ /**
39
+ * Transport that communicates with a long-lived CLI host process over JSON-RPC stdio.
40
+ *
41
+ * The host process is spawned lazily on first invocation and kept alive for
42
+ * subsequent calls. Call {@link dispose} to gracefully shut it down.
43
+ */
30
44
  export class HostTransport {
31
45
  private readonly cliBin: string;
32
46
  private readonly env?: Record<string, string | undefined>;
@@ -35,6 +49,7 @@ export class HostTransport {
35
49
  private readonly requestTimeoutMs?: number;
36
50
  private readonly watchdogTimeoutMs: number;
37
51
  private readonly maxQueueDepth: number;
52
+ private readonly defaultChangeMode?: ChangeMode;
38
53
 
39
54
  private child: ChildProcessWithoutNullStreams | null = null;
40
55
  private stdoutReader: ReadlineInterface | null = null;
@@ -43,19 +58,22 @@ export class HostTransport {
43
58
  private connecting: Promise<void> | null = null;
44
59
  private stopping = false;
45
60
 
46
- constructor(options: {
47
- cliBin: string;
48
- env?: Record<string, string | undefined>;
49
- host?: SuperDocHostOptions;
50
- }) {
61
+ constructor(options: { cliBin: string } & SuperDocClientOptions) {
51
62
  this.cliBin = options.cliBin;
52
63
  this.env = options.env;
53
64
 
54
- this.startupTimeoutMs = options.host?.startupTimeoutMs ?? 5_000;
55
- this.shutdownTimeoutMs = options.host?.shutdownTimeoutMs ?? 5_000;
56
- this.requestTimeoutMs = options.host?.requestTimeoutMs;
57
- this.watchdogTimeoutMs = options.host?.watchdogTimeoutMs ?? 30_000;
58
- this.maxQueueDepth = options.host?.maxQueueDepth ?? 100;
65
+ this.startupTimeoutMs = options.startupTimeoutMs ?? 5_000;
66
+ this.shutdownTimeoutMs = options.shutdownTimeoutMs ?? 5_000;
67
+ this.requestTimeoutMs = options.requestTimeoutMs;
68
+ this.watchdogTimeoutMs = options.watchdogTimeoutMs ?? 30_000;
69
+ this.maxQueueDepth = options.maxQueueDepth ?? 100;
70
+ if (options.defaultChangeMode != null && !CHANGE_MODES.includes(options.defaultChangeMode)) {
71
+ throw new SuperDocCliError('defaultChangeMode must be "direct" or "tracked".', {
72
+ code: 'INVALID_ARGUMENT',
73
+ details: { defaultChangeMode: options.defaultChangeMode },
74
+ });
75
+ }
76
+ this.defaultChangeMode = options.defaultChangeMode;
59
77
  }
60
78
 
61
79
  async connect(): Promise<void> {
@@ -97,7 +115,7 @@ export class HostTransport {
97
115
  ): Promise<TData> {
98
116
  await this.ensureConnected();
99
117
 
100
- const argv = buildOperationArgv(operation, params, options, this.requestTimeoutMs, false);
118
+ const argv = buildOperationArgv(operation, params, options, this.requestTimeoutMs, false, this.defaultChangeMode);
101
119
  const stdinBase64 = options.stdinBytes ? Buffer.from(options.stdinBytes).toString('base64') : '';
102
120
  const watchdogTimeout = this.resolveWatchdogTimeout(options.timeoutMs);
103
121
 
@@ -1,36 +1,27 @@
1
1
  import { HostTransport } from './host';
2
- import { SpawnTransport } from './spawn';
3
2
  import { resolveEmbeddedCliBinary } from './embedded-cli';
4
3
  import type {
5
4
  InvokeOptions,
6
5
  OperationParamSpec,
7
6
  OperationSpec,
8
7
  SuperDocClientOptions,
9
- SuperDocHostOptions,
10
- SuperDocTransport,
11
8
  } from './transport-common';
12
9
 
13
- type RuntimeTransport = {
14
- invoke<TData extends Record<string, unknown>>(
15
- operation: OperationSpec,
16
- params?: Record<string, unknown>,
17
- options?: InvokeOptions,
18
- ): Promise<TData>;
19
- connect(): Promise<void>;
20
- dispose(): Promise<void>;
21
- };
22
-
10
+ /**
11
+ * Internal runtime that delegates CLI invocations to a persistent host transport.
12
+ *
13
+ * Resolves the CLI binary and creates a {@link HostTransport} that communicates
14
+ * with a long-lived `superdoc host --stdio` process.
15
+ */
23
16
  export class SuperDocRuntime {
24
- private readonly transport: RuntimeTransport;
17
+ private readonly transport: HostTransport;
25
18
 
26
19
  constructor(options: SuperDocClientOptions = {}) {
27
- const cliBin = options.cliBin ?? process.env.SUPERDOC_CLI_BIN ?? resolveEmbeddedCliBinary();
28
- const transportMode = options.transport ?? 'spawn';
20
+ const cliBin = options.env?.SUPERDOC_CLI_BIN ?? process.env.SUPERDOC_CLI_BIN ?? resolveEmbeddedCliBinary();
29
21
 
30
- this.transport = this.createTransport(transportMode, {
22
+ this.transport = new HostTransport({
31
23
  cliBin,
32
- env: options.env,
33
- host: options.host,
24
+ ...options,
34
25
  });
35
26
  }
36
27
 
@@ -49,21 +40,6 @@ export class SuperDocRuntime {
49
40
  ): Promise<TData> {
50
41
  return this.transport.invoke<TData>(operation, params, options);
51
42
  }
52
-
53
- private createTransport(
54
- mode: SuperDocTransport,
55
- options: {
56
- cliBin: string;
57
- env?: Record<string, string | undefined>;
58
- host?: SuperDocHostOptions;
59
- },
60
- ): RuntimeTransport {
61
- if (mode === 'host') {
62
- return new HostTransport(options);
63
- }
64
-
65
- return new SpawnTransport(options);
66
- }
67
43
  }
68
44
 
69
- export type { InvokeOptions, OperationParamSpec, OperationSpec, SuperDocClientOptions, SuperDocHostOptions, SuperDocTransport };
45
+ export type { InvokeOptions, OperationParamSpec, OperationSpec, SuperDocClientOptions };
@@ -1,44 +1,56 @@
1
1
  import { SuperDocCliError } from './errors';
2
2
 
3
+ /** Allowed CLI parameter value types. */
3
4
  export type ParamType = 'string' | 'number' | 'boolean' | 'json' | 'string[]';
5
+
6
+ /** How a parameter is passed to the CLI (`doc` positional, `flag`, or `jsonFlag`). */
4
7
  export type ParamKind = 'doc' | 'flag' | 'jsonFlag';
5
8
 
9
+ /** Describes a single parameter in a CLI operation. */
6
10
  export interface OperationParamSpec {
7
11
  readonly name: string;
8
12
  readonly kind: ParamKind;
9
- flag?: string;
13
+ readonly flag?: string;
10
14
  readonly type: ParamType;
11
- required?: boolean;
15
+ readonly required?: boolean;
12
16
  }
13
17
 
18
+ /** Describes a CLI operation (command segments and its parameter specs). */
14
19
  export interface OperationSpec {
15
20
  readonly id: string;
16
21
  readonly command: readonly string[];
17
22
  readonly params: readonly OperationParamSpec[];
18
23
  }
19
24
 
25
+ /** Per-call options for a CLI invocation. */
20
26
  export interface InvokeOptions {
27
+ /** Timeout in milliseconds forwarded to the CLI `--timeout-ms` flag. */
21
28
  timeoutMs?: number;
29
+ /** Raw bytes piped to the CLI process stdin. */
22
30
  stdinBytes?: Uint8Array;
23
31
  }
24
32
 
25
- export type SuperDocTransport = 'spawn' | 'host';
33
+ export type ChangeMode = 'direct' | 'tracked';
26
34
 
27
- export interface SuperDocHostOptions {
35
+ /** Top-level options for creating a {@link SuperDocClient}. */
36
+ export interface SuperDocClientOptions {
37
+ /** Extra environment variables merged into the CLI process environment. */
38
+ env?: Record<string, string | undefined>;
39
+ /** Timeout in milliseconds for the host process startup handshake. */
28
40
  startupTimeoutMs?: number;
41
+ /** Timeout in milliseconds for graceful host shutdown. */
29
42
  shutdownTimeoutMs?: number;
43
+ /** Default per-request timeout in milliseconds. */
30
44
  requestTimeoutMs?: number;
45
+ /** Idle watchdog timeout in milliseconds. */
31
46
  watchdogTimeoutMs?: number;
47
+ /** Maximum number of queued requests. */
32
48
  maxQueueDepth?: number;
49
+ /** Default change mode for mutation operations that support `changeMode`. */
50
+ defaultChangeMode?: ChangeMode;
33
51
  }
34
52
 
35
- export interface SuperDocClientOptions {
36
- cliBin?: string;
37
- env?: Record<string, string | undefined>;
38
- transport?: SuperDocTransport;
39
- host?: SuperDocHostOptions;
40
- }
41
-
53
+ /** Resolved command and prefix args for spawning the CLI. */
42
54
  export interface CliInvocation {
43
55
  command: string;
44
56
  prefixArgs: string[];
@@ -48,6 +60,12 @@ function hasExtension(filePath: string, extension: string): boolean {
48
60
  return filePath.toLowerCase().endsWith(extension);
49
61
  }
50
62
 
63
+ /**
64
+ * Determine how to spawn the CLI binary based on its file extension.
65
+ *
66
+ * @param cliBin - Path to the CLI binary or script.
67
+ * @returns The shell command and any prefix arguments needed to execute it.
68
+ */
51
69
  export function resolveInvocation(cliBin: string): CliInvocation {
52
70
  if (hasExtension(cliBin, '.js')) {
53
71
  return { command: 'node', prefixArgs: [cliBin] };
@@ -109,16 +127,33 @@ function encodeParam(args: string[], spec: OperationParamSpec, value: unknown):
109
127
  args.push(flag, String(value));
110
128
  }
111
129
 
130
+ /**
131
+ * Build the CLI argument vector for an operation invocation.
132
+ *
133
+ * @param operation - The operation spec describing the command and its parameters.
134
+ * @param params - User-supplied parameter values keyed by param name.
135
+ * @param options - Per-call invoke options (timeout, stdin).
136
+ * @param runtimeTimeoutMs - Default timeout from the runtime configuration.
137
+ * @param includeOutputFlag - Whether to append `--output json`.
138
+ * @returns The argument array ready to be passed to `spawn`.
139
+ * @throws {SuperDocCliError} With code `INVALID_ARGUMENT` if a required parameter is missing.
140
+ */
112
141
  export function buildOperationArgv(
113
142
  operation: OperationSpec,
114
143
  params: Record<string, unknown>,
115
144
  options: InvokeOptions,
116
145
  runtimeTimeoutMs: number | undefined,
117
146
  includeOutputFlag: boolean,
147
+ defaultChangeMode?: ChangeMode,
118
148
  ): string[] {
149
+ const normalizedParams =
150
+ defaultChangeMode != null && params.changeMode == null && operation.params.some((param) => param.name === 'changeMode')
151
+ ? { ...params, changeMode: defaultChangeMode }
152
+ : params;
153
+
119
154
  const args: string[] = [...operation.command];
120
155
  for (const param of operation.params) {
121
- encodeParam(args, param, params[param.name]);
156
+ encodeParam(args, param, normalizedParams[param.name]);
122
157
  }
123
158
 
124
159
  const timeoutMs = options.timeoutMs ?? runtimeTimeoutMs;