@molecule/api-ai-tools 1.0.8 → 1.1.0

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/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-09-18T06:57:26.872Z
6
+ Generated: 2026-09-22T18:02:23.461Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -133,6 +133,50 @@ interface FileDiffEvent {
133
133
  }
134
134
  ```
135
135
 
136
+ #### `GuardOptions`
137
+
138
+ Options shared by {@link guardToolExecute} and {@link guardAITool}.
139
+
140
+ ```typescript
141
+ interface GuardOptions {
142
+ /**
143
+ * Redactor applied to a THROWN error's message before it is returned to the
144
+ * model. Defaults to this package's `redactSecrets`.
145
+ */
146
+ redactError?: (message: string) => string
147
+ }
148
+ ```
149
+
150
+ #### `NormalizableSchema`
151
+
152
+ The JSON-Schema-ish shape every tool in this package declares.
153
+
154
+ ```typescript
155
+ interface NormalizableSchema {
156
+ name: string
157
+ parameters?: {
158
+ type?: string
159
+ properties?: Record<string, { type?: string; description?: string }>
160
+ required?: string[]
161
+ }
162
+ }
163
+ ```
164
+
165
+ #### `NormalizedInput`
166
+
167
+ The result of normalizing one tool call's input.
168
+
169
+ ```typescript
170
+ interface NormalizedInput {
171
+ /** The input to hand the handler, with aliases moved onto the declared names. */
172
+ input: Record<string, unknown>
173
+ /** Declared-required parameters still absent after aliasing. */
174
+ missing: string[]
175
+ /** `alias→declared` pairs that were moved, for logging. */
176
+ aliased: string[]
177
+ }
178
+ ```
179
+
136
180
  #### `PromptContext`
137
181
 
138
182
  Context for building a composable system prompt.
@@ -355,6 +399,21 @@ function createSandboxBackend(sandbox: SandboxLike, projectRoot?: string): Execu
355
399
 
356
400
  **Returns:** A backend that proxies all filesystem calls into the sandbox.
357
401
 
402
+ #### `declaredBudgetSeconds(command, timeoutParam)`
403
+
404
+ The time budget a COMMAND declares for itself — a `timeout <seconds>` prefix
405
+ anywhere in it, or the tool call's own `timeout` (ms) parameter, whichever is
406
+ larger. Zero when it declares none.
407
+
408
+ ```typescript
409
+ function declaredBudgetSeconds(command: string, timeoutParam: unknown): number
410
+ ```
411
+
412
+ - `command` — The shell command.
413
+ - `timeoutParam` — The tool call's `timeout` input, in milliseconds.
414
+
415
+ **Returns:** The declared budget in seconds, or 0.
416
+
358
417
  #### `directoryReadHint(message, path)`
359
418
 
360
419
  Detect a "read/edit targeted a directory, not a file" failure from a backend
@@ -386,6 +445,50 @@ function discoverSkills(backend: ExecutionBackend): Promise<SkillEntry[]>
386
445
 
387
446
  **Returns:** Array of discovered skills with name, description, and path
388
447
 
448
+ #### `guardAITool(tool)`
449
+
450
+ Apply {@link guardToolExecute} to a ready-made tool object (the
451
+ `@molecule/api-ai` `AITool` shape: `{ name, description, parameters,
452
+ execute }`), returning a copy whose `execute` is guarded.
453
+
454
+ Use it at the point a consumer hands its full tool list to the model, so
455
+ tools built outside this package get the same treatment as the ones built
456
+ inside it.
457
+
458
+ ```typescript
459
+ function guardAITool(tool: T, options?: GuardOptions): T
460
+ ```
461
+
462
+ - `tool` — The tool to guard.
463
+
464
+ **Returns:** A copy with a guarded `execute`.
465
+
466
+ #### `guardToolExecute(schema, impl)`
467
+
468
+ Wrap a tool handler so every call is normalized first and a handler that
469
+ THROWS returns something the model can act on.
470
+
471
+ The guard deliberately does NOT pre-empt the handler when a required
472
+ parameter looks absent: several handlers accept a documented older shape
473
+ (`edit_file`'s top-level `old_string`/`new_string`) or answer a missing
474
+ parameter with far better guidance than a generic message could (`edit_file`
475
+ telling the model to re-read the file). It steps in only where the handler
476
+ would otherwise crash — and when it does, a still-missing required parameter
477
+ is the accurate diagnosis for that crash, so it is reported as one.
478
+
479
+ ```typescript
480
+ function guardToolExecute(
481
+ schema: NormalizableSchema,
482
+ impl: (input: Record<string, unknown>, ...rest: never[]) => Promise<unknown>,
483
+ options?: GuardOptions,
484
+ ): (input: unknown, ...rest: never[]) => Promise<unknown>
485
+ ```
486
+
487
+ - `schema` — The tool's declared schema.
488
+ - `impl` — The raw handler.
489
+
490
+ **Returns:** A handler with the same signature, guarded.
491
+
389
492
  #### `isEnvFilePath(path)`
390
493
 
391
494
  Whether a path is an env file, for which {@link redactSecrets}' full env-dump
@@ -422,6 +525,62 @@ function isValidGlob(pattern: string): boolean
422
525
 
423
526
  **Returns:** `true` when the pattern contains only allowed characters.
424
527
 
528
+ #### `missingParamError(schema, input, missing)`
529
+
530
+ The error a tool returns when a required parameter is still absent after
531
+ aliasing. Model-facing, so it names the right parameter, what was actually
532
+ sent, and the shape of a correct call — everything needed to fix it in one
533
+ turn instead of concluding the tool is broken.
534
+
535
+ ```typescript
536
+ function missingParamError(
537
+ schema: NormalizableSchema,
538
+ input: Record<string, unknown>,
539
+ missing: string[],
540
+ ): string
541
+ ```
542
+
543
+ - `schema` — The tool's declared schema.
544
+ - `input` — The normalized input (used to list the keys that were sent).
545
+ - `missing` — The required parameters still absent.
546
+
547
+ **Returns:** A single-sentence-per-clause error string.
548
+
549
+ #### `normalizeToolInput(schema, raw)`
550
+
551
+ Move known aliases onto the declared parameter names and report what is still
552
+ missing. Unknown keys are left in place — handlers ignore them, and dropping
553
+ them would hide a model's intent from anything reading the call log.
554
+
555
+ ```typescript
556
+ function normalizeToolInput(schema: NormalizableSchema, raw: unknown): NormalizedInput
557
+ ```
558
+
559
+ - `schema` — The tool's declared schema.
560
+ - `raw` — Whatever the model sent as the tool input.
561
+
562
+ **Returns:** The normalized input plus the missing-required and aliased lists.
563
+
564
+ #### `outputIsWithheldUntilExit(command)`
565
+
566
+ Whether a command's output reaches the tool only when the whole pipeline
567
+ ends — a pipe into `tail`, `grep`, `sort`, `wc` and friends, which
568
+ block-buffer when stdout is not a terminal.
569
+
570
+ It matters only in combination with an overrun: a command stopped at the
571
+ tool's ceiling normally hands back everything it printed, but behind one of
572
+ these it printed nothing, so the whole budget is spent for no information.
573
+ Measured across six real agent runs: ten such commands, every one of them
574
+ piped, 49 minutes returning a few hundred bytes each.
575
+
576
+ ```typescript
577
+ function outputIsWithheldUntilExit(command: string): boolean
578
+ ```
579
+
580
+ - `command` — The shell command.
581
+
582
+ **Returns:** True when a kill would return no useful output.
583
+
425
584
  #### `pathArgError(path, tool)`
426
585
 
427
586
  Validate a file tool's `path` argument is a non-empty string. A weak model
@@ -512,6 +671,28 @@ function shellQuote(s: string): string
512
671
 
513
672
  **Returns:** A single-quoted shell literal representing `s`.
514
673
 
674
+ #### `sliceLines(content, window)`
675
+
676
+ Return a line window of `content`, or null when no window was asked for.
677
+
678
+ `read_file` returned whole files only, so an executor that wanted part of one
679
+ shelled out: across six real agent runs, 260 of 377 shell-inspection commands
680
+ (69%) were `head`/`tail`/`sed -n` windowing a file the tool could have
681
+ windowed itself. `offset` is 1-based, matching the line numbers every other
682
+ tool reports and what `grep -n` prints.
683
+
684
+ ```typescript
685
+ function sliceLines(
686
+ content: string,
687
+ window?: { offset?: unknown; limit?: unknown },
688
+ ): { text: string; offset: number; lines: number; totalLines: number; truncated: boolean } | null
689
+ ```
690
+
691
+ - `content` — The file's full text.
692
+ - `window` — The requested `offset` (1-based) and `limit` (line count).
693
+
694
+ **Returns:** The window plus its position, or null when neither was given.
695
+
515
696
  #### `stripControlChars(s)`
516
697
 
517
698
  Strip C0 control chars (except tab, newline, CR) that break PostgreSQL JSONB
@@ -614,6 +795,25 @@ const DEFAULT_SEARCH_EXCLUDED_DIRS: readonly [
614
795
  ]
615
796
  ```
616
797
 
798
+ #### `MAX_BATCH_READ_BYTES`
799
+
800
+ Total content bytes one batched `read_file` call returns, across all files.
801
+ The per-file {@link MAX_READ_SIZE} still applies to each one.
802
+
803
+ ```typescript
804
+ const MAX_BATCH_READ_BYTES: number
805
+ ```
806
+
807
+ #### `MAX_BATCH_READ_FILES`
808
+
809
+ Most files one batched `read_file` call returns. The cap exists so a survey
810
+ of a large tree cannot swallow the turn's context in one result; the tool
811
+ reports where it stopped so the model continues from there.
812
+
813
+ ```typescript
814
+ const MAX_BATCH_READ_FILES: 25
815
+ ```
816
+
617
817
  #### `MAX_FIND_RESULTS`
618
818
 
619
819
  Max find results.
@@ -630,6 +830,23 @@ Max command output size (100KB per stream).
630
830
  const MAX_OUTPUT_SIZE: number
631
831
  ```
632
832
 
833
+ #### `MAX_READ_RETURN_CHARS`
834
+
835
+ The most one read_file call returns for ONE file when no window was asked
836
+ for, in chars. Larger files come back as their first window with a note
837
+ saying how to read the rest.
838
+
839
+ MAX_READ_SIZE (5 MB) is a bound on what the tool will open, not on what it
840
+ should hand a model: a single batched read that returned a 1.2 MB README
841
+ put the next three provider calls at 366k tokens (~330k of them fresh)
842
+ against a 120k-token context cap (X0 run x5, call 26). ~80 KB is ~20–25k
843
+ tokens — a fifth of that cap, and more than any one file read usually
844
+ needs.
845
+
846
+ ```typescript
847
+ const MAX_READ_RETURN_CHARS: number
848
+ ```
849
+
633
850
  #### `MAX_READ_SIZE`
634
851
 
635
852
  Max file size for read_file (5MB).
@@ -638,6 +855,16 @@ Max file size for read_file (5MB).
638
855
  const MAX_READ_SIZE: number
639
856
  ```
640
857
 
858
+ #### `MAX_SEARCH_CONTEXT_LINES`
859
+
860
+ Most lines of surrounding context `search_files` will return per match.
861
+ Bounded so a broad pattern with a generous context cannot pull a whole tree
862
+ into one reply.
863
+
864
+ ```typescript
865
+ const MAX_SEARCH_CONTEXT_LINES: 40
866
+ ```
867
+
641
868
  #### `MAX_SEARCH_RESULTS`
642
869
 
643
870
  Max search results.
@@ -685,6 +912,19 @@ Peer dependencies:
685
912
  rules via `blockCommand` (return an actionable error string to refuse — it is
686
913
  shown to the model verbatim) and enable `blockDangerousCommands` (default
687
914
  FALSE) where appropriate.
915
+ - **`read_file` takes `paths` as well as `path`.** An agent that issues one
916
+ tool call per model round-trip pays a full round-trip per file, so a project
917
+ survey costs minutes; batched, the same reads cost one call each 25 files
918
+ (`MAX_BATCH_READ_FILES` / `MAX_BATCH_READ_BYTES`, which report where they
919
+ stopped). Each entry carries its own content or its own error, so one bad
920
+ path never fails the batch.
921
+ - **A parameter under another name is not a broken tool.** Every tool's input
922
+ is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
923
+ snake_case), and a handler that throws returns an error naming the missing
924
+ parameter rather than a bare internal message — which a weak executor reads
925
+ as a broken tool and routes around, taking its verification with it. Use
926
+ `guardAITool` to give tools built elsewhere the same treatment, passing
927
+ `redactError` so a thrown message is masked by your own redactor.
688
928
  - **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
689
929
  should not receive `write_file`/`exec_command`.
690
930
  - Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
package/dist/index.d.ts CHANGED
@@ -23,6 +23,19 @@
23
23
  * rules via `blockCommand` (return an actionable error string to refuse — it is
24
24
  * shown to the model verbatim) and enable `blockDangerousCommands` (default
25
25
  * FALSE) where appropriate.
26
+ * - **`read_file` takes `paths` as well as `path`.** An agent that issues one
27
+ * tool call per model round-trip pays a full round-trip per file, so a project
28
+ * survey costs minutes; batched, the same reads cost one call each 25 files
29
+ * (`MAX_BATCH_READ_FILES` / `MAX_BATCH_READ_BYTES`, which report where they
30
+ * stopped). Each entry carries its own content or its own error, so one bad
31
+ * path never fails the batch.
32
+ * - **A parameter under another name is not a broken tool.** Every tool's input
33
+ * is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
34
+ * snake_case), and a handler that throws returns an error naming the missing
35
+ * parameter rather than a bare internal message — which a weak executor reads
36
+ * as a broken tool and routes around, taking its verification with it. Use
37
+ * `guardAITool` to give tools built elsewhere the same treatment, passing
38
+ * `redactError` so a thrown message is masked by your own redactor.
26
39
  * - **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
27
40
  * should not receive `write_file`/`exec_command`.
28
41
  * - Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
@@ -85,6 +98,7 @@
85
98
  export * from './backends/local.js';
86
99
  export * from './backends/sandbox.js';
87
100
  export * from './browser-guard.js';
101
+ export * from './input-normalizer.js';
88
102
  export * from './schemas.js';
89
103
  export * from './system-prompt.js';
90
104
  export * from './tools.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmFG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -23,6 +23,19 @@
23
23
  * rules via `blockCommand` (return an actionable error string to refuse — it is
24
24
  * shown to the model verbatim) and enable `blockDangerousCommands` (default
25
25
  * FALSE) where appropriate.
26
+ * - **`read_file` takes `paths` as well as `path`.** An agent that issues one
27
+ * tool call per model round-trip pays a full round-trip per file, so a project
28
+ * survey costs minutes; batched, the same reads cost one call each 25 files
29
+ * (`MAX_BATCH_READ_FILES` / `MAX_BATCH_READ_BYTES`, which report where they
30
+ * stopped). Each entry carries its own content or its own error, so one bad
31
+ * path never fails the batch.
32
+ * - **A parameter under another name is not a broken tool.** Every tool's input
33
+ * is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
34
+ * snake_case), and a handler that throws returns an error naming the missing
35
+ * parameter rather than a bare internal message — which a weak executor reads
36
+ * as a broken tool and routes around, taking its verification with it. Use
37
+ * `guardAITool` to give tools built elsewhere the same treatment, passing
38
+ * `redactError` so a thrown message is masked by your own redactor.
26
39
  * - **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
27
40
  * should not receive `write_file`/`exec_command`.
28
41
  * - Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
@@ -85,6 +98,7 @@
85
98
  export * from './backends/local.js';
86
99
  export * from './backends/sandbox.js';
87
100
  export * from './browser-guard.js';
101
+ export * from './input-normalizer.js';
88
102
  export * from './schemas.js';
89
103
  export * from './system-prompt.js';
90
104
  export * from './tools.js';
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Tool-input normalization: accept the parameter names a weak executor
3
+ * actually sends, and never let a missing one reach a handler as `undefined`.
4
+ *
5
+ * @module
6
+ */
7
+ /** The JSON-Schema-ish shape every tool in this package declares. */
8
+ export interface NormalizableSchema {
9
+ name: string;
10
+ parameters?: {
11
+ type?: string;
12
+ properties?: Record<string, {
13
+ type?: string;
14
+ description?: string;
15
+ }>;
16
+ required?: string[];
17
+ };
18
+ }
19
+ /** The result of normalizing one tool call's input. */
20
+ export interface NormalizedInput {
21
+ /** The input to hand the handler, with aliases moved onto the declared names. */
22
+ input: Record<string, unknown>;
23
+ /** Declared-required parameters still absent after aliasing. */
24
+ missing: string[];
25
+ /** `alias→declared` pairs that were moved, for logging. */
26
+ aliased: string[];
27
+ }
28
+ /** Options shared by {@link guardToolExecute} and {@link guardAITool}. */
29
+ export interface GuardOptions {
30
+ /**
31
+ * Redactor applied to a THROWN error's message before it is returned to the
32
+ * model. Defaults to this package's `redactSecrets`.
33
+ */
34
+ redactError?: (message: string) => string;
35
+ }
36
+ /**
37
+ * Move known aliases onto the declared parameter names and report what is still
38
+ * missing. Unknown keys are left in place — handlers ignore them, and dropping
39
+ * them would hide a model's intent from anything reading the call log.
40
+ *
41
+ * @param schema - The tool's declared schema.
42
+ * @param raw - Whatever the model sent as the tool input.
43
+ * @returns The normalized input plus the missing-required and aliased lists.
44
+ */
45
+ export declare function normalizeToolInput(schema: NormalizableSchema, raw: unknown): NormalizedInput;
46
+ /**
47
+ * The error a tool returns when a required parameter is still absent after
48
+ * aliasing. Model-facing, so it names the right parameter, what was actually
49
+ * sent, and the shape of a correct call — everything needed to fix it in one
50
+ * turn instead of concluding the tool is broken.
51
+ *
52
+ * @param schema - The tool's declared schema.
53
+ * @param input - The normalized input (used to list the keys that were sent).
54
+ * @param missing - The required parameters still absent.
55
+ * @returns A single-sentence-per-clause error string.
56
+ */
57
+ export declare function missingParamError(schema: NormalizableSchema, input: Record<string, unknown>, missing: string[]): string;
58
+ /**
59
+ * Wrap a tool handler so every call is normalized first and a handler that
60
+ * THROWS returns something the model can act on.
61
+ *
62
+ * The guard deliberately does NOT pre-empt the handler when a required
63
+ * parameter looks absent: several handlers accept a documented older shape
64
+ * (`edit_file`'s top-level `old_string`/`new_string`) or answer a missing
65
+ * parameter with far better guidance than a generic message could (`edit_file`
66
+ * telling the model to re-read the file). It steps in only where the handler
67
+ * would otherwise crash — and when it does, a still-missing required parameter
68
+ * is the accurate diagnosis for that crash, so it is reported as one.
69
+ *
70
+ * @param schema - The tool's declared schema.
71
+ * @param impl - The raw handler.
72
+ * @returns A handler with the same signature, guarded.
73
+ */
74
+ export declare function guardToolExecute(schema: NormalizableSchema, impl: (input: Record<string, unknown>, ...rest: never[]) => Promise<unknown>, options?: GuardOptions): (input: unknown, ...rest: never[]) => Promise<unknown>;
75
+ /**
76
+ * Apply {@link guardToolExecute} to a ready-made tool object (the
77
+ * `@molecule/api-ai` `AITool` shape: `{ name, description, parameters,
78
+ * execute }`), returning a copy whose `execute` is guarded.
79
+ *
80
+ * Use it at the point a consumer hands its full tool list to the model, so
81
+ * tools built outside this package get the same treatment as the ones built
82
+ * inside it.
83
+ *
84
+ * @param tool - The tool to guard.
85
+ * @returns A copy with a guarded `execute`.
86
+ */
87
+ export declare function guardAITool<T extends {
88
+ name: string;
89
+ parameters?: unknown;
90
+ execute: (input: unknown, ...rest: never[]) => Promise<unknown>;
91
+ }>(tool: T, options?: GuardOptions): T;
92
+ //# sourceMappingURL=input-normalizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input-normalizer.d.ts","sourceRoot":"","sources":["../src/input-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QACpE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KACpB,CAAA;CACF;AAED,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAiDD,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,EAAE,GAAG,EAAE,OAAO,GAAG,eAAe,CAiD5F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EAAE,GAChB,MAAM,CAaR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,EAC5E,OAAO,GAAE,YAAiB,GACzB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAsBxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,CAAC,SAAS;IACR,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAChE,EACD,IAAI,EAAE,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,CAAC,CAaxC"}
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Tool-input normalization: accept the parameter names a weak executor
3
+ * actually sends, and never let a missing one reach a handler as `undefined`.
4
+ *
5
+ * @module
6
+ */
7
+ import { redactSecrets } from './utilities.js';
8
+ /**
9
+ * Other names a model sends for a declared parameter, most likely first.
10
+ *
11
+ * Every entry here is a name a real run actually produced or a direct synonym
12
+ * of one. `cmd` is the one that cost the most: `exec_command` read only
13
+ * `input.command`, so a call sent as `{ cmd, timeout }` reached
14
+ * `checkBlockedCommand(undefined)` and died with `Cannot read properties of
15
+ * undefined (reading 'match')` — an internal TypeError, not a usable error. The
16
+ * executor read that as "the shell tool is failing consistently now", stopped
17
+ * verifying, and ended the turn reporting "All checks pass" while seven
18
+ * acceptance checks failed (X0 rehearsal 83).
19
+ */
20
+ const PARAM_ALIASES = {
21
+ command: ['cmd', 'shell', 'script', 'bash', 'commandLine', 'command_line', 'run'],
22
+ path: ['file', 'filePath', 'file_path', 'filename', 'file_name', 'filepath', 'target'],
23
+ content: [
24
+ 'contents',
25
+ 'body',
26
+ 'text',
27
+ 'data',
28
+ 'source',
29
+ 'markdown',
30
+ 'fileContent',
31
+ 'file_content',
32
+ ],
33
+ pattern: ['query', 'regex', 'search', 'searchPattern', 'search_pattern', 'text'],
34
+ old_path: ['from', 'src', 'source', 'sourcePath', 'source_path', 'oldPath', 'old'],
35
+ new_path: ['to', 'dest', 'destination', 'destinationPath', 'destination_path', 'newPath', 'new'],
36
+ name: ['skill', 'skillName', 'skill_name', 'title', 'planName', 'plan_name'],
37
+ cwd: ['dir', 'directory', 'workdir', 'workingDirectory', 'working_directory'],
38
+ include: ['glob', 'filePattern', 'file_pattern'],
39
+ };
40
+ /** `old_path` ⇄ `oldPath`, so a casing slip is never a missing parameter. */
41
+ function caseVariants(key) {
42
+ const camel = key.replace(/_([a-z])/g, (_m, c) => c.toUpperCase());
43
+ const snake = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
44
+ return [camel, snake].filter((v) => v !== key);
45
+ }
46
+ /** Present means the key is there with a value that is not null/undefined. */
47
+ function present(obj, key) {
48
+ return Object.hasOwn(obj, key) && obj[key] !== undefined && obj[key] !== null;
49
+ }
50
+ const warned = new Set();
51
+ /**
52
+ * Move known aliases onto the declared parameter names and report what is still
53
+ * missing. Unknown keys are left in place — handlers ignore them, and dropping
54
+ * them would hide a model's intent from anything reading the call log.
55
+ *
56
+ * @param schema - The tool's declared schema.
57
+ * @param raw - Whatever the model sent as the tool input.
58
+ * @returns The normalized input plus the missing-required and aliased lists.
59
+ */
60
+ export function normalizeToolInput(schema, raw) {
61
+ const input = typeof raw === 'object' && raw !== null && !Array.isArray(raw)
62
+ ? { ...raw }
63
+ : {};
64
+ const properties = schema.parameters?.properties ?? {};
65
+ const declared = Object.keys(properties);
66
+ const declaredSet = new Set(declared);
67
+ const aliased = [];
68
+ for (const key of declared) {
69
+ if (present(input, key))
70
+ continue;
71
+ const candidates = [...caseVariants(key), ...(PARAM_ALIASES[key] ?? [])];
72
+ for (const alias of candidates) {
73
+ // Never steal a name this same schema declares — `path` on rename_file
74
+ // would be ambiguous between old_path and new_path, so it stays unread.
75
+ if (declaredSet.has(alias))
76
+ continue;
77
+ if (!present(input, alias))
78
+ continue;
79
+ input[key] = input[alias];
80
+ aliased.push(`${alias}→${key}`);
81
+ break;
82
+ }
83
+ }
84
+ // A declared string that arrived as a number/boolean is stringified rather
85
+ // than handed to a handler that will call .match()/.split() on it.
86
+ for (const key of declared) {
87
+ if (properties[key]?.type !== 'string')
88
+ continue;
89
+ const value = input[key];
90
+ if (typeof value === 'number' || typeof value === 'boolean')
91
+ input[key] = String(value);
92
+ }
93
+ const required = schema.parameters?.required ?? [];
94
+ const missing = required.filter((key) => {
95
+ if (!present(input, key))
96
+ return true;
97
+ const value = input[key];
98
+ return typeof value === 'string' && value.trim() === '' && key !== 'content';
99
+ });
100
+ if (aliased.length > 0) {
101
+ const signature = `${schema.name}:${aliased.join(',')}`;
102
+ if (!warned.has(signature)) {
103
+ warned.add(signature);
104
+ console.warn(`[ai-tools] ${schema.name} input normalized (${aliased.join(', ')})`);
105
+ }
106
+ }
107
+ return { input, missing, aliased };
108
+ }
109
+ /**
110
+ * The error a tool returns when a required parameter is still absent after
111
+ * aliasing. Model-facing, so it names the right parameter, what was actually
112
+ * sent, and the shape of a correct call — everything needed to fix it in one
113
+ * turn instead of concluding the tool is broken.
114
+ *
115
+ * @param schema - The tool's declared schema.
116
+ * @param input - The normalized input (used to list the keys that were sent).
117
+ * @param missing - The required parameters still absent.
118
+ * @returns A single-sentence-per-clause error string.
119
+ */
120
+ export function missingParamError(schema, input, missing) {
121
+ const properties = schema.parameters?.properties ?? {};
122
+ const sent = Object.keys(input);
123
+ const names = missing.map((k) => `"${k}"`).join(', ');
124
+ const example = Object.fromEntries((schema.parameters?.required ?? []).map((k) => [k, `<${properties[k]?.type ?? 'value'}>`]));
125
+ return (`${schema.name} needs ${missing.length === 1 ? 'the parameter' : 'the parameters'} ${names}. ` +
126
+ (sent.length > 0 ? `You sent: ${sent.join(', ')}. ` : 'You sent no parameters. ') +
127
+ `Call it as ${JSON.stringify(example)}. This is a parameter-name problem, not a broken tool — ` +
128
+ 'retry the same call with the names above.');
129
+ }
130
+ /**
131
+ * Wrap a tool handler so every call is normalized first and a handler that
132
+ * THROWS returns something the model can act on.
133
+ *
134
+ * The guard deliberately does NOT pre-empt the handler when a required
135
+ * parameter looks absent: several handlers accept a documented older shape
136
+ * (`edit_file`'s top-level `old_string`/`new_string`) or answer a missing
137
+ * parameter with far better guidance than a generic message could (`edit_file`
138
+ * telling the model to re-read the file). It steps in only where the handler
139
+ * would otherwise crash — and when it does, a still-missing required parameter
140
+ * is the accurate diagnosis for that crash, so it is reported as one.
141
+ *
142
+ * @param schema - The tool's declared schema.
143
+ * @param impl - The raw handler.
144
+ * @returns A handler with the same signature, guarded.
145
+ */
146
+ export function guardToolExecute(schema, impl, options = {}) {
147
+ // A thrown message is untrusted text (a connection string in a driver error,
148
+ // a key echoed by a failing command). The guard turns a throw into a RETURNED
149
+ // result, which leaves the consumer's thrown-error redaction path unused — so
150
+ // the message is redacted here, before it can reach the model, the SSE stream
151
+ // or the stored transcript. Defaults to this package's name-keyword redactor;
152
+ // pass a stronger one where the consumer has it.
153
+ const redactError = options.redactError ?? redactSecrets;
154
+ return async (raw, ...rest) => {
155
+ const { input, missing } = normalizeToolInput(schema, raw);
156
+ try {
157
+ return await impl(input, ...rest);
158
+ }
159
+ catch (e) {
160
+ if (missing.length > 0)
161
+ return { error: missingParamError(schema, input, missing) };
162
+ const message = redactError(e instanceof Error ? e.message : String(e));
163
+ return {
164
+ error: `${schema.name} failed: ${message}. Parameters sent: ${Object.keys(input).join(', ') || '(none)'}. ` +
165
+ `This tool expects ${JSON.stringify(schema.parameters?.required ?? [])} — check them and retry.`,
166
+ };
167
+ }
168
+ };
169
+ }
170
+ /**
171
+ * Apply {@link guardToolExecute} to a ready-made tool object (the
172
+ * `@molecule/api-ai` `AITool` shape: `{ name, description, parameters,
173
+ * execute }`), returning a copy whose `execute` is guarded.
174
+ *
175
+ * Use it at the point a consumer hands its full tool list to the model, so
176
+ * tools built outside this package get the same treatment as the ones built
177
+ * inside it.
178
+ *
179
+ * @param tool - The tool to guard.
180
+ * @returns A copy with a guarded `execute`.
181
+ */
182
+ export function guardAITool(tool, options = {}) {
183
+ const schema = {
184
+ name: tool.name,
185
+ parameters: tool.parameters,
186
+ };
187
+ return {
188
+ ...tool,
189
+ execute: guardToolExecute(schema, tool.execute, options),
190
+ };
191
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CA2O1C,CAAA"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAqT1C,CAAA"}