@superdoc-dev/sdk 1.0.0-alpha.1 → 1.0.0-alpha.2
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/package.json +7 -6
- package/skills/editing-docx.md +53 -13
- package/src/__tests__/skills.test.ts +93 -0
- package/src/generated/DO_NOT_EDIT +2 -0
- package/src/generated/client.ts +2372 -78
- package/src/generated/contract.ts +10659 -730
- package/src/index.ts +39 -0
- package/src/runtime/__tests__/transport-common.test.ts +151 -0
- package/src/runtime/embedded-cli.ts +10 -0
- package/src/runtime/errors.ts +17 -0
- package/src/runtime/host.ts +13 -11
- package/src/runtime/process.ts +11 -35
- package/src/runtime/transport-common.ts +37 -12
- package/src/skills.ts +15 -0
- package/src/runtime/spawn.ts +0 -190
package/src/index.ts
CHANGED
|
@@ -1,24 +1,63 @@
|
|
|
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
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
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);
|
|
35
|
+
expect(args).toEqual(['test', 'run']);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('appends --output json when includeOutputFlag is true', () => {
|
|
39
|
+
const args = buildOperationArgv(baseOperation, {}, {}, undefined, true);
|
|
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);
|
|
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);
|
|
58
|
+
expect(trueArgs).toEqual(['test', 'run', '--force']);
|
|
59
|
+
|
|
60
|
+
const falseArgs = buildOperationArgv(op, { force: false }, {}, undefined, false);
|
|
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);
|
|
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);
|
|
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);
|
|
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);
|
|
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);
|
|
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)).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)).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);
|
|
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);
|
|
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);
|
|
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);
|
|
149
|
+
expect(args).toEqual(['test', 'run', '--mode', 'fast']);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -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) {
|
package/src/runtime/errors.ts
CHANGED
|
@@ -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;
|
package/src/runtime/host.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
3
|
+
import { buildOperationArgv, resolveInvocation, type InvokeOptions, type OperationSpec, type SuperDocClientOptions } from './transport-common';
|
|
4
4
|
import { SuperDocCliError } from './errors';
|
|
5
5
|
|
|
6
6
|
type PendingRequest = {
|
|
@@ -27,6 +27,12 @@ const REQUIRED_FEATURES = ['cli.invoke', 'host.shutdown'];
|
|
|
27
27
|
|
|
28
28
|
const JSON_RPC_TIMEOUT_CODE = -32011;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Transport that communicates with a long-lived CLI host process over JSON-RPC stdio.
|
|
32
|
+
*
|
|
33
|
+
* The host process is spawned lazily on first invocation and kept alive for
|
|
34
|
+
* subsequent calls. Call {@link dispose} to gracefully shut it down.
|
|
35
|
+
*/
|
|
30
36
|
export class HostTransport {
|
|
31
37
|
private readonly cliBin: string;
|
|
32
38
|
private readonly env?: Record<string, string | undefined>;
|
|
@@ -43,19 +49,15 @@ export class HostTransport {
|
|
|
43
49
|
private connecting: Promise<void> | null = null;
|
|
44
50
|
private stopping = false;
|
|
45
51
|
|
|
46
|
-
constructor(options: {
|
|
47
|
-
cliBin: string;
|
|
48
|
-
env?: Record<string, string | undefined>;
|
|
49
|
-
host?: SuperDocHostOptions;
|
|
50
|
-
}) {
|
|
52
|
+
constructor(options: { cliBin: string } & SuperDocClientOptions) {
|
|
51
53
|
this.cliBin = options.cliBin;
|
|
52
54
|
this.env = options.env;
|
|
53
55
|
|
|
54
|
-
this.startupTimeoutMs = options.
|
|
55
|
-
this.shutdownTimeoutMs = options.
|
|
56
|
-
this.requestTimeoutMs = options.
|
|
57
|
-
this.watchdogTimeoutMs = options.
|
|
58
|
-
this.maxQueueDepth = options.
|
|
56
|
+
this.startupTimeoutMs = options.startupTimeoutMs ?? 5_000;
|
|
57
|
+
this.shutdownTimeoutMs = options.shutdownTimeoutMs ?? 5_000;
|
|
58
|
+
this.requestTimeoutMs = options.requestTimeoutMs;
|
|
59
|
+
this.watchdogTimeoutMs = options.watchdogTimeoutMs ?? 30_000;
|
|
60
|
+
this.maxQueueDepth = options.maxQueueDepth ?? 100;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
async connect(): Promise<void> {
|
package/src/runtime/process.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
17
|
+
private readonly transport: HostTransport;
|
|
25
18
|
|
|
26
19
|
constructor(options: SuperDocClientOptions = {}) {
|
|
27
|
-
const cliBin =
|
|
28
|
-
const transportMode = options.transport ?? 'spawn';
|
|
20
|
+
const cliBin = process.env.SUPERDOC_CLI_BIN ?? resolveEmbeddedCliBinary();
|
|
29
21
|
|
|
30
|
-
this.transport =
|
|
22
|
+
this.transport = new HostTransport({
|
|
31
23
|
cliBin,
|
|
32
|
-
|
|
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
|
|
45
|
+
export type { InvokeOptions, OperationParamSpec, OperationSpec, SuperDocClientOptions };
|
|
@@ -1,44 +1,52 @@
|
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
/** Top-level options for creating a {@link SuperDocClient}. */
|
|
34
|
+
export interface SuperDocClientOptions {
|
|
35
|
+
/** Extra environment variables merged into the CLI process environment. */
|
|
36
|
+
env?: Record<string, string | undefined>;
|
|
37
|
+
/** Timeout in milliseconds for the host process startup handshake. */
|
|
28
38
|
startupTimeoutMs?: number;
|
|
39
|
+
/** Timeout in milliseconds for graceful host shutdown. */
|
|
29
40
|
shutdownTimeoutMs?: number;
|
|
41
|
+
/** Default per-request timeout in milliseconds. */
|
|
30
42
|
requestTimeoutMs?: number;
|
|
43
|
+
/** Idle watchdog timeout in milliseconds. */
|
|
31
44
|
watchdogTimeoutMs?: number;
|
|
45
|
+
/** Maximum number of queued requests. */
|
|
32
46
|
maxQueueDepth?: number;
|
|
33
47
|
}
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
cliBin?: string;
|
|
37
|
-
env?: Record<string, string | undefined>;
|
|
38
|
-
transport?: SuperDocTransport;
|
|
39
|
-
host?: SuperDocHostOptions;
|
|
40
|
-
}
|
|
41
|
-
|
|
49
|
+
/** Resolved command and prefix args for spawning the CLI. */
|
|
42
50
|
export interface CliInvocation {
|
|
43
51
|
command: string;
|
|
44
52
|
prefixArgs: string[];
|
|
@@ -48,6 +56,12 @@ function hasExtension(filePath: string, extension: string): boolean {
|
|
|
48
56
|
return filePath.toLowerCase().endsWith(extension);
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Determine how to spawn the CLI binary based on its file extension.
|
|
61
|
+
*
|
|
62
|
+
* @param cliBin - Path to the CLI binary or script.
|
|
63
|
+
* @returns The shell command and any prefix arguments needed to execute it.
|
|
64
|
+
*/
|
|
51
65
|
export function resolveInvocation(cliBin: string): CliInvocation {
|
|
52
66
|
if (hasExtension(cliBin, '.js')) {
|
|
53
67
|
return { command: 'node', prefixArgs: [cliBin] };
|
|
@@ -109,6 +123,17 @@ function encodeParam(args: string[], spec: OperationParamSpec, value: unknown):
|
|
|
109
123
|
args.push(flag, String(value));
|
|
110
124
|
}
|
|
111
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Build the CLI argument vector for an operation invocation.
|
|
128
|
+
*
|
|
129
|
+
* @param operation - The operation spec describing the command and its parameters.
|
|
130
|
+
* @param params - User-supplied parameter values keyed by param name.
|
|
131
|
+
* @param options - Per-call invoke options (timeout, stdin).
|
|
132
|
+
* @param runtimeTimeoutMs - Default timeout from the runtime configuration.
|
|
133
|
+
* @param includeOutputFlag - Whether to append `--output json`.
|
|
134
|
+
* @returns The argument array ready to be passed to `spawn`.
|
|
135
|
+
* @throws {SuperDocCliError} With code `INVALID_ARGUMENT` if a required parameter is missing.
|
|
136
|
+
*/
|
|
112
137
|
export function buildOperationArgv(
|
|
113
138
|
operation: OperationSpec,
|
|
114
139
|
params: Record<string, unknown>,
|
package/src/skills.ts
CHANGED
|
@@ -18,6 +18,12 @@ function resolveSkillFilePath(skillName: string): string {
|
|
|
18
18
|
return filePath;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* List the names of all SDK skills bundled with this package.
|
|
23
|
+
*
|
|
24
|
+
* @returns Sorted array of skill names (without the `.md` extension).
|
|
25
|
+
* @throws {SuperDocCliError} With code `SKILL_IO_ERROR` if the skills directory cannot be read.
|
|
26
|
+
*/
|
|
21
27
|
export function listSkills(): string[] {
|
|
22
28
|
try {
|
|
23
29
|
return readdirSync(skillsDir)
|
|
@@ -35,6 +41,15 @@ export function listSkills(): string[] {
|
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Read the content of a bundled SDK skill by name.
|
|
46
|
+
*
|
|
47
|
+
* @param name - Skill name (e.g. `"editing-docx"`). Must match `[A-Za-z0-9][A-Za-z0-9_-]*`.
|
|
48
|
+
* @returns The skill file content as a UTF-8 string.
|
|
49
|
+
* @throws {SuperDocCliError} With code `INVALID_ARGUMENT` if the name is empty or contains invalid characters.
|
|
50
|
+
* @throws {SuperDocCliError} With code `SKILL_NOT_FOUND` if no skill with that name exists.
|
|
51
|
+
* @throws {SuperDocCliError} With code `SKILL_IO_ERROR` for other file-system read failures.
|
|
52
|
+
*/
|
|
38
53
|
export function getSkill(name: string): string {
|
|
39
54
|
const normalized = name.trim();
|
|
40
55
|
if (!normalized || !SKILL_NAME_RE.test(normalized)) {
|
package/src/runtime/spawn.ts
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { CONTRACT } from '../generated/contract';
|
|
3
|
-
import { SuperDocCliError } from './errors';
|
|
4
|
-
import { buildOperationArgv, resolveInvocation, type InvokeOptions, type OperationSpec } from './transport-common';
|
|
5
|
-
|
|
6
|
-
type CliEnvelopeSuccess = {
|
|
7
|
-
ok: true;
|
|
8
|
-
command: string;
|
|
9
|
-
data: Record<string, unknown>;
|
|
10
|
-
meta?: {
|
|
11
|
-
version?: string;
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type CliEnvelopeError = {
|
|
16
|
-
ok: false;
|
|
17
|
-
error: {
|
|
18
|
-
code: string;
|
|
19
|
-
message: string;
|
|
20
|
-
details?: unknown;
|
|
21
|
-
};
|
|
22
|
-
meta?: {
|
|
23
|
-
version?: string;
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
type CliEnvelope = CliEnvelopeSuccess | CliEnvelopeError;
|
|
28
|
-
|
|
29
|
-
function parseEnvelope(stdout: string, stderr: string): CliEnvelope {
|
|
30
|
-
const output = stdout || stderr;
|
|
31
|
-
if (!output.trim()) {
|
|
32
|
-
throw new SuperDocCliError('CLI returned no JSON envelope.', {
|
|
33
|
-
code: 'COMMAND_FAILED',
|
|
34
|
-
details: { stdout, stderr },
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const attempts: string[] = [output.trim()];
|
|
39
|
-
const lines = output.split(/\r?\n/);
|
|
40
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
41
|
-
if (!lines[index]?.trim().startsWith('{')) continue;
|
|
42
|
-
attempts.push(lines.slice(index).join('\n').trim());
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
for (const candidate of attempts) {
|
|
46
|
-
if (!candidate) continue;
|
|
47
|
-
try {
|
|
48
|
-
const parsed = JSON.parse(candidate) as CliEnvelope;
|
|
49
|
-
if (typeof parsed === 'object' && parsed != null && 'ok' in parsed) {
|
|
50
|
-
return parsed;
|
|
51
|
-
}
|
|
52
|
-
} catch {
|
|
53
|
-
// try next candidate
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
return JSON.parse(output.trim()) as CliEnvelope;
|
|
59
|
-
} catch (error) {
|
|
60
|
-
throw new SuperDocCliError('CLI returned invalid JSON envelope.', {
|
|
61
|
-
code: 'JSON_PARSE_ERROR',
|
|
62
|
-
details: {
|
|
63
|
-
stdout,
|
|
64
|
-
stderr,
|
|
65
|
-
message: error instanceof Error ? error.message : String(error),
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function parseSemver(version: string): [number, number, number] | null {
|
|
72
|
-
const core = version.split('-', 1)[0];
|
|
73
|
-
const [majorText, minorText, patchText] = core.split('.');
|
|
74
|
-
if (!majorText || !minorText || !patchText) return null;
|
|
75
|
-
|
|
76
|
-
const major = Number(majorText);
|
|
77
|
-
const minor = Number(minorText);
|
|
78
|
-
const patch = Number(patchText);
|
|
79
|
-
if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch)) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
return [major, minor, patch];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isVersionLessThan(actual: string, minimum: string): boolean {
|
|
86
|
-
const actualParsed = parseSemver(actual);
|
|
87
|
-
const minimumParsed = parseSemver(minimum);
|
|
88
|
-
if (!actualParsed || !minimumParsed) return false;
|
|
89
|
-
|
|
90
|
-
if (actualParsed[0] !== minimumParsed[0]) return actualParsed[0] < minimumParsed[0];
|
|
91
|
-
if (actualParsed[1] !== minimumParsed[1]) return actualParsed[1] < minimumParsed[1];
|
|
92
|
-
return actualParsed[2] < minimumParsed[2];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function assertCompatibleCliVersion(envelope: CliEnvelope): void {
|
|
96
|
-
const actualVersion = envelope.meta?.version;
|
|
97
|
-
const minimumVersion = CONTRACT.cli.minVersion;
|
|
98
|
-
if (!actualVersion || !minimumVersion) return;
|
|
99
|
-
if (actualVersion === '0.0.0') return;
|
|
100
|
-
|
|
101
|
-
if (isVersionLessThan(actualVersion, minimumVersion)) {
|
|
102
|
-
throw new SuperDocCliError(
|
|
103
|
-
`CLI version ${actualVersion} is older than minimum required ${minimumVersion}.`,
|
|
104
|
-
{
|
|
105
|
-
code: 'CLI_VERSION_UNSUPPORTED',
|
|
106
|
-
details: {
|
|
107
|
-
cliVersion: actualVersion,
|
|
108
|
-
minVersion: minimumVersion,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export class SpawnTransport {
|
|
116
|
-
private readonly cliBin: string;
|
|
117
|
-
private readonly env?: Record<string, string | undefined>;
|
|
118
|
-
|
|
119
|
-
constructor(options: { cliBin: string; env?: Record<string, string | undefined> }) {
|
|
120
|
-
this.cliBin = options.cliBin;
|
|
121
|
-
this.env = options.env;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async invoke<TData extends Record<string, unknown>>(
|
|
125
|
-
operation: OperationSpec,
|
|
126
|
-
params: Record<string, unknown> = {},
|
|
127
|
-
options: InvokeOptions = {},
|
|
128
|
-
): Promise<TData> {
|
|
129
|
-
const { command, prefixArgs } = resolveInvocation(this.cliBin);
|
|
130
|
-
const commandArgs = buildOperationArgv(operation, params, options, undefined, true);
|
|
131
|
-
const args: string[] = [...prefixArgs, ...commandArgs];
|
|
132
|
-
|
|
133
|
-
const spawned = spawn(command, args, {
|
|
134
|
-
env: {
|
|
135
|
-
...process.env,
|
|
136
|
-
...(this.env ?? {}),
|
|
137
|
-
},
|
|
138
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
let stdout = '';
|
|
142
|
-
let stderr = '';
|
|
143
|
-
|
|
144
|
-
const stdoutPromise = new Promise<void>((resolve) => {
|
|
145
|
-
spawned.stdout.on('data', (chunk) => {
|
|
146
|
-
stdout += String(chunk);
|
|
147
|
-
});
|
|
148
|
-
spawned.stdout.on('end', () => resolve());
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const stderrPromise = new Promise<void>((resolve) => {
|
|
152
|
-
spawned.stderr.on('data', (chunk) => {
|
|
153
|
-
stderr += String(chunk);
|
|
154
|
-
});
|
|
155
|
-
spawned.stderr.on('end', () => resolve());
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
if (options.stdinBytes) {
|
|
159
|
-
spawned.stdin.write(options.stdinBytes);
|
|
160
|
-
}
|
|
161
|
-
spawned.stdin.end();
|
|
162
|
-
|
|
163
|
-
const exitCode = await new Promise<number>((resolve, reject) => {
|
|
164
|
-
spawned.on('error', reject);
|
|
165
|
-
spawned.on('close', (code) => resolve(code ?? 1));
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
await Promise.all([stdoutPromise, stderrPromise]);
|
|
169
|
-
|
|
170
|
-
const envelope = parseEnvelope(stdout, stderr);
|
|
171
|
-
assertCompatibleCliVersion(envelope);
|
|
172
|
-
if (envelope.ok) {
|
|
173
|
-
return envelope.data as TData;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
throw new SuperDocCliError(envelope.error.message, {
|
|
177
|
-
code: envelope.error.code,
|
|
178
|
-
details: envelope.error.details,
|
|
179
|
-
exitCode,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async connect(): Promise<void> {
|
|
184
|
-
// no-op in one-shot spawn mode
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async dispose(): Promise<void> {
|
|
188
|
-
// no-op in one-shot spawn mode
|
|
189
|
-
}
|
|
190
|
-
}
|