@superdoc-dev/sdk 1.0.0-alpha.2 → 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
@@ -62,7 +62,15 @@ export function createSuperDocClient(options: SuperDocClientOptions = {}): Super
62
62
  return new SuperDocClient(options);
63
63
  }
64
64
 
65
- 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';
66
74
  export { SuperDocCliError } from './runtime/errors';
67
75
  export type {
68
76
  InvokeOptions,
@@ -70,3 +78,10 @@ export type {
70
78
  OperationParamSpec,
71
79
  SuperDocClientOptions,
72
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
+ });
@@ -31,12 +31,12 @@ describe('buildOperationArgv', () => {
31
31
  };
32
32
 
33
33
  test('starts with command segments', () => {
34
- const args = buildOperationArgv(baseOperation, {}, {}, undefined, false);
34
+ const args = buildOperationArgv(baseOperation, {}, {}, undefined, false, undefined);
35
35
  expect(args).toEqual(['test', 'run']);
36
36
  });
37
37
 
38
38
  test('appends --output json when includeOutputFlag is true', () => {
39
- const args = buildOperationArgv(baseOperation, {}, {}, undefined, true);
39
+ const args = buildOperationArgv(baseOperation, {}, {}, undefined, true, undefined);
40
40
  expect(args).toEqual(['test', 'run', '--output', 'json']);
41
41
  });
42
42
 
@@ -45,7 +45,7 @@ describe('buildOperationArgv', () => {
45
45
  ...baseOperation,
46
46
  params: [{ name: 'session', kind: 'flag', flag: 'session', type: 'string' }],
47
47
  };
48
- const args = buildOperationArgv(op, { session: 'my-session' }, {}, undefined, false);
48
+ const args = buildOperationArgv(op, { session: 'my-session' }, {}, undefined, false, undefined);
49
49
  expect(args).toEqual(['test', 'run', '--session', 'my-session']);
50
50
  });
51
51
 
@@ -54,10 +54,10 @@ describe('buildOperationArgv', () => {
54
54
  ...baseOperation,
55
55
  params: [{ name: 'force', kind: 'flag', flag: 'force', type: 'boolean' }],
56
56
  };
57
- const trueArgs = buildOperationArgv(op, { force: true }, {}, undefined, false);
57
+ const trueArgs = buildOperationArgv(op, { force: true }, {}, undefined, false, undefined);
58
58
  expect(trueArgs).toEqual(['test', 'run', '--force']);
59
59
 
60
- const falseArgs = buildOperationArgv(op, { force: false }, {}, undefined, false);
60
+ const falseArgs = buildOperationArgv(op, { force: false }, {}, undefined, false, undefined);
61
61
  expect(falseArgs).toEqual(['test', 'run']);
62
62
  });
63
63
 
@@ -66,7 +66,7 @@ describe('buildOperationArgv', () => {
66
66
  ...baseOperation,
67
67
  params: [{ name: 'limit', kind: 'flag', flag: 'limit', type: 'number' }],
68
68
  };
69
- const args = buildOperationArgv(op, { limit: 10 }, {}, undefined, false);
69
+ const args = buildOperationArgv(op, { limit: 10 }, {}, undefined, false, undefined);
70
70
  expect(args).toEqual(['test', 'run', '--limit', '10']);
71
71
  });
72
72
 
@@ -75,7 +75,7 @@ describe('buildOperationArgv', () => {
75
75
  ...baseOperation,
76
76
  params: [{ name: 'query', kind: 'jsonFlag', flag: 'query-json', type: 'json' }],
77
77
  };
78
- const args = buildOperationArgv(op, { query: { type: 'text' } }, {}, undefined, false);
78
+ const args = buildOperationArgv(op, { query: { type: 'text' } }, {}, undefined, false, undefined);
79
79
  expect(args).toEqual(['test', 'run', '--query-json', '{"type":"text"}']);
80
80
  });
81
81
 
@@ -84,7 +84,7 @@ describe('buildOperationArgv', () => {
84
84
  ...baseOperation,
85
85
  params: [{ name: 'doc', kind: 'doc', type: 'string' }],
86
86
  };
87
- const args = buildOperationArgv(op, { doc: '/path/to/file.docx' }, {}, undefined, false);
87
+ const args = buildOperationArgv(op, { doc: '/path/to/file.docx' }, {}, undefined, false, undefined);
88
88
  expect(args).toEqual(['test', 'run', '/path/to/file.docx']);
89
89
  });
90
90
 
@@ -93,7 +93,7 @@ describe('buildOperationArgv', () => {
93
93
  ...baseOperation,
94
94
  params: [{ name: 'include', kind: 'flag', flag: 'include', type: 'string[]' }],
95
95
  };
96
- const args = buildOperationArgv(op, { include: ['a', 'b'] }, {}, undefined, false);
96
+ const args = buildOperationArgv(op, { include: ['a', 'b'] }, {}, undefined, false, undefined);
97
97
  expect(args).toEqual(['test', 'run', '--include', 'a', '--include', 'b']);
98
98
  });
99
99
 
@@ -102,7 +102,7 @@ describe('buildOperationArgv', () => {
102
102
  ...baseOperation,
103
103
  params: [{ name: 'session', kind: 'flag', flag: 'session', type: 'string' }],
104
104
  };
105
- const args = buildOperationArgv(op, {}, {}, undefined, false);
105
+ const args = buildOperationArgv(op, {}, {}, undefined, false, undefined);
106
106
  expect(args).toEqual(['test', 'run']);
107
107
  });
108
108
 
@@ -111,7 +111,7 @@ describe('buildOperationArgv', () => {
111
111
  ...baseOperation,
112
112
  params: [{ name: 'doc', kind: 'doc', type: 'string', required: true }],
113
113
  };
114
- expect(() => buildOperationArgv(op, {}, {}, undefined, false)).toThrow('Missing required parameter: doc');
114
+ expect(() => buildOperationArgv(op, {}, {}, undefined, false, undefined)).toThrow('Missing required parameter: doc');
115
115
  });
116
116
 
117
117
  test('throws on non-array value for string[] parameter', () => {
@@ -119,24 +119,24 @@ describe('buildOperationArgv', () => {
119
119
  ...baseOperation,
120
120
  params: [{ name: 'include', kind: 'flag', flag: 'include', type: 'string[]' }],
121
121
  };
122
- expect(() => buildOperationArgv(op, { include: 'not-an-array' }, {}, undefined, false)).toThrow(
122
+ expect(() => buildOperationArgv(op, { include: 'not-an-array' }, {}, undefined, false, undefined)).toThrow(
123
123
  'must be an array',
124
124
  );
125
125
  });
126
126
 
127
127
  test('appends timeout from invoke options', () => {
128
- const args = buildOperationArgv(baseOperation, {}, { timeoutMs: 5000 }, undefined, false);
128
+ const args = buildOperationArgv(baseOperation, {}, { timeoutMs: 5000 }, undefined, false, undefined);
129
129
  expect(args).toEqual(['test', 'run', '--timeout-ms', '5000']);
130
130
  });
131
131
 
132
132
  test('invoke timeout takes precedence over runtime timeout', () => {
133
- const args = buildOperationArgv(baseOperation, {}, { timeoutMs: 3000 }, 10000, false);
133
+ const args = buildOperationArgv(baseOperation, {}, { timeoutMs: 3000 }, 10000, false, undefined);
134
134
  expect(args).toContain('3000');
135
135
  expect(args).not.toContain('10000');
136
136
  });
137
137
 
138
138
  test('falls back to runtime timeout when invoke timeout is absent', () => {
139
- const args = buildOperationArgv(baseOperation, {}, {}, 7000, false);
139
+ const args = buildOperationArgv(baseOperation, {}, {}, 7000, false, undefined);
140
140
  expect(args).toEqual(['test', 'run', '--timeout-ms', '7000']);
141
141
  });
142
142
 
@@ -145,7 +145,30 @@ describe('buildOperationArgv', () => {
145
145
  ...baseOperation,
146
146
  params: [{ name: 'mode', kind: 'flag', type: 'string' }],
147
147
  };
148
- const args = buildOperationArgv(op, { mode: 'fast' }, {}, undefined, false);
148
+ const args = buildOperationArgv(op, { mode: 'fast' }, {}, undefined, false, undefined);
149
149
  expect(args).toEqual(['test', 'run', '--mode', 'fast']);
150
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
+ });
151
174
  });
@@ -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 SuperDocClientOptions } 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,6 +31,7 @@ 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
 
@@ -41,6 +49,7 @@ export class HostTransport {
41
49
  private readonly requestTimeoutMs?: number;
42
50
  private readonly watchdogTimeoutMs: number;
43
51
  private readonly maxQueueDepth: number;
52
+ private readonly defaultChangeMode?: ChangeMode;
44
53
 
45
54
  private child: ChildProcessWithoutNullStreams | null = null;
46
55
  private stdoutReader: ReadlineInterface | null = null;
@@ -58,6 +67,13 @@ export class HostTransport {
58
67
  this.requestTimeoutMs = options.requestTimeoutMs;
59
68
  this.watchdogTimeoutMs = options.watchdogTimeoutMs ?? 30_000;
60
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;
61
77
  }
62
78
 
63
79
  async connect(): Promise<void> {
@@ -99,7 +115,7 @@ export class HostTransport {
99
115
  ): Promise<TData> {
100
116
  await this.ensureConnected();
101
117
 
102
- const argv = buildOperationArgv(operation, params, options, this.requestTimeoutMs, false);
118
+ const argv = buildOperationArgv(operation, params, options, this.requestTimeoutMs, false, this.defaultChangeMode);
103
119
  const stdinBase64 = options.stdinBytes ? Buffer.from(options.stdinBytes).toString('base64') : '';
104
120
  const watchdogTimeout = this.resolveWatchdogTimeout(options.timeoutMs);
105
121
 
@@ -17,7 +17,7 @@ export class SuperDocRuntime {
17
17
  private readonly transport: HostTransport;
18
18
 
19
19
  constructor(options: SuperDocClientOptions = {}) {
20
- const cliBin = process.env.SUPERDOC_CLI_BIN ?? resolveEmbeddedCliBinary();
20
+ const cliBin = options.env?.SUPERDOC_CLI_BIN ?? process.env.SUPERDOC_CLI_BIN ?? resolveEmbeddedCliBinary();
21
21
 
22
22
  this.transport = new HostTransport({
23
23
  cliBin,
@@ -30,6 +30,8 @@ export interface InvokeOptions {
30
30
  stdinBytes?: Uint8Array;
31
31
  }
32
32
 
33
+ export type ChangeMode = 'direct' | 'tracked';
34
+
33
35
  /** Top-level options for creating a {@link SuperDocClient}. */
34
36
  export interface SuperDocClientOptions {
35
37
  /** Extra environment variables merged into the CLI process environment. */
@@ -44,6 +46,8 @@ export interface SuperDocClientOptions {
44
46
  watchdogTimeoutMs?: number;
45
47
  /** Maximum number of queued requests. */
46
48
  maxQueueDepth?: number;
49
+ /** Default change mode for mutation operations that support `changeMode`. */
50
+ defaultChangeMode?: ChangeMode;
47
51
  }
48
52
 
49
53
  /** Resolved command and prefix args for spawning the CLI. */
@@ -140,10 +144,16 @@ export function buildOperationArgv(
140
144
  options: InvokeOptions,
141
145
  runtimeTimeoutMs: number | undefined,
142
146
  includeOutputFlag: boolean,
147
+ defaultChangeMode?: ChangeMode,
143
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
+
144
154
  const args: string[] = [...operation.command];
145
155
  for (const param of operation.params) {
146
- encodeParam(args, param, params[param.name]);
156
+ encodeParam(args, param, normalizedParams[param.name]);
147
157
  }
148
158
 
149
159
  const timeoutMs = options.timeoutMs ?? runtimeTimeoutMs;
package/src/skills.ts CHANGED
@@ -1,10 +1,34 @@
1
- import { readFileSync, readdirSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { SuperDocCliError } from './runtime/errors';
5
6
 
6
7
  const skillsDir = path.resolve(fileURLToPath(new URL('../skills', import.meta.url)));
7
8
  const SKILL_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
9
+ const SUPPORTED_SKILL_RUNTIMES = ['claude'] as const;
10
+ const SUPPORTED_INSTALL_SCOPES = ['project', 'user'] as const;
11
+
12
+ type SkillRuntime = (typeof SUPPORTED_SKILL_RUNTIMES)[number];
13
+ type SkillInstallScope = (typeof SUPPORTED_INSTALL_SCOPES)[number];
14
+
15
+ export interface InstallSkillOptions {
16
+ runtime?: SkillRuntime;
17
+ scope?: SkillInstallScope;
18
+ targetDir?: string;
19
+ cwd?: string;
20
+ homeDir?: string;
21
+ overwrite?: boolean;
22
+ }
23
+
24
+ export interface InstalledSkillResult {
25
+ name: string;
26
+ runtime: SkillRuntime;
27
+ scope: SkillInstallScope | 'custom';
28
+ path: string;
29
+ written: boolean;
30
+ overwritten: boolean;
31
+ }
8
32
 
9
33
  function resolveSkillFilePath(skillName: string): string {
10
34
  const filePath = path.resolve(skillsDir, `${skillName}.md`);
@@ -18,6 +42,17 @@ function resolveSkillFilePath(skillName: string): string {
18
42
  return filePath;
19
43
  }
20
44
 
45
+ function normalizeSkillName(name: string): string {
46
+ const normalized = name.trim();
47
+ if (!normalized || !SKILL_NAME_RE.test(normalized)) {
48
+ throw new SuperDocCliError('Skill name is required.', {
49
+ code: 'INVALID_ARGUMENT',
50
+ details: { name },
51
+ });
52
+ }
53
+ return normalized;
54
+ }
55
+
21
56
  /**
22
57
  * List the names of all SDK skills bundled with this package.
23
58
  *
@@ -51,13 +86,7 @@ export function listSkills(): string[] {
51
86
  * @throws {SuperDocCliError} With code `SKILL_IO_ERROR` for other file-system read failures.
52
87
  */
53
88
  export function getSkill(name: string): string {
54
- const normalized = name.trim();
55
- if (!normalized || !SKILL_NAME_RE.test(normalized)) {
56
- throw new SuperDocCliError('Skill name is required.', {
57
- code: 'INVALID_ARGUMENT',
58
- details: { name },
59
- });
60
- }
89
+ const normalized = normalizeSkillName(name);
61
90
 
62
91
  const filePath = resolveSkillFilePath(normalized);
63
92
  try {
@@ -89,3 +118,78 @@ export function getSkill(name: string): string {
89
118
  });
90
119
  }
91
120
  }
121
+
122
+ /**
123
+ * Install a bundled SDK skill into an agent runtime directory.
124
+ *
125
+ * Defaults to Claude's project-local skill path: `./.claude/skills/<name>/SKILL.md`.
126
+ */
127
+ export function installSkill(name: string, options: InstallSkillOptions = {}): InstalledSkillResult {
128
+ const normalizedName = normalizeSkillName(name);
129
+ const runtime = options.runtime ?? 'claude';
130
+ if (!SUPPORTED_SKILL_RUNTIMES.includes(runtime)) {
131
+ throw new SuperDocCliError('Unsupported skill runtime.', {
132
+ code: 'INVALID_ARGUMENT',
133
+ details: { runtime, supportedRuntimes: SUPPORTED_SKILL_RUNTIMES },
134
+ });
135
+ }
136
+
137
+ const scope = options.scope ?? 'project';
138
+ if (!SUPPORTED_INSTALL_SCOPES.includes(scope)) {
139
+ throw new SuperDocCliError('Unsupported skill install scope.', {
140
+ code: 'INVALID_ARGUMENT',
141
+ details: { scope, supportedScopes: SUPPORTED_INSTALL_SCOPES },
142
+ });
143
+ }
144
+
145
+ const skillsRoot =
146
+ options.targetDir !== undefined
147
+ ? path.resolve(options.targetDir)
148
+ : scope === 'user'
149
+ ? path.resolve(options.homeDir ?? os.homedir(), '.claude', 'skills')
150
+ : path.resolve(options.cwd ?? process.cwd(), '.claude', 'skills');
151
+ const skillFile = path.join(skillsRoot, normalizedName, 'SKILL.md');
152
+ const overwrite = options.overwrite ?? true;
153
+ const alreadyExists = existsSync(skillFile);
154
+
155
+ if (!overwrite && alreadyExists) {
156
+ return {
157
+ name: normalizedName,
158
+ runtime,
159
+ scope: options.targetDir !== undefined ? 'custom' : scope,
160
+ path: skillFile,
161
+ written: false,
162
+ overwritten: false,
163
+ };
164
+ }
165
+
166
+ try {
167
+ const content = getSkill(name);
168
+ mkdirSync(path.dirname(skillFile), { recursive: true });
169
+ writeFileSync(skillFile, content, 'utf8');
170
+ } catch (error) {
171
+ if (error instanceof SuperDocCliError) {
172
+ throw error;
173
+ }
174
+
175
+ throw new SuperDocCliError('Unable to install SDK skill.', {
176
+ code: 'SKILL_IO_ERROR',
177
+ details: {
178
+ name: normalizedName,
179
+ runtime,
180
+ scope: options.targetDir !== undefined ? 'custom' : scope,
181
+ path: skillFile,
182
+ message: error instanceof Error ? error.message : String(error),
183
+ },
184
+ });
185
+ }
186
+
187
+ return {
188
+ name: normalizedName,
189
+ runtime,
190
+ scope: options.targetDir !== undefined ? 'custom' : scope,
191
+ path: skillFile,
192
+ written: true,
193
+ overwritten: alreadyExists,
194
+ };
195
+ }