@superdoc-dev/sdk 1.14.0 → 1.15.0-next.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.
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ var errors = require('./runtime/errors.cjs');
4
+ var legacy = require('./presets/legacy.cjs');
5
+
6
+ /**
7
+ * Preset registry for SuperDoc LLM tools.
8
+ *
9
+ * A preset is a self-contained collection of LLM tools — provider catalogs
10
+ * (openai / anthropic / vercel / generic), a system prompt, and a dispatcher.
11
+ * Multiple presets can coexist in the SDK; consumers select one at runtime via
12
+ * `chooseTools({ preset })`.
13
+ *
14
+ * const { tools, meta } = await chooseTools({ provider: 'vercel', preset: 'legacy' });
15
+ *
16
+ * v1 ships a single preset: `'legacy'` — a thin wrapper around today's
17
+ * codegen-emitted intent tools. When callers omit `preset`, `legacy` is used.
18
+ * The default may move once a replacement preset reaches parity; bumping it is
19
+ * a coordinated change in this file alone.
20
+ *
21
+ * Presets are NOT versioned. The preset id encodes the variant; a new shape
22
+ * ships as a new id, not a new version of an existing one.
23
+ *
24
+ * @internal
25
+ */
26
+ // ---------------------------------------------------------------------------
27
+ // Registry
28
+ // ---------------------------------------------------------------------------
29
+ /**
30
+ * The default preset returned when callers omit `preset`. Set to `'legacy'`
31
+ * so consumers built before presets existed (today's intent-tool path) keep
32
+ * working without changes.
33
+ */
34
+ const DEFAULT_PRESET = 'legacy';
35
+ const PRESETS = {
36
+ legacy: legacy.legacyPreset,
37
+ };
38
+ /** List the IDs of all registered presets. */
39
+ function listPresets() {
40
+ return Object.keys(PRESETS);
41
+ }
42
+ /**
43
+ * Resolve a preset by ID. Throws {@link SuperDocCliError} with code
44
+ * `PRESET_NOT_FOUND` if the ID is not registered. Omit the argument to
45
+ * get the default preset.
46
+ */
47
+ function getPreset(id = DEFAULT_PRESET) {
48
+ const preset = PRESETS[id];
49
+ if (preset == null) {
50
+ throw new errors.SuperDocCliError(`Unknown LLM-tools preset: "${id}"`, {
51
+ code: 'PRESET_NOT_FOUND',
52
+ details: { id, availablePresets: Object.keys(PRESETS) },
53
+ });
54
+ }
55
+ return preset;
56
+ }
57
+
58
+ exports.DEFAULT_PRESET = DEFAULT_PRESET;
59
+ exports.getPreset = getPreset;
60
+ exports.listPresets = listPresets;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Preset registry for SuperDoc LLM tools.
3
+ *
4
+ * A preset is a self-contained collection of LLM tools — provider catalogs
5
+ * (openai / anthropic / vercel / generic), a system prompt, and a dispatcher.
6
+ * Multiple presets can coexist in the SDK; consumers select one at runtime via
7
+ * `chooseTools({ preset })`.
8
+ *
9
+ * const { tools, meta } = await chooseTools({ provider: 'vercel', preset: 'legacy' });
10
+ *
11
+ * v1 ships a single preset: `'legacy'` — a thin wrapper around today's
12
+ * codegen-emitted intent tools. When callers omit `preset`, `legacy` is used.
13
+ * The default may move once a replacement preset reaches parity; bumping it is
14
+ * a coordinated change in this file alone.
15
+ *
16
+ * Presets are NOT versioned. The preset id encodes the variant; a new shape
17
+ * ships as a new id, not a new version of an existing one.
18
+ *
19
+ * @internal
20
+ */
21
+ import type { BoundDocApi } from './generated/client.js';
22
+ import type { InvokeOptions } from './runtime/process.js';
23
+ /**
24
+ * Wire format the tools are emitted in.
25
+ *
26
+ * - `openai` — OpenAI Chat Completions / Responses
27
+ * - `anthropic` — Anthropic Messages API
28
+ * - `vercel` — Vercel AI SDK (provider-agnostic adapter)
29
+ * - `generic` — vendor-neutral JSON Schema shape
30
+ */
31
+ export type ToolProvider = 'openai' | 'anthropic' | 'vercel' | 'generic';
32
+ /**
33
+ * Prompt-cache strategy returned by `chooseTools.meta.cacheStrategy`.
34
+ *
35
+ * - `explicit` — preset emitted provider-specific cache markers (Anthropic `cache_control`)
36
+ * - `automatic` — provider caches automatically (OpenAI ≥ 1024 prompt tokens)
37
+ * - `unsupported` — pass-through; caching depends on the underlying model (vercel/generic)
38
+ * - `disabled` — caller passed `cache: false` or omitted the flag
39
+ */
40
+ export type CacheStrategy = 'explicit' | 'automatic' | 'unsupported' | 'disabled';
41
+ /**
42
+ * One operation row in a {@link ToolCatalogEntry}. Each catalog entry can
43
+ * dispatch to one or more operations (e.g. multi-action intent tools), so
44
+ * the catalog records the operation id and the action discriminator that
45
+ * routes to it.
46
+ */
47
+ export type ToolCatalogOperation = {
48
+ operationId: string;
49
+ intentAction: string;
50
+ required?: string[];
51
+ requiredOneOf?: string[][];
52
+ };
53
+ /**
54
+ * One entry in the {@link ToolCatalog}. Matches the shape of the catalog
55
+ * emitted by the legacy preset's codegen — kept stable as the public
56
+ * catalog row shape so TypeScript consumers can introspect `tools[i]`
57
+ * without losing property typing.
58
+ */
59
+ export type ToolCatalogEntry = {
60
+ toolName: string;
61
+ description: string;
62
+ inputSchema: Record<string, unknown>;
63
+ mutates: boolean;
64
+ operations: ToolCatalogOperation[];
65
+ };
66
+ /**
67
+ * Full tool catalog shape. The legacy preset returns the existing codegen
68
+ * catalog with `contractVersion`, `generatedAt`, `toolCount`, `tools`.
69
+ */
70
+ export type ToolCatalog = {
71
+ contractVersion: string;
72
+ generatedAt: string | null;
73
+ toolCount: number;
74
+ tools: ToolCatalogEntry[];
75
+ };
76
+ export interface GetToolsOptions {
77
+ /**
78
+ * When `true`, the preset applies provider-specific prompt-cache markers
79
+ * (Anthropic `cache_control: { type: "ephemeral" }` on the last tool,
80
+ * for example). When omitted or `false`, no markers are added.
81
+ */
82
+ cache?: boolean;
83
+ }
84
+ export interface GetToolsResult {
85
+ tools: unknown[];
86
+ cacheStrategy: CacheStrategy;
87
+ }
88
+ /**
89
+ * Self-contained preset of LLM tools.
90
+ *
91
+ * Each preset owns:
92
+ * - its tool catalogs per provider format
93
+ * - its system prompt (and MCP-flavored variant)
94
+ * - its dispatcher (how a named tool call routes against a doc handle)
95
+ *
96
+ * Presets are stateless; the same descriptor handles every call.
97
+ *
98
+ * @internal
99
+ */
100
+ export interface PresetDescriptor {
101
+ /** Stable identifier — used as the preset's only "version" reference. */
102
+ readonly id: string;
103
+ /** Human-readable description shown by `listPresets()`. */
104
+ readonly description: string;
105
+ /**
106
+ * Whether this preset's provider adapters emit Anthropic prompt-cache
107
+ * markers when called with `cache: true`. Informational; per-provider
108
+ * behavior is reported via `GetToolsResult.cacheStrategy`.
109
+ */
110
+ readonly supportsCacheControl: boolean;
111
+ /** Tool definitions for the requested provider format. */
112
+ getTools(provider: ToolProvider, options?: GetToolsOptions): Promise<GetToolsResult>;
113
+ /** Full tool catalog with metadata (contract version, tool count, etc.). */
114
+ getCatalog(): Promise<ToolCatalog>;
115
+ /** System prompt for embedded LLM usage (OpenAI/Anthropic/Vercel APIs). */
116
+ getSystemPrompt(): Promise<string>;
117
+ /** System prompt for MCP server `instructions`. */
118
+ getMcpPrompt(): Promise<string>;
119
+ /**
120
+ * Dispatch a tool call against a bound document handle.
121
+ *
122
+ * The handle injects session targeting; `args` must NOT carry `doc` or
123
+ * `sessionId`. Returns whatever the underlying operation produces.
124
+ */
125
+ dispatch(documentHandle: BoundDocApi, toolName: string, args: Record<string, unknown>, invokeOptions?: InvokeOptions): Promise<unknown>;
126
+ }
127
+ /**
128
+ * The default preset returned when callers omit `preset`. Set to `'legacy'`
129
+ * so consumers built before presets existed (today's intent-tool path) keep
130
+ * working without changes.
131
+ */
132
+ export declare const DEFAULT_PRESET = "legacy";
133
+ /** List the IDs of all registered presets. */
134
+ export declare function listPresets(): readonly string[];
135
+ /**
136
+ * Resolve a preset by ID. Throws {@link SuperDocCliError} with code
137
+ * `PRESET_NOT_FOUND` if the ID is not registered. Omit the argument to
138
+ * get the default preset.
139
+ */
140
+ export declare function getPreset(id?: string): PresetDescriptor;
141
+ //# sourceMappingURL=presets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAI1D;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEzE;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,WAAW,GAAG,aAAa,GAAG,UAAU,CAAC;AAElF;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC;CAC5B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,2DAA2D;IAC3D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;IAEvC,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAErF,4EAA4E;IAC5E,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IAEnC,2EAA2E;IAC3E,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnC,mDAAmD;IACnD,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAEhC;;;;;OAKG;IACH,QAAQ,CACN,cAAc,EAAE,WAAW,EAC3B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,aAAa,CAAC,EAAE,aAAa,GAC5B,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAMD;;;;GAIG;AACH,eAAO,MAAM,cAAc,WAAW,CAAC;AAMvC,8CAA8C;AAC9C,wBAAgB,WAAW,IAAI,SAAS,MAAM,EAAE,CAE/C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,GAAE,MAAuB,GAAG,gBAAgB,CASvE"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Preset registry for SuperDoc LLM tools.
3
+ *
4
+ * A preset is a self-contained collection of LLM tools — provider catalogs
5
+ * (openai / anthropic / vercel / generic), a system prompt, and a dispatcher.
6
+ * Multiple presets can coexist in the SDK; consumers select one at runtime via
7
+ * `chooseTools({ preset })`.
8
+ *
9
+ * const { tools, meta } = await chooseTools({ provider: 'vercel', preset: 'legacy' });
10
+ *
11
+ * v1 ships a single preset: `'legacy'` — a thin wrapper around today's
12
+ * codegen-emitted intent tools. When callers omit `preset`, `legacy` is used.
13
+ * The default may move once a replacement preset reaches parity; bumping it is
14
+ * a coordinated change in this file alone.
15
+ *
16
+ * Presets are NOT versioned. The preset id encodes the variant; a new shape
17
+ * ships as a new id, not a new version of an existing one.
18
+ *
19
+ * @internal
20
+ */
21
+ import { SuperDocCliError } from './runtime/errors.js';
22
+ import { legacyPreset } from './presets/legacy.js';
23
+ // ---------------------------------------------------------------------------
24
+ // Registry
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * The default preset returned when callers omit `preset`. Set to `'legacy'`
28
+ * so consumers built before presets existed (today's intent-tool path) keep
29
+ * working without changes.
30
+ */
31
+ export const DEFAULT_PRESET = 'legacy';
32
+ const PRESETS = {
33
+ legacy: legacyPreset,
34
+ };
35
+ /** List the IDs of all registered presets. */
36
+ export function listPresets() {
37
+ return Object.keys(PRESETS);
38
+ }
39
+ /**
40
+ * Resolve a preset by ID. Throws {@link SuperDocCliError} with code
41
+ * `PRESET_NOT_FOUND` if the ID is not registered. Omit the argument to
42
+ * get the default preset.
43
+ */
44
+ export function getPreset(id = DEFAULT_PRESET) {
45
+ const preset = PRESETS[id];
46
+ if (preset == null) {
47
+ throw new SuperDocCliError(`Unknown LLM-tools preset: "${id}"`, {
48
+ code: 'PRESET_NOT_FOUND',
49
+ details: { id, availablePresets: Object.keys(PRESETS) },
50
+ });
51
+ }
52
+ return preset;
53
+ }
package/dist/tools.cjs CHANGED
@@ -1,334 +1,101 @@
1
1
  'use strict';
2
2
 
3
- var promises = require('node:fs/promises');
4
- var path = require('node:path');
5
- var node_url = require('node:url');
6
- var errors = require('./runtime/errors.cjs');
7
- var intentDispatch_generated = require('./generated/intent-dispatch.generated.cjs');
3
+ var presets = require('./presets.cjs');
8
4
 
9
- var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
10
- // Resolve tools directory relative to package root (works from both src/ and dist/)
11
- const toolsDir = path.resolve(path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('tools.cjs', document.baseURI).href)))), '..', 'tools');
12
- const providerFileByName = {
13
- openai: 'tools.openai.json',
14
- anthropic: 'tools.anthropic.json',
15
- vercel: 'tools.vercel.json',
16
- generic: 'tools.generic.json',
17
- };
18
- const STRIP_EMPTY_OPTIONAL_ARGS = new Set(['parentId', 'parentCommentId', 'id', 'status']);
19
- function isRecord(value) {
20
- return typeof value === 'object' && value != null && !Array.isArray(value);
21
- }
22
- function isObviouslyCorruptedToolArgKey(key) {
23
- const trimmed = key.trim();
24
- return trimmed.length === 0 || !/[\p{L}\p{N}]/u.test(trimmed);
25
- }
26
- function stripCorruptedToolArgKeys(value) {
27
- if (Array.isArray(value)) {
28
- return value.map((item) => stripCorruptedToolArgKeys(item));
29
- }
30
- if (!isRecord(value))
31
- return value;
32
- const clean = {};
33
- for (const [key, entryValue] of Object.entries(value)) {
34
- if (isObviouslyCorruptedToolArgKey(key))
35
- continue;
36
- clean[key] = stripCorruptedToolArgKeys(entryValue);
37
- }
38
- return clean;
39
- }
40
- async function readJson(fileName) {
41
- const filePath = path.join(toolsDir, fileName);
42
- let raw = '';
43
- try {
44
- raw = await promises.readFile(filePath, 'utf8');
45
- }
46
- catch (error) {
47
- throw new errors.SuperDocCliError('Unable to load packaged tool artifact.', {
48
- code: 'TOOLS_ASSET_NOT_FOUND',
49
- details: {
50
- filePath,
51
- message: error instanceof Error ? error.message : String(error),
52
- },
53
- });
54
- }
55
- try {
56
- return JSON.parse(raw);
57
- }
58
- catch (error) {
59
- throw new errors.SuperDocCliError('Packaged tool artifact is invalid JSON.', {
60
- code: 'TOOLS_ASSET_INVALID',
61
- details: {
62
- filePath,
63
- message: error instanceof Error ? error.message : String(error),
64
- },
65
- });
66
- }
67
- }
68
- async function loadProviderBundle(provider) {
69
- return readJson(providerFileByName[provider]);
70
- }
71
- async function loadCatalog() {
72
- return readJson('catalog.json');
73
- }
74
- async function getToolCatalog() {
75
- return getCachedCatalog();
76
- }
77
- async function listTools(provider) {
78
- const bundle = await loadProviderBundle(provider);
79
- const tools = bundle.tools;
80
- if (!Array.isArray(tools)) {
81
- throw new errors.SuperDocCliError('Tool provider bundle is missing tools array.', {
82
- code: 'TOOLS_ASSET_INVALID',
83
- details: { provider },
84
- });
85
- }
86
- return tools;
87
- }
88
5
  /**
89
- * Select all intent tools for a specific provider.
6
+ * Public LLM-tools API. Thin layer over the preset registry — every call here
7
+ * resolves a preset (defaulting to `legacy` for backwards compat) and delegates
8
+ * to it.
90
9
  *
91
- * Returns all intent tools in the requested provider format. Pass
92
- * `cache: true` to apply provider-specific caching markers (see
93
- * {@link ToolChooserInput.cache}).
10
+ * Presets are the unit of swapping. To add a new tool surface (e.g. handwritten
11
+ * "core" tools, prompt-caching variant, lazy-load experiment), register a new
12
+ * descriptor in `presets.ts` — no changes here required.
13
+ */
14
+ /**
15
+ * Select tools for a specific provider from a preset.
94
16
  *
95
17
  * @example
96
18
  * ```ts
19
+ * // Default — legacy preset, no cache markers.
20
+ * const { tools, meta } = await chooseTools({ provider: 'vercel' });
21
+ *
97
22
  * // Anthropic — last tool gets cache_control automatically.
98
23
  * const { tools, meta } = await chooseTools({ provider: 'anthropic', cache: true });
99
24
  *
100
- * // OpenAI caching is automatic when prompts exceed 1024 tokens.
101
- * const { tools } = await chooseTools({ provider: 'openai', cache: true });
25
+ * // Pick a specific preset by ID.
26
+ * const { tools, meta } = await chooseTools({ provider: 'openai', preset: 'legacy' });
102
27
  * ```
103
28
  */
104
29
  async function chooseTools(input) {
105
- const bundle = await loadProviderBundle(input.provider);
106
- const rawTools = Array.isArray(bundle.tools) ? bundle.tools : [];
107
- const cacheRequested = input.cache === true;
108
- const { tools, cacheStrategy } = applyCacheMarkers(rawTools, input.provider, cacheRequested);
30
+ const presetId = input.preset ?? presets.DEFAULT_PRESET;
31
+ const preset = presets.getPreset(presetId);
32
+ const { tools, cacheStrategy } = await preset.getTools(input.provider, {
33
+ cache: input.cache === true,
34
+ });
109
35
  return {
110
36
  tools,
111
37
  meta: {
112
38
  provider: input.provider,
39
+ preset: presetId,
113
40
  toolCount: tools.length,
114
41
  cacheStrategy,
115
42
  },
116
43
  };
117
44
  }
118
- /**
119
- * Apply provider-specific caching markers to the tools array. Mutates a clone,
120
- * never the input. Anthropic gets an explicit `cache_control` on the last
121
- * tool; other providers pass through.
122
- */
123
- function applyCacheMarkers(tools, provider, cacheRequested) {
124
- if (!cacheRequested) {
125
- return { tools, cacheStrategy: 'disabled' };
126
- }
127
- if (provider === 'anthropic') {
128
- if (tools.length === 0)
129
- return { tools, cacheStrategy: 'explicit' };
130
- // Anthropic: marking the LAST tool with cache_control caches the entire
131
- // tools block (and everything before it in the request — system prompt
132
- // first if it also has cache_control). Shallow-spread the last entry so we
133
- // don't mutate the cached bundle in place.
134
- const next = tools.slice(0, -1);
135
- const last = {
136
- ...tools[tools.length - 1],
137
- cache_control: { type: 'ephemeral' },
138
- };
139
- next.push(last);
140
- return { tools: next, cacheStrategy: 'explicit' };
141
- }
142
- if (provider === 'openai') {
143
- // OpenAI caches prompts ≥ 1024 tokens automatically. No marker needed,
144
- // but we still report cacheStrategy:'automatic' so callers can branch on
145
- // it (e.g. for measurement).
146
- return { tools, cacheStrategy: 'automatic' };
147
- }
148
- // vercel / generic — depends on underlying model.
149
- return { tools, cacheStrategy: 'unsupported' };
150
- }
151
- function resolveDocApiMethod(documentHandle, operationId) {
152
- const tokens = operationId.split('.').slice(1);
153
- let cursor = documentHandle;
154
- for (const token of tokens) {
155
- if (!isRecord(cursor) || !(token in cursor)) {
156
- throw new errors.SuperDocCliError(`No SDK doc method found for operation ${operationId}.`, {
157
- code: 'TOOL_DISPATCH_NOT_FOUND',
158
- details: { operationId, token },
159
- });
160
- }
161
- cursor = cursor[token];
162
- }
163
- if (typeof cursor !== 'function') {
164
- throw new errors.SuperDocCliError(`Resolved member for ${operationId} is not callable.`, {
165
- code: 'TOOL_DISPATCH_NOT_FOUND',
166
- details: { operationId },
167
- });
168
- }
169
- return cursor;
170
- }
171
- // Cached catalog instance — loaded once per process.
172
- let _catalogCache = null;
173
- async function getCachedCatalog() {
174
- if (_catalogCache == null) {
175
- _catalogCache = await loadCatalog();
176
- }
177
- return _catalogCache;
45
+ // ---------------------------------------------------------------------------
46
+ // Catalog + listings (preset-scoped; default to legacy)
47
+ // ---------------------------------------------------------------------------
48
+ /** Return the full tool catalog for a preset (default: legacy). */
49
+ async function getToolCatalog(preset) {
50
+ return presets.getPreset(preset ?? presets.DEFAULT_PRESET).getCatalog();
178
51
  }
179
52
  /**
180
- * Validate tool arguments against the catalog schema.
53
+ * Return the raw tool array for a provider from a preset (default: legacy).
181
54
  *
182
- * Checks three things in order:
183
- * 1. No unknown keys (additionalProperties: false in merged schema)
184
- * 2. All universally-required keys present (merged schema `required`)
185
- * 3. All action-specific required keys present (per-operation `required`)
55
+ * No cache markers are applied. Use {@link chooseTools} when you need cache
56
+ * markers and metadata.
186
57
  */
187
- function validateToolArgs(toolName, args, tool) {
188
- const schema = tool.inputSchema;
189
- const properties = isRecord(schema.properties) ? schema.properties : {};
190
- const required = Array.isArray(schema.required) ? schema.required : [];
191
- // 1. Reject unknown keys
192
- const knownKeys = new Set(Object.keys(properties));
193
- const unknownKeys = Object.keys(args).filter((k) => !knownKeys.has(k));
194
- if (unknownKeys.length > 0) {
195
- throw new errors.SuperDocCliError(`Unknown argument(s) for ${toolName}: ${unknownKeys.join(', ')}`, {
196
- code: 'INVALID_ARGUMENT',
197
- details: { toolName, unknownKeys, knownKeys: [...knownKeys] },
198
- });
199
- }
200
- // 2. Reject missing universally-required keys
201
- const missingKeys = required.filter((k) => args[k] == null);
202
- if (missingKeys.length > 0) {
203
- throw new errors.SuperDocCliError(`Missing required argument(s) for ${toolName}: ${missingKeys.join(', ')}`, {
204
- code: 'INVALID_ARGUMENT',
205
- details: { toolName, missingKeys },
206
- });
207
- }
208
- // 3. Reject missing per-operation required keys.
209
- // For multi-action tools, resolve the operation by action; for single-op
210
- // tools, use the sole operation entry.
211
- const action = args.action;
212
- let op;
213
- if (typeof action === 'string' && tool.operations.length > 1) {
214
- op = tool.operations.find((o) => o.intentAction === action);
215
- }
216
- else if (tool.operations.length === 1) {
217
- op = tool.operations[0];
218
- }
219
- if (op) {
220
- validateOperationRequired(toolName, action, args, op);
221
- }
58
+ async function listTools(provider, preset) {
59
+ const { tools } = await presets.getPreset(preset ?? presets.DEFAULT_PRESET).getTools(provider, { cache: false });
60
+ return tools;
222
61
  }
62
+ // ---------------------------------------------------------------------------
63
+ // Dispatch
64
+ // ---------------------------------------------------------------------------
223
65
  /**
224
- * Check per-operation required constraints.
66
+ * Dispatch a tool call against a bound document handle using the default
67
+ * preset (`legacy`).
225
68
  *
226
- * Handles two shapes emitted by the codegen:
227
- * - `required: string[]` — all listed keys must be present
228
- * - `requiredOneOf: string[][]` — at least one branch must be fully satisfied
229
- * (mirrors JSON Schema `oneOf` with per-branch `required` arrays)
230
- */
231
- function validateOperationRequired(toolName, action, args, op) {
232
- const actionLabel = typeof action === 'string' ? ` action "${action}"` : '';
233
- if (op.requiredOneOf && op.requiredOneOf.length > 0) {
234
- const satisfied = op.requiredOneOf.some((branch) => branch.every((k) => args[k] != null));
235
- if (!satisfied) {
236
- const options = op.requiredOneOf.map((b) => b.join(' + ')).join(' | ');
237
- throw new errors.SuperDocCliError(`Missing required argument(s) for ${toolName}${actionLabel}: must provide one of: ${options}`, {
238
- code: 'INVALID_ARGUMENT',
239
- details: { toolName, action, requiredOneOf: op.requiredOneOf },
240
- });
241
- }
242
- }
243
- else if (op.required && op.required.length > 0) {
244
- const missingActionKeys = op.required.filter((k) => args[k] == null);
245
- if (missingActionKeys.length > 0) {
246
- throw new errors.SuperDocCliError(`Missing required argument(s) for ${toolName}${actionLabel}: ${missingActionKeys.join(', ')}`, {
247
- code: 'INVALID_ARGUMENT',
248
- details: { toolName, action, missingKeys: missingActionKeys },
249
- });
250
- }
251
- }
252
- }
253
- /**
254
- * Dispatch a tool call against a bound document handle.
69
+ * The document handle injects session targeting automatically; tool arguments
70
+ * should not contain `doc` or `sessionId`.
255
71
  *
256
- * The document handle injects session targeting automatically.
257
- * Tool arguments should not contain `doc` or `sessionId`.
72
+ * For preset-aware dispatch e.g. when comparing two presets — call
73
+ * `getPreset('id').dispatch(...)` directly.
258
74
  */
259
75
  async function dispatchSuperDocTool(documentHandle, toolName, args = {}, invokeOptions) {
260
- if (!isRecord(args)) {
261
- throw new errors.SuperDocCliError(`Tool arguments for ${toolName} must be an object.`, {
262
- code: 'INVALID_ARGUMENT',
263
- details: { toolName },
264
- });
265
- }
266
- const sanitizedArgs = stripCorruptedToolArgKeys(args);
267
- if (!isRecord(sanitizedArgs)) {
268
- throw new errors.SuperDocCliError(`Tool arguments for ${toolName} must be an object.`, {
269
- code: 'INVALID_ARGUMENT',
270
- details: { toolName },
271
- });
272
- }
273
- // Validate against the tool schema before dispatch.
274
- const catalog = await getCachedCatalog();
275
- const tool = catalog.tools.find((t) => t.toolName === toolName);
276
- if (tool == null) {
277
- throw new errors.SuperDocCliError(`Unknown tool: ${toolName}`, {
278
- code: 'TOOL_DISPATCH_NOT_FOUND',
279
- details: { toolName },
280
- });
281
- }
282
- validateToolArgs(toolName, sanitizedArgs, tool);
283
- // Strip empty strings for known optional ID/enum params that LLMs fill with ""
284
- // instead of omitting. Only target params where "" is never a valid value.
285
- const cleanArgs = {};
286
- for (const [key, value] of Object.entries(sanitizedArgs)) {
287
- if (value === '' && STRIP_EMPTY_OPTIONAL_ARGS.has(key))
288
- continue;
289
- cleanArgs[key] = value;
290
- }
291
- return intentDispatch_generated.dispatchIntentTool(toolName, cleanArgs, (operationId, input) => {
292
- const method = resolveDocApiMethod(documentHandle, operationId);
293
- return method(input, invokeOptions);
294
- });
76
+ return presets.getPreset(presets.DEFAULT_PRESET).dispatch(documentHandle, toolName, args, invokeOptions);
295
77
  }
78
+ // ---------------------------------------------------------------------------
79
+ // System prompts (preset-scoped; default to legacy)
80
+ // ---------------------------------------------------------------------------
296
81
  /**
297
- * Read the bundled SDK system prompt for intent tools.
82
+ * Read the packaged SDK system prompt (default preset: legacy).
298
83
  *
299
- * This prompt includes a persona preamble ("You are a document editing assistant…")
300
- * suitable for embedded LLM usage (OpenAI, Anthropic, Vercel APIs).
301
- * For MCP server instructions, use {@link getMcpPrompt} instead.
84
+ * Includes a persona preamble ("You are a document editing assistant…")
85
+ * suitable for embedded LLM usage (OpenAI, Anthropic, Vercel APIs). For MCP
86
+ * server instructions, use {@link getMcpPrompt} instead.
302
87
  */
303
- async function getSystemPrompt() {
304
- const promptPath = path.join(toolsDir, 'system-prompt.md');
305
- try {
306
- return await promises.readFile(promptPath, 'utf8');
307
- }
308
- catch {
309
- throw new errors.SuperDocCliError('System prompt not found.', {
310
- code: 'TOOLS_ASSET_NOT_FOUND',
311
- details: { filePath: promptPath },
312
- });
313
- }
88
+ async function getSystemPrompt(preset) {
89
+ return presets.getPreset(preset ?? presets.DEFAULT_PRESET).getSystemPrompt();
314
90
  }
315
91
  /**
316
- * Read the bundled MCP system prompt for intent tools.
92
+ * Read the packaged MCP system prompt for intent tools (default preset: legacy).
317
93
  *
318
- * This prompt omits the persona preamble and includes session lifecycle
319
- * instructions (open/save/close) suitable for MCP server `instructions`.
94
+ * Omits the persona preamble and includes session lifecycle instructions
95
+ * (open/save/close) suitable for MCP server `instructions`.
320
96
  */
321
- async function getMcpPrompt() {
322
- const promptPath = path.join(toolsDir, 'system-prompt-mcp.md');
323
- try {
324
- return await promises.readFile(promptPath, 'utf8');
325
- }
326
- catch {
327
- throw new errors.SuperDocCliError('MCP system prompt not found.', {
328
- code: 'TOOLS_ASSET_NOT_FOUND',
329
- details: { filePath: promptPath },
330
- });
331
- }
97
+ async function getMcpPrompt(preset) {
98
+ return presets.getPreset(preset ?? presets.DEFAULT_PRESET).getMcpPrompt();
332
99
  }
333
100
  /**
334
101
  * Get the system prompt formatted for a specific LLM provider, with optional
@@ -355,7 +122,7 @@ async function getMcpPrompt() {
355
122
  * ```
356
123
  */
357
124
  async function getSystemPromptForProvider(input) {
358
- const text = await getSystemPrompt();
125
+ const text = await getSystemPrompt(input.preset);
359
126
  const cacheRequested = input.cache === true;
360
127
  if (input.provider === 'anthropic') {
361
128
  const block = { type: 'text', text };
@@ -375,6 +142,9 @@ async function getSystemPromptForProvider(input) {
375
142
  return { provider: input.provider, content: text, cacheStrategy };
376
143
  }
377
144
 
145
+ exports.DEFAULT_PRESET = presets.DEFAULT_PRESET;
146
+ exports.getPreset = presets.getPreset;
147
+ exports.listPresets = presets.listPresets;
378
148
  exports.chooseTools = chooseTools;
379
149
  exports.dispatchSuperDocTool = dispatchSuperDocTool;
380
150
  exports.getMcpPrompt = getMcpPrompt;