@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/package.json +8 -7
- package/skills/editing-docx.md +5 -1
- package/src/__tests__/skills.test.ts +74 -1
- package/src/__tests__/tools.test.ts +96 -0
- package/src/generated/client.ts +492 -0
- package/src/generated/contract.ts +2556 -0
- package/src/index.ts +16 -1
- package/src/runtime/__tests__/process.test.ts +38 -0
- package/src/runtime/__tests__/transport-common.test.ts +39 -16
- package/src/runtime/host.ts +18 -2
- package/src/runtime/process.ts +1 -1
- package/src/runtime/transport-common.ts +11 -1
- package/src/skills.ts +112 -8
- package/src/tools.ts +701 -0
- package/skills/.gitkeep +0 -0
- package/src/generated/DO_NOT_EDIT +0 -2
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
|
});
|
package/src/runtime/host.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
package/src/runtime/process.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
+
}
|