@molecule/api-ai-tools 1.5.8 → 1.5.10

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-10-15T23:00:29.499Z
6
+ Generated: 2026-10-16T00:26:34.364Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-ai-tools
@@ -58,6 +58,62 @@ npm install @molecule/api-ai-tools @molecule/api-ai
58
58
 
59
59
  ### Interfaces
60
60
 
61
+ #### `NormalizableSchema`
62
+
63
+ The JSON-Schema-ish shape every tool in this package declares.
64
+
65
+ ```typescript
66
+ interface NormalizableSchema {
67
+ name: string
68
+ parameters?: {
69
+ type?: string
70
+ properties?: Record<string, { type?: string; description?: string }>
71
+ required?: string[]
72
+ }
73
+ }
74
+ ```
75
+
76
+ #### `NormalizedInput`
77
+
78
+ The result of normalizing one tool call's input.
79
+
80
+ ```typescript
81
+ interface NormalizedInput {
82
+ /** The input to hand the handler, with aliases moved onto the declared names. */
83
+ input: Record<string, unknown>
84
+ /** Declared-required parameters still absent after aliasing. */
85
+ missing: string[]
86
+ /** `alias→declared` pairs that were moved, for logging. */
87
+ aliased: string[]
88
+ }
89
+ ```
90
+
91
+ #### `GuardOptions`
92
+
93
+ Options shared by {@link guardToolExecute} and {@link guardAITool}.
94
+
95
+ ```typescript
96
+ interface GuardOptions {
97
+ /**
98
+ * Redactor applied to a THROWN error's message before it is returned to the
99
+ * model. Defaults to this package's `redactSecrets`.
100
+ */
101
+ redactError?: (message: string) => string
102
+ }
103
+ ```
104
+
105
+ #### `ToolSchema`
106
+
107
+ JSON-schema-backed definition for a single agent tool.
108
+
109
+ ```typescript
110
+ interface ToolSchema {
111
+ name: string
112
+ description: string
113
+ parameters: JSONSchema
114
+ }
115
+ ```
116
+
61
117
  #### `ExecutionBackend`
62
118
 
63
119
  Abstraction over the execution environment.
@@ -110,117 +166,6 @@ interface ExecutionBackend {
110
166
  }
111
167
  ```
112
168
 
113
- #### `FileChangeEvent`
114
-
115
- Payload emitted when a file is created, modified, or deleted structurally.
116
-
117
- ```typescript
118
- interface FileChangeEvent {
119
- type: 'created' | 'modified' | 'deleted'
120
- path: string
121
- }
122
- ```
123
-
124
- #### `FileDiffEvent`
125
-
126
- Payload emitted when a tracked file changes contents.
127
-
128
- ```typescript
129
- interface FileDiffEvent {
130
- path: string
131
- oldContent: string | null
132
- newContent: string
133
- }
134
- ```
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
-
180
- #### `PromptContext`
181
-
182
- Context for building a composable system prompt.
183
-
184
- ```typescript
185
- interface PromptContext {
186
- /** Agent identity (e.g. 'Synthase', 'Polish Agent'). */
187
- agentName: string
188
-
189
- /** Project root path. */
190
- projectRoot: string
191
-
192
- /** Names of available tools (for the tool listing section). */
193
- tools: string[]
194
-
195
- /** Project-specific rules (AGENTS.md or CLAUDE.md content). */
196
- projectDocs?: string
197
-
198
- /** Additional skill/reference content to inject. */
199
- skills?: string[]
200
-
201
- /** Discovered skills to list in the prompt (use load_skill to read on demand). */
202
- discoveredSkills?: SkillEntry[]
203
-
204
- /** Custom sections to append to the prompt. */
205
- customSections?: string[]
206
- }
207
- ```
208
-
209
- #### `SkillEntry`
210
-
211
- Metadata for a discovered skill (used in PromptContext).
212
-
213
- ```typescript
214
- interface SkillEntry {
215
- /** Skill name. */
216
- name: string
217
- /** Short description. */
218
- description: string
219
- /** Relative path to the SKILL.md file. */
220
- path: string
221
- }
222
- ```
223
-
224
169
  #### `ToolBuildConfig`
225
170
 
226
171
  Configuration for building the tool set.
@@ -308,15 +253,70 @@ interface ToolBuildConfig {
308
253
  }
309
254
  ```
310
255
 
311
- #### `ToolSchema`
256
+ #### `FileDiffEvent`
312
257
 
313
- JSON-schema-backed definition for a single agent tool.
258
+ Payload emitted when a tracked file changes contents.
314
259
 
315
260
  ```typescript
316
- interface ToolSchema {
261
+ interface FileDiffEvent {
262
+ path: string
263
+ oldContent: string | null
264
+ newContent: string
265
+ }
266
+ ```
267
+
268
+ #### `FileChangeEvent`
269
+
270
+ Payload emitted when a file is created, modified, or deleted structurally.
271
+
272
+ ```typescript
273
+ interface FileChangeEvent {
274
+ type: 'created' | 'modified' | 'deleted'
275
+ path: string
276
+ }
277
+ ```
278
+
279
+ #### `SkillEntry`
280
+
281
+ Metadata for a discovered skill (used in PromptContext).
282
+
283
+ ```typescript
284
+ interface SkillEntry {
285
+ /** Skill name. */
317
286
  name: string
287
+ /** Short description. */
318
288
  description: string
319
- parameters: JSONSchema
289
+ /** Relative path to the SKILL.md file. */
290
+ path: string
291
+ }
292
+ ```
293
+
294
+ #### `PromptContext`
295
+
296
+ Context for building a composable system prompt.
297
+
298
+ ```typescript
299
+ interface PromptContext {
300
+ /** Agent identity (e.g. 'Synthase', 'Polish Agent'). */
301
+ agentName: string
302
+
303
+ /** Project root path. */
304
+ projectRoot: string
305
+
306
+ /** Names of available tools (for the tool listing section). */
307
+ tools: string[]
308
+
309
+ /** Project-specific rules (AGENTS.md or CLAUDE.md content). */
310
+ projectDocs?: string
311
+
312
+ /** Additional skill/reference content to inject. */
313
+ skills?: string[]
314
+
315
+ /** Discovered skills to list in the prompt (use load_skill to read on demand). */
316
+ discoveredSkills?: SkillEntry[]
317
+
318
+ /** Custom sections to append to the prompt. */
319
+ customSections?: string[]
320
320
  }
321
321
  ```
322
322
 
@@ -338,112 +338,117 @@ A tool call the handler refuses with a message meant for the model as is —
338
338
  thrown from deep inside a handler (a path check), returned by the guard as the
339
339
  call's `error` without the generic "check your parameters" wrapping.
340
340
 
341
- ### Functions
342
-
343
- #### `buildAgentPrompt(ctx)`
341
+ ```typescript
342
+ class ToolInputError extends Error {}
343
+ ```
344
344
 
345
- Build a coding-focused system prompt from composable sections.
345
+ ### Functions
346
346
 
347
- Returns a string that includes:
347
+ #### `createLocalBackend(projectRoot)`
348
348
 
349
- - Agent identity
350
- - Available tools listing
351
- - Coding best practices
352
- - Tool argument formatting guidance
353
- - Project docs (if provided)
354
- - Discovered skills listing (if provided)
355
- - Inline skills (if provided)
356
- - Custom sections (if provided)
349
+ Create an ExecutionBackend that operates on the local filesystem.
357
350
 
358
351
  ```typescript
359
- function buildAgentPrompt(ctx: PromptContext): string
352
+ function createLocalBackend(projectRoot: string): ExecutionBackend
360
353
  ```
361
354
 
362
- - `ctx` — Prompt construction inputs (tools, docs, skills, etc.).
355
+ - `projectRoot` — Absolute path to the project root directory
363
356
 
364
- **Returns:** Fully assembled system prompt text for the coding agent.
357
+ **Returns:** A backend wired to `fs/promises` and guarded subprocess calls.
365
358
 
366
- #### `buildTools(backend, config)`
359
+ #### `createSandboxBackend(sandbox, projectRoot)`
367
360
 
368
- Build a complete set of AI agent tools bound to an execution backend.
361
+ Create an ExecutionBackend that delegates to a Docker sandbox instance.
362
+ The sandbox.exec method is inherently safe — it runs inside an isolated Docker container.
369
363
 
370
364
  ```typescript
371
- function buildTools(backend: ExecutionBackend, config?: ToolBuildConfig): AITool[]
365
+ function createSandboxBackend(sandbox: SandboxLike, projectRoot?: string): ExecutionBackend
372
366
  ```
373
367
 
374
- - `backend` — The execution environment (sandbox or local filesystem)
375
- - `config` — Optional configuration for security, callbacks, and tool selection
368
+ - `sandbox` — A running Sandbox instance from `@molecule/api-code-sandbox`
369
+ - `projectRoot` — Root directory inside the sandbox (default: '/workspace')
376
370
 
377
- **Returns:** Array of AITool objects ready to pass to an AI provider
371
+ **Returns:** A backend that proxies all filesystem calls into the sandbox.
378
372
 
379
- #### `checkBlockedCommand(command)`
373
+ #### `normalizeToolInput(schema, raw)`
380
374
 
381
- Check if a command is blocked for security reasons. Returns error message or null if allowed.
375
+ Move known aliases onto the declared parameter names and report what is still
376
+ missing. Unknown keys are left in place — handlers ignore them, and dropping
377
+ them would hide a model's intent from anything reading the call log.
382
378
 
383
379
  ```typescript
384
- function checkBlockedCommand(command: string): string | null
380
+ function normalizeToolInput(schema: NormalizableSchema, raw: unknown): NormalizedInput
385
381
  ```
386
382
 
387
- - `command` — Shell command string proposed for execution.
383
+ - `schema` — The tool's declared schema.
384
+ - `raw` — Whatever the model sent as the tool input.
388
385
 
389
- **Returns:** A human-readable block reason, or `null` when the command is allowed.
386
+ **Returns:** The normalized input plus the missing-required and aliased lists.
390
387
 
391
- #### `createLocalBackend(projectRoot)`
388
+ #### `missingParamError(schema, input, missing)`
392
389
 
393
- Create an ExecutionBackend that operates on the local filesystem.
390
+ The error a tool returns when a required parameter is still absent after
391
+ aliasing. Model-facing, so it names the right parameter, what was actually
392
+ sent, and the shape of a correct call — everything needed to fix it in one
393
+ turn instead of concluding the tool is broken.
394
394
 
395
395
  ```typescript
396
- function createLocalBackend(projectRoot: string): ExecutionBackend
396
+ function missingParamError(
397
+ schema: NormalizableSchema,
398
+ input: Record<string, unknown>,
399
+ missing: string[],
400
+ ): string
397
401
  ```
398
402
 
399
- - `projectRoot` — Absolute path to the project root directory
400
-
401
- **Returns:** A backend wired to `fs/promises` and guarded subprocess calls.
402
-
403
- #### `createSandboxBackend(sandbox, projectRoot)`
404
-
405
- Create an ExecutionBackend that delegates to a Docker sandbox instance.
406
- The sandbox.exec method is inherently safe — it runs inside an isolated Docker container.
407
-
408
- ```typescript
409
- function createSandboxBackend(sandbox: SandboxLike, projectRoot?: string): ExecutionBackend
410
- ```
403
+ - `schema` — The tool's declared schema.
404
+ - `input` — The normalized input (used to list the keys that were sent).
405
+ - `missing` — The required parameters still absent.
411
406
 
412
- - `sandbox` — A running Sandbox instance from `@molecule/api-code-sandbox`
413
- - `projectRoot` — Root directory inside the sandbox (default: '/workspace')
407
+ **Returns:** A single-sentence-per-clause error string.
414
408
 
415
- **Returns:** A backend that proxies all filesystem calls into the sandbox.
409
+ #### `guardToolExecute(schema, impl)`
416
410
 
417
- #### `declaredBudgetSeconds(command, timeoutParam)`
411
+ Wrap a tool handler so every call is normalized first and a handler that
412
+ THROWS returns something the model can act on.
418
413
 
419
- The time budget a COMMAND declares for itself — a `timeout <seconds>` prefix
420
- anywhere in it, or the tool call's own `timeout` (ms) parameter, whichever is
421
- larger. Zero when it declares none.
414
+ The guard deliberately does NOT pre-empt the handler when a required
415
+ parameter looks absent: several handlers accept a documented older shape
416
+ (`edit_file`'s top-level `old_string`/`new_string`) or answer a missing
417
+ parameter with far better guidance than a generic message could (`edit_file`
418
+ telling the model to re-read the file). It steps in only where the handler
419
+ would otherwise crash — and when it does, a still-missing required parameter
420
+ is the accurate diagnosis for that crash, so it is reported as one.
422
421
 
423
422
  ```typescript
424
- function declaredBudgetSeconds(command: string, timeoutParam: unknown): number
423
+ function guardToolExecute(
424
+ schema: NormalizableSchema,
425
+ impl: (input: Record<string, unknown>, ...rest: never[]) => Promise<unknown>,
426
+ options?: GuardOptions,
427
+ ): (input: unknown, ...rest: never[]) => Promise<unknown>
425
428
  ```
426
429
 
427
- - `command` — The shell command.
428
- - `timeoutParam` — The tool call's `timeout` input, in milliseconds.
430
+ - `schema` — The tool's declared schema.
431
+ - `impl` — The raw handler.
429
432
 
430
- **Returns:** The declared budget in seconds, or 0.
433
+ **Returns:** A handler with the same signature, guarded.
431
434
 
432
- #### `directoryReadHint(message, path)`
435
+ #### `guardAITool(tool)`
433
436
 
434
- Detect a "read/edit targeted a directory, not a file" failure from a backend
435
- error message (local fs `EISDIR` or the sandbox's `cat: X: Is a directory`),
436
- and return an actionable message steering the model to `list_files`. Returns
437
- null when the error is not a directory error.
437
+ Apply {@link guardToolExecute} to a ready-made tool object (the
438
+ `@molecule/api-ai` `AITool` shape: `{ name, description, parameters,
439
+ execute }`), returning a copy whose `execute` is guarded.
440
+
441
+ Use it at the point a consumer hands its full tool list to the model, so
442
+ tools built outside this package get the same treatment as the ones built
443
+ inside it.
438
444
 
439
445
  ```typescript
440
- function directoryReadHint(message: string, path: string): string | null
446
+ function guardAITool(tool: T, options?: GuardOptions): T
441
447
  ```
442
448
 
443
- - `message` — The backend error message.
444
- - `path` — The resolved path that was targeted.
449
+ - `tool` — The tool to guard.
445
450
 
446
- **Returns:** An actionable directory-error string, or null.
451
+ **Returns:** A copy with a guarded `execute`.
447
452
 
448
453
  #### `discoverSkills(backend)`
449
454
 
@@ -460,139 +465,141 @@ function discoverSkills(backend: ExecutionBackend): Promise<SkillEntry[]>
460
465
 
461
466
  **Returns:** Array of discovered skills with name, description, and path
462
467
 
463
- #### `guardAITool(tool)`
468
+ #### `buildAgentPrompt(ctx)`
464
469
 
465
- Apply {@link guardToolExecute} to a ready-made tool object (the
466
- `@molecule/api-ai` `AITool` shape: `{ name, description, parameters,
467
- execute }`), returning a copy whose `execute` is guarded.
470
+ Build a coding-focused system prompt from composable sections.
468
471
 
469
- Use it at the point a consumer hands its full tool list to the model, so
470
- tools built outside this package get the same treatment as the ones built
471
- inside it.
472
+ Returns a string that includes:
473
+
474
+ - Agent identity
475
+ - Available tools listing
476
+ - Coding best practices
477
+ - Tool argument formatting guidance
478
+ - Project docs (if provided)
479
+ - Discovered skills listing (if provided)
480
+ - Inline skills (if provided)
481
+ - Custom sections (if provided)
472
482
 
473
483
  ```typescript
474
- function guardAITool(tool: T, options?: GuardOptions): T
484
+ function buildAgentPrompt(ctx: PromptContext): string
475
485
  ```
476
486
 
477
- - `tool` — The tool to guard.
487
+ - `ctx` — Prompt construction inputs (tools, docs, skills, etc.).
478
488
 
479
- **Returns:** A copy with a guarded `execute`.
489
+ **Returns:** Fully assembled system prompt text for the coding agent.
480
490
 
481
- #### `guardToolExecute(schema, impl)`
491
+ #### `buildTools(backend, config)`
482
492
 
483
- Wrap a tool handler so every call is normalized first and a handler that
484
- THROWS returns something the model can act on.
493
+ Build a complete set of AI agent tools bound to an execution backend.
485
494
 
486
- The guard deliberately does NOT pre-empt the handler when a required
487
- parameter looks absent: several handlers accept a documented older shape
488
- (`edit_file`'s top-level `old_string`/`new_string`) or answer a missing
489
- parameter with far better guidance than a generic message could (`edit_file`
490
- telling the model to re-read the file). It steps in only where the handler
491
- would otherwise crash — and when it does, a still-missing required parameter
492
- is the accurate diagnosis for that crash, so it is reported as one.
495
+ ```typescript
496
+ function buildTools(backend: ExecutionBackend, config?: ToolBuildConfig): AITool[]
497
+ ```
498
+
499
+ - `backend` — The execution environment (sandbox or local filesystem)
500
+ - `config` — Optional configuration for security, callbacks, and tool selection
501
+
502
+ **Returns:** Array of AITool objects ready to pass to an AI provider
503
+
504
+ #### `shellQuote(s)`
505
+
506
+ Shell-safe quoting using single quotes. Unlike JSON.stringify (double quotes),
507
+ single-quoted strings prevent command substitution ($(), backticks) and variable expansion.
493
508
 
494
509
  ```typescript
495
- function guardToolExecute(
496
- schema: NormalizableSchema,
497
- impl: (input: Record<string, unknown>, ...rest: never[]) => Promise<unknown>,
498
- options?: GuardOptions,
499
- ): (input: unknown, ...rest: never[]) => Promise<unknown>
510
+ function shellQuote(s: string): string
500
511
  ```
501
512
 
502
- - `schema` — The tool's declared schema.
503
- - `impl` — The raw handler.
513
+ - `s` — Raw string to wrap for POSIX shell single-quoted context.
504
514
 
505
- **Returns:** A handler with the same signature, guarded.
515
+ **Returns:** A single-quoted shell literal representing `s`.
506
516
 
507
- #### `isEnvFilePath(path)`
517
+ #### `stripControlChars(s)`
508
518
 
509
- Whether a path is an env file, for which {@link redactSecrets}' full env-dump
510
- treatment is appropriate rather than {@link redactSecretsInCode}. Matches
511
- `.env`, `.env.<suffix>`, and `<name>.env`.
519
+ Strip C0 control chars (except tab, newline, CR) that break PostgreSQL JSONB
520
+ and can cause rendering issues.
512
521
 
513
522
  ```typescript
514
- function isEnvFilePath(path: string): boolean
523
+ function stripControlChars(s: string): string
515
524
  ```
516
525
 
517
- - `path` — A workspace-relative or absolute file path.
526
+ - `s` — Arbitrary text that may contain disallowed control characters.
518
527
 
519
- **Returns:** `true` when the file is an env file.
528
+ **Returns:** A copy of `s` with unsafe control characters removed.
520
529
 
521
- #### `isOutsideRoot(path, projectRoot)`
530
+ #### `redactSecrets(s)`
522
531
 
523
- Whether a tool path points outside the project root once normalized (an
524
- absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
525
- clamps such a path to the root; a file tool must refuse it instead of
526
- answering about the root — a `list_files /tmp/x` used to return the
527
- workspace listing as if it were `/tmp/x` (X1 runs sonnet-m17 and sol-m05,
528
- 2026-10-14).
532
+ Redact values of common secret/credential patterns in text output.
533
+
534
+ ENV-DUMP GRADE — includes the JSON `KEY: 'value'` passes, which key off the
535
+ NAME beside the value and therefore cannot tell a credential from ordinary
536
+ code that happens to use a keyword-ish identifier. Use this for `.env` reads
537
+ and command output (where an env dump is the actual threat); use
538
+ {@link redactSecretsInCode} for source-file content.
529
539
 
530
540
  ```typescript
531
- function isOutsideRoot(path: string, projectRoot: string): boolean
541
+ function redactSecrets(s: string): string
532
542
  ```
533
543
 
534
- - `path` — The path the tool was given.
535
- - `projectRoot` — The project root.
544
+ - `s` — Log or command output that may contain `.env`-style secrets.
536
545
 
537
- **Returns:** True when the path leaves the root.
546
+ **Returns:** A redacted copy safe to surface to end users or models.
538
547
 
539
- #### `isValidGlob(pattern)`
548
+ #### `redactSecretsInCode(s)`
540
549
 
541
- Validate that a glob/include pattern is safe (no shell metacharacters that could
542
- inject). Allows alphanumeric, `* ? . _ - /` and the bracket/paren glob chars `[] ()`.
550
+ CODE-SAFE redaction — the env-assignment pass of {@link redactSecrets} WITHOUT
551
+ the JSON `KEY: 'value'` passes.
543
552
 
544
- The brackets/parens matter for real frameworks: Next.js App Router names route
545
- directories `[id]`, `[...slug]`, `(group)`, `[[...optional]]`, so without them the
546
- executor cannot `find_files`/`search_files` its own routes on any Next.js project — a
547
- hard block observed on live imports. They are injection-safe here because every caller
548
- passes the pattern through `shellQuote` before it reaches `find -name`/`grep --include`,
549
- where inside single quotes `[]()` are literal (a subshell `(...)` only starts UNquoted);
550
- to the glob engine `[abc]` is a normal character class. The genuinely dangerous
551
- metacharacters (`; | & $ \` > < \n` space) remain disallowed.
553
+ Those passes match on the NAME next to a quoted value, so over source code they
554
+ replace legitimate content at enormous scale: `forgotPasswordEndpoint:
555
+ '/users/forgot-password'`, `apiKeys: 'API keys'`, and every localized "Show
556
+ password" string all became `'[REDACTED]'`. Because the agent writes back the
557
+ content it reads, that token then lands in the user's project — measured at
558
+ 10,952 of 27,919 flagship template files before this split.
559
+
560
+ No value heuristic can fix that: a legitimate `password = 'TestPass123!'` in a
561
+ test helper is indistinguishable from a real credential by shape. So the
562
+ name-keyed passes simply do not run over code. Credentials in source are still
563
+ caught by the env-assignment pass here, and consumers layer VALUE-SHAPE
564
+ detection (vendor prefixes, PEM blocks, credentials in a URL authority) on top —
565
+ which is what actually catches a secret sitting under an innocuous name.
552
566
 
553
567
  ```typescript
554
- function isValidGlob(pattern: string): boolean
568
+ function redactSecretsInCode(s: string): string
555
569
  ```
556
570
 
557
- - `pattern` — User-supplied glob fragment for search/list operations.
571
+ - `s` — Source-file content or other code-shaped text.
558
572
 
559
- **Returns:** `true` when the pattern contains only allowed characters.
573
+ **Returns:** A redacted copy that preserves ordinary code verbatim.
560
574
 
561
- #### `missingParamError(schema, input, missing)`
575
+ #### `isEnvFilePath(path)`
562
576
 
563
- The error a tool returns when a required parameter is still absent after
564
- aliasing. Model-facing, so it names the right parameter, what was actually
565
- sent, and the shape of a correct call — everything needed to fix it in one
566
- turn instead of concluding the tool is broken.
577
+ Whether a path is an env file, for which {@link redactSecrets}' full env-dump
578
+ treatment is appropriate rather than {@link redactSecretsInCode}. Matches
579
+ `.env`, `.env.<suffix>`, and `<name>.env`.
567
580
 
568
581
  ```typescript
569
- function missingParamError(
570
- schema: NormalizableSchema,
571
- input: Record<string, unknown>,
572
- missing: string[],
573
- ): string
582
+ function isEnvFilePath(path: string): boolean
574
583
  ```
575
584
 
576
- - `schema` — The tool's declared schema.
577
- - `input` — The normalized input (used to list the keys that were sent).
578
- - `missing` — The required parameters still absent.
585
+ - `path` — A workspace-relative or absolute file path.
579
586
 
580
- **Returns:** A single-sentence-per-clause error string.
587
+ **Returns:** `true` when the file is an env file.
581
588
 
582
- #### `normalizeToolInput(schema, raw)`
589
+ #### `declaredBudgetSeconds(command, timeoutParam)`
583
590
 
584
- Move known aliases onto the declared parameter names and report what is still
585
- missing. Unknown keys are left in place — handlers ignore them, and dropping
586
- them would hide a model's intent from anything reading the call log.
591
+ The time budget a COMMAND declares for itself — a `timeout <seconds>` prefix
592
+ anywhere in it, or the tool call's own `timeout` (ms) parameter, whichever is
593
+ larger. Zero when it declares none.
587
594
 
588
595
  ```typescript
589
- function normalizeToolInput(schema: NormalizableSchema, raw: unknown): NormalizedInput
596
+ function declaredBudgetSeconds(command: string, timeoutParam: unknown): number
590
597
  ```
591
598
 
592
- - `schema` — The tool's declared schema.
593
- - `raw` — Whatever the model sent as the tool input.
599
+ - `command` — The shell command.
600
+ - `timeoutParam` — The tool call's `timeout` input, in milliseconds.
594
601
 
595
- **Returns:** The normalized input plus the missing-required and aliased lists.
602
+ **Returns:** The declared budget in seconds, or 0.
596
603
 
597
604
  #### `outputIsWithheldUntilExit(command)`
598
605
 
@@ -629,126 +636,127 @@ function outputRedirectTarget(command: string): string | null
629
636
 
630
637
  **Returns:** The target as written, or null (`/dev/null` and `&1`/`&2` are not files).
631
638
 
632
- #### `parseCheckCommand(path)`
639
+ #### `streamingBackgroundCommand(command)`
633
640
 
634
- The shell command that parses one file and exits non-zero with the error
635
- when it does not parse: `node --check` for JavaScript, `JSON.parse` for
636
- JSON, esbuild's transform (present wherever Vite is) for TypeScript.
641
+ Rewrite a command about to run in the background so its log grows while it
642
+ runs. Two shapes left the log empty until exit, and an empty log is what
643
+ `wait_for_task` reads as "stuck", so working suites were reported stuck and
644
+ killed (X1 run flash-m15 lost 20 of its 60 minutes this way, 2026-10-15):
645
+
646
+ - a trailing `| tail -N`, which holds everything back until the command ends.
647
+ It is dropped: `wait_for_task` already returns the end of the log.
648
+ - `playwright test` with no `--reporter`, which runs the config's reporter —
649
+ often `html`, which prints nothing per test. `--reporter=line` is added
650
+ when the playwright run is the command's last step and nothing is piped or
651
+ redirected from it ( aside); the results are the same, printed as they happen.
637
652
 
638
653
  ```typescript
639
- function parseCheckCommand(path: string): string | null
654
+ function streamingBackgroundCommand(command: string): { command: string; changes: string[] }
640
655
  ```
641
656
 
642
- - `path` — The file's absolute path.
657
+ - `command` — The command as the executor sent it.
643
658
 
644
- **Returns:** The command, or null when the file type has no cheap parser.
659
+ **Returns:** The command to run and one sentence per change made (empty when none).
645
660
 
646
- #### `parseCheckProblem(result)`
661
+ #### `checkBlockedCommand(command)`
647
662
 
648
- What a failed parse check says to the executor, or null when the check
649
- passed or could not run (no parser available is not a syntax error).
663
+ Check if a command is blocked for security reasons. Returns error message or null if allowed.
650
664
 
651
665
  ```typescript
652
- function parseCheckProblem(result: {
653
- stdout: string
654
- stderr: string
655
- exitCode: number
656
- }): string | null
666
+ function checkBlockedCommand(command: string): string | null
657
667
  ```
658
668
 
659
- - `result` — The check command's result.
669
+ - `command` — Shell command string proposed for execution.
660
670
 
661
- **Returns:** The message for the tool result, or null.
671
+ **Returns:** A human-readable block reason, or `null` when the command is allowed.
662
672
 
663
- #### `pathArgError(path, tool)`
673
+ #### `resolvePath(path, projectRoot)`
664
674
 
665
- Validate a file tool's `path` argument is a non-empty string. A weak model
666
- sometimes omits it or passes a non-string, which would otherwise crash
667
- `resolvePath` (`path.replace` on undefined) with the cryptic, unactionable
668
- "Cannot read properties of undefined (reading 'replace')" — wasting executor
669
- turns. Returns an actionable message, or null when the path is usable.
675
+ Normalize a path to be absolute within the project root.
676
+ Empty string and '/' both resolve to projectRoot.
677
+ Rejects paths that escape via traversal or absolute paths outside root.
670
678
 
671
679
  ```typescript
672
- function pathArgError(path: unknown, tool: string): string | null
680
+ function resolvePath(path: string, projectRoot: string): string
673
681
  ```
674
682
 
675
- - `path` — The raw `path` argument from the tool input.
676
- - `tool` — The tool name, for the error message (e.g. 'read_file').
677
-
678
- **Returns:** An actionable error string, or null when `path` is a non-empty string.
683
+ - `path` — Relative or absolute path inside the workspace.
684
+ - `projectRoot` — Absolute filesystem root for the active project.
679
685
 
680
- #### `redactSecrets(s)`
686
+ **Returns:** A normalized absolute path confined to `projectRoot`.
681
687
 
682
- Redact values of common secret/credential patterns in text output.
688
+ #### `isOutsideRoot(path, projectRoot)`
683
689
 
684
- ENV-DUMP GRADE — includes the JSON `KEY: 'value'` passes, which key off the
685
- NAME beside the value and therefore cannot tell a credential from ordinary
686
- code that happens to use a keyword-ish identifier. Use this for `.env` reads
687
- and command output (where an env dump is the actual threat); use
688
- {@link redactSecretsInCode} for source-file content.
690
+ Whether a tool path points outside the project root once normalized (an
691
+ absolute path elsewhere, or a `..` that climbs out). {@link resolvePath}
692
+ clamps such a path to the root; a file tool must refuse it instead of
693
+ answering about the root — a `list_files /tmp/x` used to return the
694
+ workspace listing as if it were `/tmp/x` (X1 runs sonnet-m17 and sol-m05,
695
+ 2026-10-14).
689
696
 
690
697
  ```typescript
691
- function redactSecrets(s: string): string
698
+ function isOutsideRoot(path: string, projectRoot: string): boolean
692
699
  ```
693
700
 
694
- - `s` — Log or command output that may contain `.env`-style secrets.
695
-
696
- **Returns:** A redacted copy safe to surface to end users or models.
701
+ - `path` — The path the tool was given.
702
+ - `projectRoot` — The project root.
697
703
 
698
- #### `redactSecretsInCode(s)`
704
+ **Returns:** True when the path leaves the root.
699
705
 
700
- CODE-SAFE redaction — the env-assignment pass of {@link redactSecrets} WITHOUT
701
- the JSON `KEY: 'value'` passes.
706
+ #### `isValidGlob(pattern)`
702
707
 
703
- Those passes match on the NAME next to a quoted value, so over source code they
704
- replace legitimate content at enormous scale: `forgotPasswordEndpoint:
705
- '/users/forgot-password'`, `apiKeys: 'API keys'`, and every localized "Show
706
- password" string all became `'[REDACTED]'`. Because the agent writes back the
707
- content it reads, that token then lands in the user's project — measured at
708
- 10,952 of 27,919 flagship template files before this split.
708
+ Validate that a glob/include pattern is safe (no shell metacharacters that could
709
+ inject). Allows alphanumeric, `* ? . _ - /` and the bracket/paren glob chars `[] ()`.
709
710
 
710
- No value heuristic can fix that: a legitimate `password = 'TestPass123!'` in a
711
- test helper is indistinguishable from a real credential by shape. So the
712
- name-keyed passes simply do not run over code. Credentials in source are still
713
- caught by the env-assignment pass here, and consumers layer VALUE-SHAPE
714
- detection (vendor prefixes, PEM blocks, credentials in a URL authority) on top —
715
- which is what actually catches a secret sitting under an innocuous name.
711
+ The brackets/parens matter for real frameworks: Next.js App Router names route
712
+ directories `[id]`, `[...slug]`, `(group)`, `[[...optional]]`, so without them the
713
+ executor cannot `find_files`/`search_files` its own routes on any Next.js project — a
714
+ hard block observed on live imports. They are injection-safe here because every caller
715
+ passes the pattern through `shellQuote` before it reaches `find -name`/`grep --include`,
716
+ where inside single quotes `[]()` are literal (a subshell `(...)` only starts UNquoted);
717
+ to the glob engine `[abc]` is a normal character class. The genuinely dangerous
718
+ metacharacters (`; | & $ \` > < \n` space) remain disallowed.
716
719
 
717
720
  ```typescript
718
- function redactSecretsInCode(s: string): string
721
+ function isValidGlob(pattern: string): boolean
719
722
  ```
720
723
 
721
- - `s` — Source-file content or other code-shaped text.
724
+ - `pattern` — User-supplied glob fragment for search/list operations.
722
725
 
723
- **Returns:** A redacted copy that preserves ordinary code verbatim.
726
+ **Returns:** `true` when the pattern contains only allowed characters.
724
727
 
725
- #### `resolvePath(path, projectRoot)`
728
+ #### `pathArgError(path, tool)`
726
729
 
727
- Normalize a path to be absolute within the project root.
728
- Empty string and '/' both resolve to projectRoot.
729
- Rejects paths that escape via traversal or absolute paths outside root.
730
+ Validate a file tool's `path` argument is a non-empty string. A weak model
731
+ sometimes omits it or passes a non-string, which would otherwise crash
732
+ `resolvePath` (`path.replace` on undefined) with the cryptic, unactionable
733
+ "Cannot read properties of undefined (reading 'replace')" — wasting executor
734
+ turns. Returns an actionable message, or null when the path is usable.
730
735
 
731
736
  ```typescript
732
- function resolvePath(path: string, projectRoot: string): string
737
+ function pathArgError(path: unknown, tool: string): string | null
733
738
  ```
734
739
 
735
- - `path` — Relative or absolute path inside the workspace.
736
- - `projectRoot` — Absolute filesystem root for the active project.
740
+ - `path` — The raw `path` argument from the tool input.
741
+ - `tool` — The tool name, for the error message (e.g. 'read_file').
737
742
 
738
- **Returns:** A normalized absolute path confined to `projectRoot`.
743
+ **Returns:** An actionable error string, or null when `path` is a non-empty string.
739
744
 
740
- #### `shellQuote(s)`
745
+ #### `directoryReadHint(message, path)`
741
746
 
742
- Shell-safe quoting using single quotes. Unlike JSON.stringify (double quotes),
743
- single-quoted strings prevent command substitution ($(), backticks) and variable expansion.
747
+ Detect a "read/edit targeted a directory, not a file" failure from a backend
748
+ error message (local fs `EISDIR` or the sandbox's `cat: X: Is a directory`),
749
+ and return an actionable message steering the model to `list_files`. Returns
750
+ null when the error is not a directory error.
744
751
 
745
752
  ```typescript
746
- function shellQuote(s: string): string
753
+ function directoryReadHint(message: string, path: string): string | null
747
754
  ```
748
755
 
749
- - `s` — Raw string to wrap for POSIX shell single-quoted context.
756
+ - `message` — The backend error message.
757
+ - `path` — The resolved path that was targeted.
750
758
 
751
- **Returns:** A single-quoted shell literal representing `s`.
759
+ **Returns:** An actionable directory-error string, or null.
752
760
 
753
761
  #### `sliceLines(content, window)`
754
762
 
@@ -772,41 +780,6 @@ function sliceLines(
772
780
 
773
781
  **Returns:** The window plus its position, or null when neither was given.
774
782
 
775
- #### `streamingBackgroundCommand(command)`
776
-
777
- Rewrite a command about to run in the background so its log grows while it
778
- runs. Two shapes left the log empty until exit, and an empty log is what
779
- `wait_for_task` reads as "stuck", so working suites were reported stuck and
780
- killed (X1 run flash-m15 lost 20 of its 60 minutes this way, 2026-10-15):
781
-
782
- - a trailing `| tail -N`, which holds everything back until the command ends.
783
- It is dropped: `wait_for_task` already returns the end of the log.
784
- - `playwright test` with no `--reporter`, which runs the config's reporter —
785
- often `html`, which prints nothing per test. `--reporter=line` is added
786
- when the playwright run is the command's last step and nothing is piped or
787
- redirected from it ( aside); the results are the same, printed as they happen.
788
-
789
- ```typescript
790
- function streamingBackgroundCommand(command: string): { command: string; changes: string[] }
791
- ```
792
-
793
- - `command` — The command as the executor sent it.
794
-
795
- **Returns:** The command to run and one sentence per change made (empty when none).
796
-
797
- #### `stripControlChars(s)`
798
-
799
- Strip C0 control chars (except tab, newline, CR) that break PostgreSQL JSONB
800
- and can cause rendering issues.
801
-
802
- ```typescript
803
- function stripControlChars(s: string): string
804
- ```
805
-
806
- - `s` — Arbitrary text that may contain disallowed control characters.
807
-
808
- **Returns:** A copy of `s` with unsafe control characters removed.
809
-
810
783
  #### `truncate(s, maxLength)`
811
784
 
812
785
  Truncate a string to a max length with a truncation notice.
@@ -870,57 +843,61 @@ function whitespaceTolerantReplace(
870
843
 
871
844
  **Returns:** The new content if a unique fuzzy run matched, else null.
872
845
 
873
- ### Constants
846
+ #### `parseCheckCommand(path)`
874
847
 
875
- #### `DEFAULT_SEARCH_EXCLUDED_DIRS`
848
+ The shell command that parses one file and exits non-zero with the error
849
+ when it does not parse: `node --check` for JavaScript, `JSON.parse` for
850
+ JSON, esbuild's transform (present wherever Vite is) for TypeScript.
876
851
 
877
- Default directory names `search_files`/`find_files` skip — VS Code's
878
- `search.exclude` + `files.exclude` defaults (node_modules, bower_components,
879
- VCS dirs) plus the platform's vendored/build dirs. Overridable per consumer
880
- via `ToolBuildConfig.searchExcludedDirs` (a per-project, user-editable
881
- setting in molecule.dev — keep the APP-SIDE copy in
882
- `@molecule/app-ide-react`'s search types in sync with this list).
852
+ ```typescript
853
+ function parseCheckCommand(path: string): string | null
854
+ ```
855
+
856
+ - `path` — The file's absolute path.
857
+
858
+ **Returns:** The command, or null when the file type has no cheap parser.
859
+
860
+ #### `parseCheckProblem(result)`
861
+
862
+ What a failed parse check says to the executor, or null when the check
863
+ passed or could not run (no parser available is not a syntax error).
883
864
 
884
865
  ```typescript
885
- const DEFAULT_SEARCH_EXCLUDED_DIRS: readonly [
886
- 'node_modules',
887
- 'bower_components',
888
- '.git',
889
- '.svn',
890
- '.hg',
891
- 'CVS',
892
- 'dist',
893
- '.next',
894
- '.vite',
895
- 'molecule',
896
- ]
866
+ function parseCheckProblem(result: {
867
+ stdout: string
868
+ stderr: string
869
+ exitCode: number
870
+ }): string | null
897
871
  ```
898
872
 
899
- #### `MAX_BATCH_READ_BYTES`
873
+ - `result` — The check command's result.
900
874
 
901
- Total content bytes one batched `read_file` call returns, across all files.
902
- The per-file {@link MAX_READ_SIZE} still applies to each one.
875
+ **Returns:** The message for the tool result, or null.
876
+
877
+ ### Constants
878
+
879
+ #### `TOOL_SCHEMAS`
880
+
881
+ Canonical tool schemas shared by the agent runtime and documentation.
903
882
 
904
883
  ```typescript
905
- const MAX_BATCH_READ_BYTES: number
884
+ const TOOL_SCHEMAS: Record<string, ToolSchema>
906
885
  ```
907
886
 
908
- #### `MAX_BATCH_READ_FILES`
887
+ #### `MAX_READ_SIZE`
909
888
 
910
- Most files one batched `read_file` call returns. The cap exists so a survey
911
- of a large tree cannot swallow the turn's context in one result; the tool
912
- reports where it stopped so the model continues from there.
889
+ Max file size for read_file (5MB).
913
890
 
914
891
  ```typescript
915
- const MAX_BATCH_READ_FILES: 25
892
+ const MAX_READ_SIZE: number
916
893
  ```
917
894
 
918
- #### `MAX_FIND_RESULTS`
895
+ #### `MAX_WRITE_SIZE`
919
896
 
920
- Max find results.
897
+ Max content size for write_file (10MB).
921
898
 
922
899
  ```typescript
923
- const MAX_FIND_RESULTS: 100
900
+ const MAX_WRITE_SIZE: number
924
901
  ```
925
902
 
926
903
  #### `MAX_OUTPUT_SIZE`
@@ -931,6 +908,35 @@ Max command output size (100KB per stream).
931
908
  const MAX_OUTPUT_SIZE: number
932
909
  ```
933
910
 
911
+ #### `MAX_SEARCH_CONTEXT_LINES`
912
+
913
+ Most lines of surrounding context `search_files` will return per match.
914
+ Bounded so a broad pattern with a generous context cannot pull a whole tree
915
+ into one reply.
916
+
917
+ ```typescript
918
+ const MAX_SEARCH_CONTEXT_LINES: 40
919
+ ```
920
+
921
+ #### `MAX_BATCH_READ_FILES`
922
+
923
+ Most files one batched `read_file` call returns. The cap exists so a survey
924
+ of a large tree cannot swallow the turn's context in one result; the tool
925
+ reports where it stopped so the model continues from there.
926
+
927
+ ```typescript
928
+ const MAX_BATCH_READ_FILES: 25
929
+ ```
930
+
931
+ #### `MAX_BATCH_READ_BYTES`
932
+
933
+ Total content bytes one batched `read_file` call returns, across all files.
934
+ The per-file {@link MAX_READ_SIZE} still applies to each one.
935
+
936
+ ```typescript
937
+ const MAX_BATCH_READ_BYTES: number
938
+ ```
939
+
934
940
  #### `MAX_READ_RETURN_CHARS`
935
941
 
936
942
  The most one read_file call returns for ONE file when no window was asked
@@ -948,24 +954,6 @@ needs.
948
954
  const MAX_READ_RETURN_CHARS: number
949
955
  ```
950
956
 
951
- #### `MAX_READ_SIZE`
952
-
953
- Max file size for read_file (5MB).
954
-
955
- ```typescript
956
- const MAX_READ_SIZE: number
957
- ```
958
-
959
- #### `MAX_SEARCH_CONTEXT_LINES`
960
-
961
- Most lines of surrounding context `search_files` will return per match.
962
- Bounded so a broad pattern with a generous context cannot pull a whole tree
963
- into one reply.
964
-
965
- ```typescript
966
- const MAX_SEARCH_CONTEXT_LINES: 40
967
- ```
968
-
969
957
  #### `MAX_SEARCH_RESULTS`
970
958
 
971
959
  Max search results.
@@ -974,28 +962,44 @@ Max search results.
974
962
  const MAX_SEARCH_RESULTS: 50
975
963
  ```
976
964
 
977
- #### `MAX_WRITE_SIZE`
965
+ #### `MAX_FIND_RESULTS`
978
966
 
979
- Max content size for write_file (10MB).
967
+ Max find results.
980
968
 
981
969
  ```typescript
982
- const MAX_WRITE_SIZE: number
970
+ const MAX_FIND_RESULTS: 100
983
971
  ```
984
972
 
985
- #### `PARSE_CHECKABLE`
973
+ #### `DEFAULT_SEARCH_EXCLUDED_DIRS`
986
974
 
987
- Files a parse check can vouch for cheaply right after a write.
975
+ Default directory names `search_files`/`find_files` skip — VS Code's
976
+ `search.exclude` + `files.exclude` defaults (node_modules, bower_components,
977
+ VCS dirs) plus the platform's vendored/build dirs. Overridable per consumer
978
+ via `ToolBuildConfig.searchExcludedDirs` (a per-project, user-editable
979
+ setting in molecule.dev — keep the APP-SIDE copy in
980
+ `@molecule/app-ide-react`'s search types in sync with this list).
988
981
 
989
982
  ```typescript
990
- const PARSE_CHECKABLE: RegExp
983
+ const DEFAULT_SEARCH_EXCLUDED_DIRS: readonly [
984
+ 'node_modules',
985
+ 'bower_components',
986
+ '.git',
987
+ '.svn',
988
+ '.hg',
989
+ 'CVS',
990
+ 'dist',
991
+ '.next',
992
+ '.vite',
993
+ 'molecule',
994
+ ]
991
995
  ```
992
996
 
993
- #### `TOOL_SCHEMAS`
997
+ #### `PARSE_CHECKABLE`
994
998
 
995
- Canonical tool schemas shared by the agent runtime and documentation.
999
+ Files a parse check can vouch for cheaply right after a write.
996
1000
 
997
1001
  ```typescript
998
- const TOOL_SCHEMAS: Record<string, ToolSchema>
1002
+ const PARSE_CHECKABLE: RegExp
999
1003
  ```
1000
1004
 
1001
1005
  ## Injection Notes
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA8B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAwCnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CA2/CxF"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA8B9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAwCnE;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAyhDxF"}
package/dist/tools.js CHANGED
@@ -703,6 +703,29 @@ export function buildTools(backend, config) {
703
703
  error: `old_string not found in ${path}. The file's ACTUAL content near your target (copy the exact text from here):\n${snippet}`,
704
704
  };
705
705
  };
706
+ // An import block the model rebuilt in its own order: the linter's
707
+ // import sort owns that order, so name it and show the real block. X1
708
+ // flash-m42 sent the same block three times in three orders.
709
+ // Only when old_string is import statements and nothing else.
710
+ const imported = /^(?:\s*import\b[^;]*?\bfrom\s+(['"])([^'"]+)\1;?)+\s*$/.exec(oldString)?.[2];
711
+ if (imported) {
712
+ const at = fileLines.findIndex((l) => l.includes(`'${imported}'`) || l.includes(`"${imported}"`));
713
+ if (at >= 0) {
714
+ let start = at;
715
+ while (start > 0 && !/^\s*import\b/.test(fileLines[start]))
716
+ start--;
717
+ const block = fileLines
718
+ .slice(start, at + 1)
719
+ .map((l, i) => `${start + i + 1}: ${l}`)
720
+ .join('\n');
721
+ return {
722
+ error: `old_string not found in ${path}: it is an import block, and the linter's import ` +
723
+ `sort decides its order, so do not rebuild it. The file's import from ` +
724
+ `'${imported}' reads:\n${block}\nEdit one line of it, or add your import as a ` +
725
+ 'new line and let the linter sort it.',
726
+ };
727
+ }
728
+ }
706
729
  const distinctiveLines = [
707
730
  ...new Set(oldString
708
731
  .split('\n')
@@ -1039,8 +1062,11 @@ export function buildTools(backend, config) {
1039
1062
  // behind `| tail` it returns nothing (X1 flash-m29 lost a 290 s run that way).
1040
1063
  // A short one still comes back as a foreground result, through the grace below.
1041
1064
  const overCeiling = commandBudgetMs !== undefined &&
1042
- outputIsWithheldUntilExit(command) &&
1043
- (declaredSeconds > ceilingSeconds || /\bplaywright\s+test\b/.test(command));
1065
+ ((outputIsWithheldUntilExit(command) &&
1066
+ (declaredSeconds > ceilingSeconds || /\bplaywright\s+test\b/.test(command))) ||
1067
+ // A redirected Playwright run shows nothing either (`> out.txt`), and was killed at
1068
+ // the ceiling with 38 tests unrun (X1 flash-m36).
1069
+ (/\bplaywright\s+test\b/.test(command) && outputRedirectTarget(command) !== null));
1044
1070
  if (input.run_in_background === true || overCeiling) {
1045
1071
  const id = `mol-bg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1046
1072
  const log = `/tmp/${id}.log`;
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"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"}
package/dist/utilities.js CHANGED
@@ -70,8 +70,10 @@ const SECRET_ENV_ASSIGNMENT = new RegExp(
70
70
  const SECRET_JSON_DQ = new RegExp(
71
71
  // A value ends at its closing quote OR the end of its line: cut off mid-value (`head -c`),
72
72
  // the old pattern ran on to a quote on a LATER line and swallowed the lines between (flash-m32).
73
- `(['"]?(?:\\w*(?:${SECRET_KEYWORDS})\\w*)['"]?\\s*[:=]\\s*)"(?:[^"\\\\\\n]|\\\\.)*(?:"|(?=\\n)|$)`, 'gi');
74
- const SECRET_JSON_SQ = new RegExp(`(['"]?(?:\\w*(?:${SECRET_KEYWORDS})\\w*)['"]?\\s*[:=]\\s*)'(?:[^'\\\\\\n]|\\\\.)*(?:'|(?=\\n)|$)`, 'gi');
73
+ // The key starts at a real boundary: a package name ending in a keyword ("@molecule/app-auth": "1.2.0")
74
+ // is not a secret, and matching from its tail masked the version (flash-m35).
75
+ `((?<![\\w@/.-])['"]?(?:\\w*(?:${SECRET_KEYWORDS})\\w*)['"]?\\s*[:=]\\s*)"(?:[^"\\\\\\n]|\\\\.)*(?:"|(?=\\n)|$)`, 'gi');
76
+ const SECRET_JSON_SQ = new RegExp(`((?<![\\w@/.-])['"]?(?:\\w*(?:${SECRET_KEYWORDS})\\w*)['"]?\\s*[:=]\\s*)'(?:[^'\\\\\\n]|\\\\.)*(?:'|(?=\\n)|$)`, 'gi');
75
77
  /**
76
78
  * Exact VALUES that are UI vocabulary, never credentials — kept verbatim even when they
77
79
  * sit next to a keyword-looking name. Frontend auth code is full of these: a JSX ternary
@@ -239,6 +241,13 @@ export function streamingBackgroundCommand(command) {
239
241
  changes.push(`Dropped the trailing \`${tail[0].trim()}\`: it holds all output back until the command ends, ` +
240
242
  'and wait_for_task returns the end of the log anyway.');
241
243
  }
244
+ // A Playwright run behind `| head -N` loses its tally, which prints last (X1
245
+ // flash-m32 counted ✓/✘ marks by hand); the full log comes back anyway.
246
+ const head = /\s*\|\s*head(?:\s+-n)?\s+-?\d+\s*$/.exec(out);
247
+ if (head && /\bplaywright\s+test\b/.test(out)) {
248
+ out = out.slice(0, head.index);
249
+ changes.push(`Dropped the trailing \`${head[0].trim()}\`: Playwright prints its pass/fail count last, and it would be cut.`);
250
+ }
242
251
  const lastStep = out.split(/&&|\|\||;/).at(-1) ?? '';
243
252
  if (/\bplaywright\s+test\b/.test(lastStep) &&
244
253
  !/--reporter\b/.test(lastStep) &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.5.8",
3
+ "version": "1.5.10",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",