@superdoc-dev/sdk 1.0.0-alpha.3 → 1.0.0-alpha.4

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.
Files changed (53) hide show
  1. package/dist/generated/client.d.ts +1790 -0
  2. package/dist/generated/client.d.ts.map +1 -0
  3. package/dist/generated/client.js +66 -0
  4. package/dist/generated/contract.d.ts +13676 -0
  5. package/dist/generated/contract.d.ts.map +1 -0
  6. package/dist/generated/contract.js +17809 -0
  7. package/dist/index.d.ts +23 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +29 -0
  10. package/dist/runtime/embedded-cli.d.ts +5 -0
  11. package/dist/runtime/embedded-cli.d.ts.map +1 -0
  12. package/dist/runtime/embedded-cli.js +94 -0
  13. package/dist/runtime/errors.d.ts +17 -0
  14. package/dist/runtime/errors.d.ts.map +1 -0
  15. package/dist/runtime/errors.js +18 -0
  16. package/dist/runtime/host.d.ts +36 -0
  17. package/dist/runtime/host.d.ts.map +1 -0
  18. package/dist/runtime/host.js +345 -0
  19. package/dist/runtime/process.d.ts +16 -0
  20. package/dist/runtime/process.d.ts.map +1 -0
  21. package/dist/runtime/process.js +27 -0
  22. package/dist/runtime/transport-common.d.ts +44 -0
  23. package/dist/runtime/transport-common.d.ts.map +1 -0
  24. package/dist/runtime/transport-common.js +65 -0
  25. package/dist/skills.d.ts +25 -0
  26. package/dist/skills.d.ts.map +1 -0
  27. package/dist/skills.js +140 -0
  28. package/dist/tools.d.ts +113 -0
  29. package/dist/tools.d.ts.map +1 -0
  30. package/dist/tools.js +360 -0
  31. package/package.json +19 -10
  32. package/tools/catalog.json +17128 -0
  33. package/tools/tool-name-map.json +96 -0
  34. package/tools/tools-policy.json +100 -0
  35. package/tools/tools.anthropic.json +3275 -0
  36. package/tools/tools.generic.json +16573 -0
  37. package/tools/tools.openai.json +3557 -0
  38. package/tools/tools.vercel.json +3557 -0
  39. package/skills/editing-docx.md +0 -157
  40. package/src/__tests__/skills.test.ts +0 -166
  41. package/src/__tests__/tools.test.ts +0 -96
  42. package/src/generated/client.ts +0 -3643
  43. package/src/generated/contract.ts +0 -15952
  44. package/src/index.ts +0 -87
  45. package/src/runtime/__tests__/process.test.ts +0 -38
  46. package/src/runtime/__tests__/transport-common.test.ts +0 -174
  47. package/src/runtime/embedded-cli.ts +0 -109
  48. package/src/runtime/errors.ts +0 -30
  49. package/src/runtime/host.ts +0 -481
  50. package/src/runtime/process.ts +0 -45
  51. package/src/runtime/transport-common.ts +0 -169
  52. package/src/skills.ts +0 -195
  53. package/src/tools.ts +0 -701
package/src/tools.ts DELETED
@@ -1,701 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { CONTRACT } from './generated/contract';
5
- import type { InvokeOptions } from './runtime/process';
6
- import { SuperDocCliError } from './runtime/errors';
7
-
8
- export type ToolProvider = 'openai' | 'anthropic' | 'vercel' | 'generic';
9
- export type ToolProfile = 'intent' | 'operation';
10
- export type ToolPhase = 'read' | 'locate' | 'mutate' | 'review';
11
-
12
- export type DocumentFeatures = {
13
- hasTables: boolean;
14
- hasLists: boolean;
15
- hasComments: boolean;
16
- hasTrackedChanges: boolean;
17
- isEmptyDocument: boolean;
18
- };
19
-
20
- export type ToolChooserInput = {
21
- provider: ToolProvider;
22
- profile?: ToolProfile;
23
- documentFeatures?: Partial<DocumentFeatures>;
24
- taskContext?: {
25
- phase?: ToolPhase;
26
- previousToolCalls?: Array<{ toolName: string; ok: boolean }>;
27
- };
28
- budget?: {
29
- maxTools?: number;
30
- minReadTools?: number;
31
- };
32
- policy?: {
33
- includeCategories?: string[];
34
- excludeCategories?: string[];
35
- allowMutatingTools?: boolean;
36
- forceInclude?: string[];
37
- forceExclude?: string[];
38
- };
39
- };
40
-
41
- export type ToolMetadata = {
42
- operationId: string;
43
- profile: ToolProfile;
44
- mutates: boolean;
45
- category: string;
46
- capabilities: string[];
47
- constraints?: {
48
- mutuallyExclusive?: string[][];
49
- requiresOneOf?: string[][];
50
- requiredWhen?: Array<{
51
- param: string;
52
- whenParam: string;
53
- equals?: unknown;
54
- present?: boolean;
55
- }>;
56
- };
57
- requiredCapabilities: Array<keyof DocumentFeatures>;
58
- profileTags: string[];
59
- examples: Array<{ description: string; args: Record<string, unknown> }>;
60
- commandTokens: string[];
61
- };
62
-
63
- export type GenericToolDefinition = {
64
- name: string;
65
- description: string;
66
- parameters: Record<string, unknown>;
67
- returns: Record<string, unknown>;
68
- metadata: ToolMetadata;
69
- };
70
-
71
- export type ToolCatalog = {
72
- contractVersion: string;
73
- generatedAt: string;
74
- namePolicyVersion: string;
75
- exposureVersion: string;
76
- toolCount: number;
77
- profiles: {
78
- intent: {
79
- name: 'intent';
80
- tools: Array<{
81
- operationId: string;
82
- toolName: string;
83
- profile: ToolProfile;
84
- source: 'operation' | 'intent';
85
- description: string;
86
- inputSchema: Record<string, unknown>;
87
- outputSchema: Record<string, unknown>;
88
- mutates: boolean;
89
- category: string;
90
- capabilities: string[];
91
- constraints?: ToolMetadata['constraints'];
92
- errors: string[];
93
- examples: Array<{ description: string; args: Record<string, unknown> }>;
94
- commandTokens: string[];
95
- profileTags: string[];
96
- requiredCapabilities: Array<keyof DocumentFeatures>;
97
- sessionRequirements: {
98
- requiresOpenContext: boolean;
99
- supportsSessionTargeting: boolean;
100
- };
101
- intentId?: string;
102
- }>;
103
- };
104
- operation: {
105
- name: 'operation';
106
- tools: Array<{
107
- operationId: string;
108
- toolName: string;
109
- profile: ToolProfile;
110
- source: 'operation' | 'intent';
111
- description: string;
112
- inputSchema: Record<string, unknown>;
113
- outputSchema: Record<string, unknown>;
114
- mutates: boolean;
115
- category: string;
116
- capabilities: string[];
117
- constraints?: ToolMetadata['constraints'];
118
- errors: string[];
119
- examples: Array<{ description: string; args: Record<string, unknown> }>;
120
- commandTokens: string[];
121
- profileTags: string[];
122
- requiredCapabilities: Array<keyof DocumentFeatures>;
123
- sessionRequirements: {
124
- requiresOpenContext: boolean;
125
- supportsSessionTargeting: boolean;
126
- };
127
- intentId?: string;
128
- }>;
129
- };
130
- };
131
- };
132
-
133
- const toolsDir = path.resolve(fileURLToPath(new URL('../tools', import.meta.url)));
134
- const providerFileByName: Record<ToolProvider, string> = {
135
- openai: 'tools.openai.json',
136
- anthropic: 'tools.anthropic.json',
137
- vercel: 'tools.vercel.json',
138
- generic: 'tools.generic.json',
139
- };
140
-
141
- const DEFAULT_MAX_TOOLS_BY_PROFILE: Record<ToolProfile, number> = {
142
- intent: 12,
143
- operation: 16,
144
- };
145
- const DEFAULT_MIN_READ_TOOLS = 2;
146
- const FOUNDATIONAL_READ_OPERATION_IDS = new Set(['doc.info', 'doc.find']);
147
- const CHOOSER_DECISION_VERSION = 'v1';
148
-
149
- const PHASE_POLICY: Record<ToolPhase, { include: string[]; exclude: string[]; priority: string[] }> = {
150
- read: {
151
- include: ['introspection', 'query'],
152
- exclude: ['mutation', 'trackChanges', 'session', 'create', 'comments', 'format'],
153
- priority: ['query', 'introspection'],
154
- },
155
- locate: {
156
- include: ['query'],
157
- exclude: ['mutation', 'trackChanges', 'session', 'create', 'comments', 'format'],
158
- priority: ['query'],
159
- },
160
- mutate: {
161
- include: ['query', 'mutation', 'format', 'comments', 'create'],
162
- exclude: ['session'],
163
- priority: ['query', 'mutation', 'create', 'format', 'comments'],
164
- },
165
- review: {
166
- include: ['query', 'trackChanges', 'comments'],
167
- exclude: ['mutation', 'create', 'session', 'format'],
168
- priority: ['trackChanges', 'comments', 'query'],
169
- },
170
- };
171
-
172
- function isRecord(value: unknown): value is Record<string, unknown> {
173
- return typeof value === 'object' && value != null && !Array.isArray(value);
174
- }
175
-
176
- function isPresent(value: unknown): boolean {
177
- if (value == null) return false;
178
- if (Array.isArray(value)) return value.length > 0;
179
- return true;
180
- }
181
-
182
- function invalidArgument(message: string, details?: Record<string, unknown>): never {
183
- throw new SuperDocCliError(message, {
184
- code: 'INVALID_ARGUMENT',
185
- details,
186
- });
187
- }
188
-
189
- async function readJson<T>(fileName: string): Promise<T> {
190
- const filePath = path.join(toolsDir, fileName);
191
- let raw = '';
192
- try {
193
- raw = await readFile(filePath, 'utf8');
194
- } catch (error) {
195
- throw new SuperDocCliError('Unable to load packaged tool artifact.', {
196
- code: 'TOOLS_ASSET_NOT_FOUND',
197
- details: {
198
- filePath,
199
- message: error instanceof Error ? error.message : String(error),
200
- },
201
- });
202
- }
203
-
204
- try {
205
- return JSON.parse(raw) as T;
206
- } catch (error) {
207
- throw new SuperDocCliError('Packaged tool artifact is invalid JSON.', {
208
- code: 'TOOLS_ASSET_INVALID',
209
- details: {
210
- filePath,
211
- message: error instanceof Error ? error.message : String(error),
212
- },
213
- });
214
- }
215
- }
216
-
217
- async function loadProviderBundle(provider: ToolProvider): Promise<{
218
- contractVersion: string;
219
- profiles: Record<ToolProfile, unknown[]>;
220
- }> {
221
- return readJson<{ contractVersion: string; profiles: Record<ToolProfile, unknown[]> }>(providerFileByName[provider]);
222
- }
223
-
224
- async function loadToolNameMap(): Promise<Record<string, string>> {
225
- return readJson<Record<string, string>>('tool-name-map.json');
226
- }
227
-
228
- async function loadCatalog(): Promise<ToolCatalog> {
229
- return readJson<ToolCatalog>('catalog.json');
230
- }
231
-
232
- function normalizeFeatures(features?: Partial<DocumentFeatures>): DocumentFeatures {
233
- return {
234
- hasTables: Boolean(features?.hasTables),
235
- hasLists: Boolean(features?.hasLists),
236
- hasComments: Boolean(features?.hasComments),
237
- hasTrackedChanges: Boolean(features?.hasTrackedChanges),
238
- isEmptyDocument: Boolean(features?.isEmptyDocument),
239
- };
240
- }
241
-
242
- function stableSortByPhasePriority(
243
- entries: ToolCatalog['profiles']['intent']['tools'],
244
- priorityOrder: string[],
245
- ): ToolCatalog['profiles']['intent']['tools'] {
246
- const priority = new Map(priorityOrder.map((category, index) => [category, index]));
247
- return [...entries].sort((a, b) => {
248
- const aPriority = priority.get(a.category) ?? Number.MAX_SAFE_INTEGER;
249
- const bPriority = priority.get(b.category) ?? Number.MAX_SAFE_INTEGER;
250
- if (aPriority !== bPriority) return aPriority - bPriority;
251
- return a.toolName.localeCompare(b.toolName);
252
- });
253
- }
254
-
255
- function toOperationIndex(): Record<string, (typeof CONTRACT.operations)[number]> {
256
- return Object.fromEntries(CONTRACT.operations.map((operation) => [operation.id, operation]));
257
- }
258
-
259
- const OPERATION_INDEX = toOperationIndex();
260
-
261
- function validateValueAgainstSchema(value: unknown, schema: Record<string, unknown>, path: string): void {
262
- if ('const' in schema) {
263
- if (value !== schema.const) {
264
- invalidArgument(`${path} must equal ${JSON.stringify(schema.const)}.`);
265
- }
266
- return;
267
- }
268
-
269
- if (Array.isArray(schema.oneOf)) {
270
- const errors: string[] = [];
271
- for (const variant of schema.oneOf) {
272
- if (!isRecord(variant)) continue;
273
- try {
274
- validateValueAgainstSchema(value, variant, path);
275
- return;
276
- } catch (error) {
277
- errors.push(error instanceof Error ? error.message : String(error));
278
- }
279
- }
280
-
281
- invalidArgument(`${path} must match one of the allowed schema variants.`, { errors });
282
- }
283
-
284
- const type = schema.type;
285
- if (type === 'json') return;
286
-
287
- if (type === 'string') {
288
- if (typeof value !== 'string') invalidArgument(`${path} must be a string.`);
289
- return;
290
- }
291
-
292
- if (type === 'number') {
293
- if (typeof value !== 'number' || !Number.isFinite(value)) invalidArgument(`${path} must be a finite number.`);
294
- return;
295
- }
296
-
297
- if (type === 'boolean') {
298
- if (typeof value !== 'boolean') invalidArgument(`${path} must be a boolean.`);
299
- return;
300
- }
301
-
302
- if (type === 'array') {
303
- if (!Array.isArray(value)) invalidArgument(`${path} must be an array.`);
304
- if (isRecord(schema.items)) {
305
- for (let index = 0; index < value.length; index += 1) {
306
- validateValueAgainstSchema(value[index], schema.items, `${path}[${index}]`);
307
- }
308
- }
309
- return;
310
- }
311
-
312
- if (type === 'object') {
313
- if (!isRecord(value)) invalidArgument(`${path} must be an object.`);
314
-
315
- const properties = isRecord(schema.properties) ? schema.properties : {};
316
- const required = Array.isArray(schema.required)
317
- ? schema.required.filter((entry): entry is string => typeof entry === 'string')
318
- : [];
319
-
320
- for (const key of required) {
321
- if (!Object.prototype.hasOwnProperty.call(value, key)) {
322
- invalidArgument(`${path}.${key} is required.`);
323
- }
324
- }
325
-
326
- const knownKeys = new Set(Object.keys(properties));
327
- for (const key of Object.keys(value)) {
328
- if (!knownKeys.has(key)) {
329
- invalidArgument(`${path}.${key} is not allowed by schema.`);
330
- }
331
- }
332
-
333
- for (const [key, propSchema] of Object.entries(properties)) {
334
- if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
335
- if (!isRecord(propSchema)) continue;
336
- validateValueAgainstSchema(value[key], propSchema, `${path}.${key}`);
337
- }
338
- return;
339
- }
340
- }
341
-
342
- function validateDispatchArgs(operationId: string, args: Record<string, unknown>): void {
343
- const operation = OPERATION_INDEX[operationId];
344
- if (!operation) {
345
- invalidArgument(`Unknown operation id ${operationId}.`);
346
- }
347
-
348
- const allowedParams = new Set<string>(operation.params.map((param) => String(param.name)));
349
- for (const key of Object.keys(args)) {
350
- if (!allowedParams.has(key)) {
351
- invalidArgument(`Unexpected parameter ${key} for ${operationId}.`);
352
- }
353
- }
354
-
355
- for (const param of operation.params) {
356
- const value = args[param.name];
357
- if (value == null) {
358
- if ('required' in param && Boolean(param.required)) {
359
- invalidArgument(`Missing required parameter ${param.name} for ${operationId}.`);
360
- }
361
- continue;
362
- }
363
-
364
- if ('schema' in param && isRecord(param.schema)) {
365
- validateValueAgainstSchema(value, param.schema, `${operationId}.${param.name}`);
366
- continue;
367
- }
368
-
369
- if (param.type === 'string' && typeof value !== 'string') {
370
- invalidArgument(`${operationId}.${param.name} must be a string.`);
371
- }
372
- if (param.type === 'number' && (typeof value !== 'number' || !Number.isFinite(value))) {
373
- invalidArgument(`${operationId}.${param.name} must be a finite number.`);
374
- }
375
- if (param.type === 'boolean' && typeof value !== 'boolean') {
376
- invalidArgument(`${operationId}.${param.name} must be a boolean.`);
377
- }
378
- if (param.type === 'string[]') {
379
- if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
380
- invalidArgument(`${operationId}.${param.name} must be an array of strings.`);
381
- }
382
- }
383
- }
384
-
385
- const constraints = 'constraints' in operation ? operation.constraints : undefined;
386
- if (!constraints) return;
387
-
388
- const mutuallyExclusive = 'mutuallyExclusive' in constraints ? (constraints.mutuallyExclusive ?? []) : [];
389
- const requiresOneOf = 'requiresOneOf' in constraints ? (constraints.requiresOneOf ?? []) : [];
390
- const requiredWhen = 'requiredWhen' in constraints ? (constraints.requiredWhen ?? []) : [];
391
-
392
- for (const group of mutuallyExclusive) {
393
- const present = group.filter((name: string) => isPresent(args[name]));
394
- if (present.length > 1) {
395
- invalidArgument(`Arguments are mutually exclusive for ${operationId}: ${group.join(', ')}`, {
396
- operationId,
397
- group,
398
- });
399
- }
400
- }
401
-
402
- for (const group of requiresOneOf) {
403
- const hasAny = group.some((name: string) => isPresent(args[name]));
404
- if (!hasAny) {
405
- invalidArgument(`One of the following arguments is required for ${operationId}: ${group.join(', ')}`, {
406
- operationId,
407
- group,
408
- });
409
- }
410
- }
411
-
412
- for (const rule of requiredWhen) {
413
- const whenValue = args[rule.whenParam];
414
- let shouldRequire = false;
415
- if (Object.prototype.hasOwnProperty.call(rule, 'equals')) {
416
- shouldRequire = whenValue === (rule as { equals?: unknown }).equals;
417
- } else if (Object.prototype.hasOwnProperty.call(rule, 'present')) {
418
- const present = (rule as { present?: boolean }).present === true;
419
- shouldRequire = present ? isPresent(whenValue) : !isPresent(whenValue);
420
- } else {
421
- shouldRequire = isPresent(whenValue);
422
- }
423
-
424
- if (shouldRequire && !isPresent(args[rule.param])) {
425
- invalidArgument(`Argument ${rule.param} is required by constraints for ${operationId}.`, {
426
- operationId,
427
- rule,
428
- });
429
- }
430
- }
431
- }
432
-
433
- function resolveDocApiMethod(client: { doc: Record<string, unknown> }, operationId: string): (args: unknown, options?: InvokeOptions) => Promise<unknown> {
434
- const tokens = operationId.split('.').slice(1);
435
- let cursor: unknown = client.doc;
436
-
437
- for (const token of tokens) {
438
- if (!isRecord(cursor) || !(token in cursor)) {
439
- throw new SuperDocCliError(`No SDK doc method found for operation ${operationId}.`, {
440
- code: 'TOOL_DISPATCH_NOT_FOUND',
441
- details: { operationId, token },
442
- });
443
- }
444
- cursor = cursor[token];
445
- }
446
-
447
- if (typeof cursor !== 'function') {
448
- throw new SuperDocCliError(`Resolved member for ${operationId} is not callable.`, {
449
- code: 'TOOL_DISPATCH_NOT_FOUND',
450
- details: { operationId },
451
- });
452
- }
453
-
454
- return cursor as (args: unknown, options?: InvokeOptions) => Promise<unknown>;
455
- }
456
-
457
- /**
458
- * Return a canonical view of generated tools.
459
- */
460
- export async function getToolCatalog(options: { profile?: ToolProfile } = {}): Promise<ToolCatalog> {
461
- const catalog = await loadCatalog();
462
- if (!options.profile) {
463
- return catalog;
464
- }
465
-
466
- return {
467
- ...catalog,
468
- profiles: {
469
- intent: options.profile === 'intent' ? catalog.profiles.intent : { name: 'intent', tools: [] },
470
- operation: options.profile === 'operation' ? catalog.profiles.operation : { name: 'operation', tools: [] },
471
- },
472
- };
473
- }
474
-
475
- /**
476
- * List provider-formatted tools for the requested profile.
477
- */
478
- export async function listTools(provider: ToolProvider, options: { profile?: ToolProfile } = {}): Promise<unknown[]> {
479
- const profile = options.profile ?? 'intent';
480
- const bundle = await loadProviderBundle(provider);
481
- const tools = bundle.profiles[profile];
482
- if (!Array.isArray(tools)) {
483
- throw new SuperDocCliError('Tool provider bundle is missing profile tools.', {
484
- code: 'TOOLS_ASSET_INVALID',
485
- details: { provider, profile },
486
- });
487
- }
488
- return tools;
489
- }
490
-
491
- /**
492
- * Resolve a generated tool name to its canonical operation id.
493
- */
494
- export async function resolveToolOperation(toolName: string): Promise<string | null> {
495
- const map = await loadToolNameMap();
496
- return typeof map[toolName] === 'string' ? map[toolName] : null;
497
- }
498
-
499
- /**
500
- * Infer chooser-relevant document features from a `doc.info` response payload.
501
- */
502
- export function inferDocumentFeatures(infoResult: Record<string, unknown> | null | undefined): DocumentFeatures {
503
- if (!isRecord(infoResult)) {
504
- return {
505
- hasTables: false,
506
- hasLists: false,
507
- hasComments: false,
508
- hasTrackedChanges: false,
509
- isEmptyDocument: false,
510
- };
511
- }
512
-
513
- const counts = isRecord(infoResult.counts) ? infoResult.counts : {};
514
- const words = typeof counts.words === 'number' ? counts.words : 0;
515
- const paragraphs = typeof counts.paragraphs === 'number' ? counts.paragraphs : 0;
516
- const tables = typeof counts.tables === 'number' ? counts.tables : 0;
517
- const comments = typeof counts.comments === 'number' ? counts.comments : 0;
518
- const lists =
519
- typeof counts.lists === 'number'
520
- ? counts.lists
521
- : typeof counts.listItems === 'number'
522
- ? counts.listItems
523
- : 0;
524
- const trackedChanges =
525
- typeof counts.trackedChanges === 'number'
526
- ? counts.trackedChanges
527
- : typeof counts.tracked_changes === 'number'
528
- ? counts.tracked_changes
529
- : 0;
530
-
531
- return {
532
- hasTables: tables > 0,
533
- hasLists: lists > 0,
534
- hasComments: comments > 0,
535
- hasTrackedChanges: trackedChanges > 0,
536
- isEmptyDocument: words === 0 && paragraphs <= 1,
537
- };
538
- }
539
-
540
- /**
541
- * Deterministically choose a bounded tool subset for the current turn.
542
- */
543
- export async function chooseTools(input: ToolChooserInput): Promise<{
544
- tools: unknown[];
545
- selected: Array<{
546
- operationId: string;
547
- toolName: string;
548
- category: string;
549
- mutates: boolean;
550
- profile: ToolProfile;
551
- }>;
552
- excluded: Array<{ toolName: string; reason: string }>;
553
- selectionMeta: {
554
- profile: ToolProfile;
555
- phase: ToolPhase;
556
- maxTools: number;
557
- minReadTools: number;
558
- selectedCount: number;
559
- decisionVersion: string;
560
- provider: ToolProvider;
561
- };
562
- }> {
563
- const catalog = await loadCatalog();
564
- const profile = input.profile ?? 'intent';
565
- const phase = input.taskContext?.phase ?? 'read';
566
- const phasePolicy = PHASE_POLICY[phase];
567
- const featureMap = normalizeFeatures(input.documentFeatures);
568
-
569
- const maxTools = Math.max(1, input.budget?.maxTools ?? DEFAULT_MAX_TOOLS_BY_PROFILE[profile]);
570
- const minReadTools = Math.max(0, input.budget?.minReadTools ?? DEFAULT_MIN_READ_TOOLS);
571
-
572
- const includeCategories = new Set(input.policy?.includeCategories ?? phasePolicy.include);
573
- const excludeCategories = new Set([...(input.policy?.excludeCategories ?? []), ...phasePolicy.exclude]);
574
- const allowMutatingTools = input.policy?.allowMutatingTools ?? phase === 'mutate';
575
-
576
- const excluded: Array<{ toolName: string; reason: string }> = [];
577
-
578
- const profileTools = catalog.profiles[profile].tools;
579
- const indexByToolName = new Map(profileTools.map((tool) => [tool.toolName, tool]));
580
-
581
- let candidates = profileTools.filter((tool) => {
582
- if (tool.requiredCapabilities.some((capability) => !featureMap[capability])) {
583
- excluded.push({ toolName: tool.toolName, reason: 'missing-required-capability' });
584
- return false;
585
- }
586
-
587
- if (!allowMutatingTools && tool.mutates) {
588
- excluded.push({ toolName: tool.toolName, reason: 'mutations-disabled' });
589
- return false;
590
- }
591
-
592
- if (includeCategories.size > 0 && !includeCategories.has(tool.category)) {
593
- excluded.push({ toolName: tool.toolName, reason: 'category-not-included' });
594
- return false;
595
- }
596
-
597
- if (excludeCategories.has(tool.category)) {
598
- excluded.push({ toolName: tool.toolName, reason: 'phase-category-excluded' });
599
- return false;
600
- }
601
-
602
- return true;
603
- });
604
-
605
- const forceExclude = new Set(input.policy?.forceExclude ?? []);
606
- candidates = candidates.filter((tool) => {
607
- if (!forceExclude.has(tool.toolName)) return true;
608
- excluded.push({ toolName: tool.toolName, reason: 'force-excluded' });
609
- return false;
610
- });
611
-
612
- for (const forcedToolName of input.policy?.forceInclude ?? []) {
613
- const forced = indexByToolName.get(forcedToolName);
614
- if (!forced) {
615
- excluded.push({ toolName: forcedToolName, reason: 'not-in-profile' });
616
- continue;
617
- }
618
- candidates.push(forced);
619
- }
620
-
621
- candidates = [...new Map(candidates.map((tool) => [tool.toolName, tool])).values()];
622
-
623
- const selected: typeof profileTools = [];
624
- const foundational = candidates.filter((tool) => FOUNDATIONAL_READ_OPERATION_IDS.has(tool.operationId));
625
- for (const tool of foundational) {
626
- if (selected.length >= minReadTools || selected.length >= maxTools) break;
627
- selected.push(tool);
628
- }
629
-
630
- const remaining = stableSortByPhasePriority(
631
- candidates.filter((tool) => !selected.some((entry) => entry.toolName === tool.toolName)),
632
- phasePolicy.priority,
633
- );
634
-
635
- for (const tool of remaining) {
636
- if (selected.length >= maxTools) {
637
- excluded.push({ toolName: tool.toolName, reason: 'budget-trim' });
638
- continue;
639
- }
640
- selected.push(tool);
641
- }
642
-
643
- const bundle = await loadProviderBundle(input.provider);
644
- const providerTools = Array.isArray(bundle.profiles[profile]) ? bundle.profiles[profile] : [];
645
- const providerIndex = new Map(
646
- providerTools
647
- .filter((tool): tool is Record<string, unknown> => isRecord(tool) && typeof tool.name === 'string')
648
- .map((tool) => [tool.name as string, tool]),
649
- );
650
-
651
- const selectedProviderTools = selected
652
- .map((tool) => providerIndex.get(tool.toolName))
653
- .filter((tool): tool is Record<string, unknown> => Boolean(tool));
654
-
655
- return {
656
- tools: selectedProviderTools,
657
- selected: selected.map((tool) => ({
658
- operationId: tool.operationId,
659
- toolName: tool.toolName,
660
- category: tool.category,
661
- mutates: tool.mutates,
662
- profile: tool.profile,
663
- })),
664
- excluded,
665
- selectionMeta: {
666
- profile,
667
- phase,
668
- maxTools,
669
- minReadTools,
670
- selectedCount: selected.length,
671
- decisionVersion: CHOOSER_DECISION_VERSION,
672
- provider: input.provider,
673
- },
674
- };
675
- }
676
-
677
- /**
678
- * Dispatch a SuperDoc tool call to the canonical SDK `client.doc.*` operation.
679
- */
680
- export async function dispatchSuperDocTool(
681
- client: { doc: Record<string, unknown> },
682
- toolName: string,
683
- args: Record<string, unknown> = {},
684
- invokeOptions?: InvokeOptions,
685
- ): Promise<unknown> {
686
- const operationId = await resolveToolOperation(toolName);
687
- if (!operationId) {
688
- throw new SuperDocCliError(`Unknown SuperDoc tool: ${toolName}`, {
689
- code: 'TOOL_NOT_FOUND',
690
- details: { toolName },
691
- });
692
- }
693
-
694
- if (!isRecord(args)) {
695
- invalidArgument(`Tool arguments for ${toolName} must be an object.`);
696
- }
697
-
698
- validateDispatchArgs(operationId, args);
699
- const method = resolveDocApiMethod(client, operationId);
700
- return method(args, invokeOptions);
701
- }