@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/package.json CHANGED
@@ -1,22 +1,24 @@
1
1
  {
2
2
  "name": "@superdoc-dev/sdk",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
7
7
  "module": "./src/index.ts",
8
8
  "files": [
9
9
  "src",
10
- "skills"
10
+ "skills",
11
+ "tools"
11
12
  ],
12
13
  "optionalDependencies": {
13
- "@superdoc-dev/sdk-darwin-arm64": "1.0.0-alpha.1",
14
- "@superdoc-dev/sdk-darwin-x64": "1.0.0-alpha.1",
15
- "@superdoc-dev/sdk-linux-arm64": "1.0.0-alpha.1",
16
- "@superdoc-dev/sdk-linux-x64": "1.0.0-alpha.1",
17
- "@superdoc-dev/sdk-windows-x64": "1.0.0-alpha.1"
14
+ "@superdoc-dev/sdk-darwin-arm64": "1.0.0-alpha.3",
15
+ "@superdoc-dev/sdk-darwin-x64": "1.0.0-alpha.3",
16
+ "@superdoc-dev/sdk-linux-arm64": "1.0.0-alpha.3",
17
+ "@superdoc-dev/sdk-linux-x64": "1.0.0-alpha.3",
18
+ "@superdoc-dev/sdk-windows-x64": "1.0.0-alpha.3"
18
19
  },
19
20
  "devDependencies": {
21
+ "@types/bun": "^1.3.8",
20
22
  "@types/node": "22.19.2",
21
23
  "typescript": "^5.9.2"
22
24
  },
@@ -1,6 +1,6 @@
1
1
  ---
2
- name: editing-docx
3
- description: Session-first SuperDoc CLI workflows for querying and editing DOCX files (find, inspect, comment, replace, format, and close safely).
2
+ name: superdoc
3
+ description: Session-first SuperDoc CLI workflows for querying and editing DOCX files (find/inspect, comments lifecycle, text mutations, tracked-changes review, and close safely).
4
4
  ---
5
5
 
6
6
  # SuperDoc Playground Skill
@@ -14,9 +14,9 @@ Use this skill for manual DOCX exploration/editing in Codex CLI or Claude CLI.
14
14
  3. Treat CLI envelopes as truth (`ok`, `data`, `error`).
15
15
  4. Re-query before mutating if target addresses might be stale.
16
16
  5. If matches are ambiguous, ask user which match(es) to modify.
17
- 6. End every workflow with an explicit close decision:
18
- - `close --save --out ...`
19
- - `close --save --in-place`
17
+ 6. End every workflow with an explicit persistence + close decision:
18
+ - `save --out ...` then `close`
19
+ - `save --in-place` then `close`
20
20
  - `close --discard`
21
21
 
22
22
  ## CLI Invocation
@@ -34,6 +34,8 @@ superdoc open ./contract.docx --output json
34
34
  ```
35
35
  2. Query and inspect:
36
36
  ```bash
37
+ superdoc describe --output json
38
+ superdoc describe command doc.find --output json
37
39
  superdoc info --output json
38
40
  superdoc find --type text --pattern "termination" --output json
39
41
  superdoc get-node --address-json '{"kind":"block","nodeType":"paragraph","nodeId":"p1"}' --output json
@@ -48,25 +50,28 @@ superdoc replace --target-json '{"kind":"text","blockId":"p1","range":{"start":1
48
50
  ```bash
49
51
  superdoc find --type text --pattern "Updated clause text" --output json
50
52
  ```
51
- 5. Close:
53
+ 5. Save and close:
52
54
  ```bash
53
- superdoc close --save --out ./contract.reviewed.docx --output json
55
+ superdoc save --out ./contract.reviewed.docx --output json
56
+ superdoc close --output json
54
57
  ```
55
58
 
56
59
  ## Command Patterns (Session Mode)
57
60
 
58
61
  ### Session lifecycle
59
- - `superdoc open <doc> [--session <id>] [--replace] --output json`
62
+ - `superdoc open <doc> [--session <id>] --output json`
60
63
  - `superdoc status [--session <id>] --output json`
64
+ - `superdoc save [--in-place] [--out <path>] [--force] --output json`
65
+ - `superdoc close [--discard] --output json`
61
66
  - `superdoc session list --output json`
67
+ - `superdoc session save <id> [--in-place] [--out <path>] [--force] --output json`
62
68
  - `superdoc session use <id> --output json`
63
69
  - `superdoc session set-default <id> --output json`
64
70
  - `superdoc session close <id> --discard --output json`
65
- - `superdoc close --save --in-place --output json`
66
- - `superdoc close --save --out <path> --output json`
67
- - `superdoc close --discard --output json`
68
71
 
69
72
  ### Query
73
+ - `superdoc describe --output json`
74
+ - `superdoc describe command <operationId> --output json`
70
75
  - `superdoc info --output json`
71
76
  - `superdoc find --type text --pattern "<text>" --output json`
72
77
  - `superdoc find --query-json '{...}' --output json`
@@ -74,9 +79,33 @@ superdoc close --save --out ./contract.reviewed.docx --output json
74
79
  - `superdoc get-node-by-id --id <nodeId> [--node-type <type>] --output json`
75
80
 
76
81
  ### Mutate
82
+ - `superdoc create paragraph [--input-json '{...}'] [--text "..."] [--at document-start|document-end] [--before-address-json '{...}'] [--after-address-json '{...}'] [--dry-run] [--change-mode direct|tracked] --output json`
77
83
  - `superdoc comments add --target-json '{...}' --text "..." --output json`
78
- - `superdoc replace --target-json '{...}' --text "..." [--dry-run] --output json`
79
- - `superdoc format bold --target-json '{...}' [--dry-run] --output json`
84
+ - `superdoc comments edit --id <commentId> --text "..." [--expected-revision <n>] [--force] --output json`
85
+ - `superdoc comments reply --parent-id <commentId> --text "..." [--expected-revision <n>] [--force] --output json`
86
+ - `superdoc comments move --id <commentId> --target-json '{...}' [--expected-revision <n>] [--force] --output json`
87
+ - `superdoc comments resolve --id <commentId> [--expected-revision <n>] [--force] --output json`
88
+ - `superdoc comments remove --id <commentId> [--expected-revision <n>] [--force] --output json`
89
+ - `superdoc comments set-internal --id <commentId> --is-internal <true|false> [--expected-revision <n>] [--force] --output json`
90
+ - `superdoc insert [--target-json '{...}'] --text "..." [--dry-run] [--change-mode direct|tracked] --output json`
91
+ - `superdoc replace --target-json '{...}' --text "..." [--dry-run] [--change-mode direct|tracked] --output json`
92
+ - `superdoc delete --target-json '{...}' [--dry-run] [--change-mode direct|tracked] --output json`
93
+ - `superdoc format bold --target-json '{...}' [--dry-run] [--change-mode direct|tracked] --output json`
94
+
95
+ ### Comments read/navigation
96
+ - `superdoc comments get --id <commentId> --output json`
97
+ - `superdoc comments list [--include-resolved <true|false>] --output json`
98
+ - `superdoc comments set-active --id <commentId> --output json`
99
+ - `superdoc comments set-active --clear --output json`
100
+ - `superdoc comments go-to --id <commentId> --output json`
101
+
102
+ ### Track changes
103
+ - `superdoc track-changes list [--type insert|delete|format] [--limit <n>] [--offset <n>] --output json`
104
+ - `superdoc track-changes get --id <trackedChangeId> --output json`
105
+ - `superdoc track-changes accept --id <trackedChangeId> [--expected-revision <n>] [--force] --output json`
106
+ - `superdoc track-changes reject --id <trackedChangeId> [--expected-revision <n>] [--force] --output json`
107
+ - `superdoc track-changes accept-all [--expected-revision <n>] [--force] --output json`
108
+ - `superdoc track-changes reject-all [--expected-revision <n>] [--force] --output json`
80
109
 
81
110
  ### Stateless fallback
82
111
  If a command includes `<doc>` (or `--doc`), it runs statelessly for that call.
@@ -84,21 +113,36 @@ For stateless mutate commands, provide `--out <path>`.
84
113
 
85
114
  ## Target Selection Guidance
86
115
 
116
+ - Prefer structural creation for new block insertion:
117
+ - `create paragraph` creates a new paragraph block and returns structural handles (`paragraph`, `insertionPoint`).
87
118
  - Prefer `find --type text --pattern ...` first.
88
119
  - Use `data.result.context[*].textRanges[*]` as `--target-json` for text edits/comments.
120
+ - `insert` may omit `--target-json`; it defaults to the first editable insertion point (first paragraph start when available).
89
121
  - When multiple matches exist, apply a deterministic policy:
90
122
  - first match only, or
91
123
  - first N matches, or
92
124
  - user-selected match indexes.
93
125
  - For uncertain targets, run `get-node` before mutate.
94
126
 
127
+ ## Mutation Mode Guidance
128
+
129
+ - Use `--change-mode direct|tracked` to control whether edits create tracked changes.
130
+ - `create paragraph` supports `--change-mode tracked` for tracked structural creation.
131
+ - `replace`, `insert`, `delete`, and `format bold` support `--change-mode tracked`.
132
+ - `track-changes *` commands are for review lifecycle (list/get/accept/reject), not content insertion.
133
+ - Deterministic tracked-change outcomes:
134
+ - unknown/stale ids -> `TRACK_CHANGE_NOT_FOUND`
135
+ - no applicable `accept-all`/`reject-all` change -> `NO_OP` receipt
136
+ - missing tracking capability -> `TRACK_CHANGE_COMMAND_UNAVAILABLE`
137
+
95
138
  ## Scenario Prompts (Copy/Paste)
96
139
 
97
140
  1. Open `./contracts/msa.docx`, find `termination`, add comments to the first 2 matches asking for clearer notice period, then save to `./contracts/msa.reviewed.docx`.
98
141
  2. Use the active session to find `governing law`, inspect the top match node, then add a concise legal-risk comment.
99
142
  3. Open two sessions (`vendor-a`, `vendor-b`) on different docs, switch between them, and add one comment in each.
100
143
  4. Run a redline pass replacing `shall` with `must` for the first 3 matches: dry-run first, then apply.
101
- 5. Open from stdin (`open -`), run `find`, add one comment, then close and save to a chosen output path.
144
+ 5. Run `track-changes list`, inspect one change with `track-changes get`, then accept it by id.
145
+ 6. Open from stdin (`open -`), run `find`, add one comment, then `save --out` and `close`.
102
146
 
103
147
  ## Collaboration (Node-Only, Optional)
104
148
 
@@ -0,0 +1,166 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { SuperDocCliError } from '../runtime/errors';
6
+ import { getSkill, installSkill, listSkills } from '../skills';
7
+
8
+ describe('listSkills', () => {
9
+ test('returns an array of skill names', () => {
10
+ const skills = listSkills();
11
+ expect(Array.isArray(skills)).toBe(true);
12
+ expect(skills.length).toBeGreaterThan(0);
13
+ });
14
+
15
+ test('includes the editing-docx skill', () => {
16
+ const skills = listSkills();
17
+ expect(skills).toContain('editing-docx');
18
+ });
19
+
20
+ test('returns sorted results', () => {
21
+ const skills = listSkills();
22
+ const sorted = [...skills].sort();
23
+ expect(skills).toEqual(sorted);
24
+ });
25
+
26
+ test('does not include file extensions', () => {
27
+ const skills = listSkills();
28
+ for (const name of skills) {
29
+ expect(name).not.toContain('.md');
30
+ }
31
+ });
32
+ });
33
+
34
+ describe('getSkill', () => {
35
+ test('returns content for a valid skill name', () => {
36
+ const content = getSkill('editing-docx');
37
+ expect(typeof content).toBe('string');
38
+ expect(content.length).toBeGreaterThan(0);
39
+ });
40
+
41
+ test('throws INVALID_ARGUMENT for empty name', () => {
42
+ expect(() => getSkill('')).toThrow(SuperDocCliError);
43
+ try {
44
+ getSkill('');
45
+ } catch (error) {
46
+ expect((error as SuperDocCliError).code).toBe('INVALID_ARGUMENT');
47
+ }
48
+ });
49
+
50
+ test('throws INVALID_ARGUMENT for whitespace-only name', () => {
51
+ expect(() => getSkill(' ')).toThrow(SuperDocCliError);
52
+ try {
53
+ getSkill(' ');
54
+ } catch (error) {
55
+ expect((error as SuperDocCliError).code).toBe('INVALID_ARGUMENT');
56
+ }
57
+ });
58
+
59
+ test('throws INVALID_ARGUMENT for names with path traversal characters', () => {
60
+ const maliciousNames = ['../etc/passwd', './local', 'foo/bar', 'a b c', 'skill.md'];
61
+
62
+ for (const name of maliciousNames) {
63
+ expect(() => getSkill(name)).toThrow(SuperDocCliError);
64
+ try {
65
+ getSkill(name);
66
+ } catch (error) {
67
+ expect((error as SuperDocCliError).code).toBe('INVALID_ARGUMENT');
68
+ }
69
+ }
70
+ });
71
+
72
+ test('throws SKILL_NOT_FOUND for nonexistent skill', () => {
73
+ expect(() => getSkill('nonexistent-skill-name-xyz')).toThrow(SuperDocCliError);
74
+ try {
75
+ getSkill('nonexistent-skill-name-xyz');
76
+ } catch (error) {
77
+ const sdError = error as SuperDocCliError;
78
+ expect(sdError.code).toBe('SKILL_NOT_FOUND');
79
+ expect((sdError.details as { available: string[] }).available).toContain('editing-docx');
80
+ }
81
+ });
82
+
83
+ test('accepts valid skill names with hyphens and underscores', () => {
84
+ // These should not throw INVALID_ARGUMENT (they may throw SKILL_NOT_FOUND)
85
+ const validNames = ['a', 'my-skill', 'my_skill', 'Skill1', '1skill'];
86
+
87
+ for (const name of validNames) {
88
+ try {
89
+ getSkill(name);
90
+ } catch (error) {
91
+ // SKILL_NOT_FOUND is expected; INVALID_ARGUMENT means the regex is wrong
92
+ expect((error as SuperDocCliError).code).not.toBe('INVALID_ARGUMENT');
93
+ }
94
+ }
95
+ });
96
+ });
97
+
98
+ describe('installSkill', () => {
99
+ test('installs bundled skill to Claude project skills directory', async () => {
100
+ const workdir = await mkdtemp(path.join(tmpdir(), 'superdoc-sdk-skills-project-'));
101
+ try {
102
+ const result = installSkill('editing-docx', { cwd: workdir });
103
+ const expectedPath = path.join(workdir, '.claude', 'skills', 'editing-docx', 'SKILL.md');
104
+ const content = await readFile(expectedPath, 'utf8');
105
+
106
+ expect(result.path).toBe(expectedPath);
107
+ expect(result.scope).toBe('project');
108
+ expect(result.written).toBe(true);
109
+ expect(result.overwritten).toBe(false);
110
+ expect(content).toContain('superdoc open');
111
+ } finally {
112
+ await rm(workdir, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ test('installs bundled skill to Claude user skills directory', async () => {
117
+ const homeDir = await mkdtemp(path.join(tmpdir(), 'superdoc-sdk-skills-user-'));
118
+ try {
119
+ const result = installSkill('editing-docx', { scope: 'user', homeDir });
120
+ const expectedPath = path.join(homeDir, '.claude', 'skills', 'editing-docx', 'SKILL.md');
121
+ const content = await readFile(expectedPath, 'utf8');
122
+
123
+ expect(result.path).toBe(expectedPath);
124
+ expect(result.scope).toBe('user');
125
+ expect(result.written).toBe(true);
126
+ expect(result.overwritten).toBe(false);
127
+ expect(content).toContain('superdoc open');
128
+ } finally {
129
+ await rm(homeDir, { recursive: true, force: true });
130
+ }
131
+ });
132
+
133
+ test('returns without writing when overwrite is false and target exists', async () => {
134
+ const workdir = await mkdtemp(path.join(tmpdir(), 'superdoc-sdk-skills-no-overwrite-'));
135
+ const skillFile = path.join(workdir, '.claude', 'skills', 'editing-docx', 'SKILL.md');
136
+ try {
137
+ await mkdir(path.dirname(skillFile), { recursive: true });
138
+ await writeFile(skillFile, 'custom-content', 'utf8');
139
+ const result = installSkill('editing-docx', { cwd: workdir, overwrite: false });
140
+ const content = await readFile(skillFile, 'utf8');
141
+
142
+ expect(result.written).toBe(false);
143
+ expect(result.overwritten).toBe(false);
144
+ expect(content).toBe('custom-content');
145
+ } finally {
146
+ await rm(workdir, { recursive: true, force: true });
147
+ }
148
+ });
149
+
150
+ test('overwrites existing target by default', async () => {
151
+ const workdir = await mkdtemp(path.join(tmpdir(), 'superdoc-sdk-skills-overwrite-'));
152
+ const skillFile = path.join(workdir, '.claude', 'skills', 'editing-docx', 'SKILL.md');
153
+ try {
154
+ await mkdir(path.dirname(skillFile), { recursive: true });
155
+ await writeFile(skillFile, 'old-content', 'utf8');
156
+ const result = installSkill('editing-docx', { cwd: workdir });
157
+ const content = await readFile(skillFile, 'utf8');
158
+
159
+ expect(result.written).toBe(true);
160
+ expect(result.overwritten).toBe(true);
161
+ expect(content).toContain('superdoc open');
162
+ } finally {
163
+ await rm(workdir, { recursive: true, force: true });
164
+ }
165
+ });
166
+ });
@@ -0,0 +1,96 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { dispatchSuperDocTool, resolveToolOperation } from '../tools';
3
+ import { SuperDocCliError } from '../runtime/errors';
4
+
5
+ describe('tools dispatch constraints', () => {
6
+ test('intent tool name resolves to doc.find operation', async () => {
7
+ const operationId = await resolveToolOperation('find_content');
8
+ expect(operationId).toBe('doc.find');
9
+ });
10
+
11
+ test('enforces requiresOneOf for doc.find (type | query)', async () => {
12
+ const client = {
13
+ doc: {
14
+ find: async () => ({ ok: true }),
15
+ },
16
+ };
17
+
18
+ try {
19
+ await dispatchSuperDocTool(client, 'find_content', {});
20
+ throw new Error('Expected dispatch to fail.');
21
+ } catch (error) {
22
+ expect(error).toBeInstanceOf(SuperDocCliError);
23
+ const cliError = error as SuperDocCliError;
24
+ expect(cliError.code).toBe('INVALID_ARGUMENT');
25
+ expect(cliError.message).toContain('One of the following arguments is required');
26
+ }
27
+ });
28
+
29
+ test('enforces mutuallyExclusive constraints for doc.find query + flat flags', async () => {
30
+ let called = false;
31
+ const client = {
32
+ doc: {
33
+ find: async () => {
34
+ called = true;
35
+ return { ok: true };
36
+ },
37
+ },
38
+ };
39
+
40
+ try {
41
+ await dispatchSuperDocTool(client, 'find_content', {
42
+ query: { select: { type: 'text', pattern: 'Wilde' } },
43
+ type: 'text',
44
+ });
45
+ throw new Error('Expected dispatch to fail.');
46
+ } catch (error) {
47
+ expect(error).toBeInstanceOf(SuperDocCliError);
48
+ const cliError = error as SuperDocCliError;
49
+ expect(cliError.code).toBe('INVALID_ARGUMENT');
50
+ expect(cliError.message).toContain('mutually exclusive');
51
+ expect(called).toBe(false);
52
+ }
53
+ });
54
+
55
+ test('enforces requiredWhen for doc.find text selectors', async () => {
56
+ const client = {
57
+ doc: {
58
+ find: async () => ({ ok: true }),
59
+ },
60
+ };
61
+
62
+ try {
63
+ await dispatchSuperDocTool(client, 'find_content', {
64
+ type: 'text',
65
+ });
66
+ throw new Error('Expected dispatch to fail.');
67
+ } catch (error) {
68
+ expect(error).toBeInstanceOf(SuperDocCliError);
69
+ const cliError = error as SuperDocCliError;
70
+ expect(cliError.code).toBe('INVALID_ARGUMENT');
71
+ expect(cliError.message).toContain('required by constraints');
72
+ }
73
+ });
74
+
75
+ test('dispatches when constraints are satisfied', async () => {
76
+ const client = {
77
+ doc: {
78
+ find: async (args: Record<string, unknown>) => ({
79
+ ok: true,
80
+ query: args,
81
+ }),
82
+ },
83
+ };
84
+
85
+ const result = await dispatchSuperDocTool(client, 'find_content', {
86
+ query: {
87
+ select: { type: 'text', pattern: 'Wilde' },
88
+ },
89
+ });
90
+
91
+ expect(result).toMatchObject({
92
+ ok: true,
93
+ });
94
+ });
95
+ });
96
+