@molecule/api-ai-tools 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,6 +58,48 @@ export declare function durationSeconds(word: string): number | null;
58
58
  * @returns One entry per statement or pipeline stage that runs something, in order.
59
59
  */
60
60
  export declare function commandSegments(command: string): CommandSegment[];
61
+ /**
62
+ * One statement of a shell command, with where it sits and the variables it sets for itself.
63
+ *
64
+ * `commandSegments` answers WHAT runs; this answers where, and what the shell binds to it — the
65
+ * two things a caller that WRITES into a command needs. The platform composes a run from the
66
+ * executor's own command in more than one place, and a value written for the runner belongs in
67
+ * the runner's own statement: `APP_BASE=https://… cd app && npx playwright test` sets the variable
68
+ * for `cd` alone, so the Playwright config falls back to the local app while the command line
69
+ * still names the deployed host (xr-molecule-flash-m103 +28:08).
70
+ */
71
+ export interface LocatedCommandSegment {
72
+ /**
73
+ * What the statement runs, or null when it only sets variables (`BASE=http://…` on its own):
74
+ * that shape persists in the shell, so a caller following assignments across `;`/`&&` has to
75
+ * see it.
76
+ */
77
+ segment: CommandSegment | null;
78
+ /** The statement's own leading `NAME=value` words, as written (`TEST_MODE=fullstack`). */
79
+ assignments: readonly string[];
80
+ /**
81
+ * The offset in the command of the statement's first word, or null for a segment inside a
82
+ * nested `sh -c '<command>'` string — its positions are offsets in THAT string, not in this
83
+ * command, so nothing may be spliced at them.
84
+ */
85
+ start: number | null;
86
+ /**
87
+ * The offset in the command just before the word the statement runs — after its own leading
88
+ * `NAME=value` words and their whitespace. A `NAME=value` written here is in the environment of
89
+ * the program the statement runs (`… TEST_MODE=fullstack <here>npx playwright test`). Null
90
+ * inside a nested `sh -c` string, for the same reason as {@link start}.
91
+ */
92
+ assignmentOffset: number | null;
93
+ }
94
+ /**
95
+ * Every statement of a command, with its own assignments and where it sits — assignment-only
96
+ * statements included, because the variables those persist are exactly what a later statement of
97
+ * the same command reads.
98
+ *
99
+ * @param command - The shell command.
100
+ * @returns One entry per statement or pipeline stage, in order.
101
+ */
102
+ export declare function locatedCommandSegments(command: string): LocatedCommandSegment[];
61
103
  /**
62
104
  * The bound a command puts on its own run: the largest `timeout <N>` any of its segments runs
63
105
  * under (`timeout 280 hurl --test …` → 280), or null when it carries none.
@@ -1 +1 @@
1
- {"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AA2MD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAuSD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAEjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAoCD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
1
+ {"version":3,"file":"command-position.d.ts","sourceRoot":"","sources":["../src/command-position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,qFAAqF;IACrF,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AA2MD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAkVD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,EAAE,CAIjE;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,cAAc,GAAG,IAAI,CAAA;IAC9B,0FAA0F;IAC1F,WAAW,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B;;;;OAIG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,EAAE,CAE/E;AAqDD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAK/E"}
@@ -400,11 +400,14 @@ function commandStart(words) {
400
400
  * no runner, and every run of it reads as nothing).
401
401
  *
402
402
  * @param command - The shell command.
403
- * @returns Each segment's text, in order, with empty ones dropped.
403
+ * @returns Each segment's text and the offset in the command its first character sits at, in
404
+ * order, with empty ones dropped.
404
405
  */
405
406
  function splitSegments(command) {
406
407
  const parts = [];
407
408
  let buffer = '';
409
+ /** The offset in the command of the current buffer's first character. */
410
+ let bufferStart = 0;
408
411
  /** The nestings this position sits inside, outermost first. */
409
412
  const stack = [];
410
413
  for (let i = 0; i < command.length; i++) {
@@ -466,25 +469,33 @@ function splitSegments(command) {
466
469
  continue;
467
470
  }
468
471
  if ((char === '&' && next === '&') || (char === '|' && next === '|')) {
469
- parts.push(buffer);
472
+ parts.push({ text: buffer, start: bufferStart });
470
473
  buffer = '';
471
474
  i += 1;
475
+ bufferStart = i + 1;
472
476
  continue;
473
477
  }
474
478
  if (char === ';' || char === '|' || char === '\n') {
475
- parts.push(buffer);
479
+ parts.push({ text: buffer, start: bufferStart });
476
480
  buffer = '';
481
+ bufferStart = i + 1;
477
482
  continue;
478
483
  }
479
484
  if (char === '&' && command[i - 1] !== '>' && next !== '>') {
480
- parts.push(buffer);
485
+ parts.push({ text: buffer, start: bufferStart });
481
486
  buffer = '';
487
+ bufferStart = i + 1;
482
488
  continue;
483
489
  }
484
490
  buffer += char;
485
491
  }
486
- parts.push(buffer);
487
- return parts.map((part) => part.trim()).filter(Boolean);
492
+ parts.push({ text: buffer, start: bufferStart });
493
+ return parts
494
+ .map((part) => ({
495
+ text: part.text.trim(),
496
+ start: part.start + (part.text.length - part.text.trimStart().length),
497
+ }))
498
+ .filter((part) => part.text !== '');
488
499
  }
489
500
  /**
490
501
  * One segment's words: quotes kept whole, grouping parentheses dropped, and a `(` written
@@ -506,6 +517,38 @@ function segmentWords(text) {
506
517
  }
507
518
  return cleaned;
508
519
  }
520
+ /** {@link WORD} anchored at an offset, for walking a statement's raw words. */
521
+ const WORD_AT = new RegExp(WORD.source, 'y');
522
+ /**
523
+ * A statement's own leading `NAME=value` words, and where they end.
524
+ *
525
+ * The shell binds these to the command the statement runs, so they are the environment THAT
526
+ * program sees — which is why a caller writing a variable for a run has to write it here and not
527
+ * at the start of the whole command.
528
+ *
529
+ * @param text - One statement's text.
530
+ * @returns The assignments as written, and the offset in `text` just before the word that runs.
531
+ */
532
+ function leadingAssignments(text) {
533
+ const words = [];
534
+ let at = 0;
535
+ for (let guard = 0; guard < MAX_WRAPPERS; guard++) {
536
+ while (at < text.length && /\s/.test(text[at] ?? ''))
537
+ at += 1;
538
+ // Grouping in front of the statement (`(npx …)`, `{ …`): not a word the shell assigns with.
539
+ if (text[at] === '(' || text[at] === '{') {
540
+ at += 1;
541
+ continue;
542
+ }
543
+ WORD_AT.lastIndex = at;
544
+ const match = WORD_AT.exec(text);
545
+ if (!match || !ENV_ASSIGNMENT.test(match[0]))
546
+ return { words, end: at };
547
+ words.push(match[0]);
548
+ at += match[0].length;
549
+ }
550
+ return { words, end: at };
551
+ }
509
552
  /**
510
553
  * The segments of a command, each with its program and arguments — the answer every classifier
511
554
  * that reads a command as "this runs the suite" is asking for.
@@ -514,66 +557,92 @@ function segmentWords(text) {
514
557
  * @returns One entry per statement or pipeline stage that runs something, in order.
515
558
  */
516
559
  export function commandSegments(command) {
517
- return segmentsOf(command, 0);
560
+ return locatedSegmentsOf(command, 0).flatMap((located) => located.segment === null ? [] : [located.segment]);
518
561
  }
519
562
  /**
520
- * The bound a command puts on its own run: the largest `timeout <N>` any of its segments runs
521
- * under (`timeout 280 hurl --test …` → 280), or null when it carries none.
522
- *
523
- * This is the bound `man timeout` enforces by KILLING the command — so a run that used it did not
524
- * finish, whatever the pipeline's own exit status says. It is read here, from the same parse every
525
- * other command reader uses, rather than matched as text: `timeout` in an argument
526
- * (`grep -n 'timeout 280' notes.md`) bounds nothing, and `-k 5` is the grace before the kill, not
527
- * the bound ({@link WRAPPER_VALUE_FLAGS}).
563
+ * Every statement of a command, with its own assignments and where it sits — assignment-only
564
+ * statements included, because the variables those persist are exactly what a later statement of
565
+ * the same command reads.
528
566
  *
529
567
  * @param command - The shell command.
530
- * @returns The seconds the command allows itself, or null.
568
+ * @returns One entry per statement or pipeline stage, in order.
531
569
  */
532
- export function commandTimeoutSeconds(command) {
533
- let bound = null;
534
- for (const segment of segmentsOf(command, 0)) {
535
- if (segment.timeoutSeconds === null)
536
- continue;
537
- bound = bound === null ? segment.timeoutSeconds : Math.max(bound, segment.timeoutSeconds);
538
- }
539
- return bound;
570
+ export function locatedCommandSegments(command) {
571
+ return locatedSegmentsOf(command, 0);
540
572
  }
541
573
  /**
542
- * {@link commandSegments}'s recursion, so a `sh -c '<command>'` is opened as far as
543
- * {@link MAX_NESTING} allows.
574
+ * {@link locatedCommandSegments}'s recursion, so a `sh -c '<command>'` is opened as far as
575
+ * {@link MAX_NESTING} allows. A nested statement's offsets are cleared: they are positions in
576
+ * the nested string, not in the command this walk started from.
544
577
  *
545
578
  * @param command - The shell command.
546
579
  * @param depth - How many `sh -c` strings this command already sits inside.
547
- * @returns The segments.
580
+ * @returns The located statements.
548
581
  */
549
- function segmentsOf(command, depth) {
582
+ function locatedSegmentsOf(command, depth) {
550
583
  if (typeof command !== 'string' || depth > MAX_NESTING)
551
584
  return [];
552
585
  const out = [];
553
- for (const text of splitSegments(command)) {
554
- const words = segmentWords(text);
555
- if (words.length === 0)
556
- continue;
586
+ for (const part of splitSegments(command)) {
587
+ const words = segmentWords(part.text);
557
588
  const start = commandStart(words);
589
+ const leading = leadingAssignments(part.text);
558
590
  if (start.nested !== null) {
559
- out.push(...segmentsOf(start.nested, depth + 1));
591
+ for (const inner of locatedSegmentsOf(start.nested, depth + 1))
592
+ out.push({ ...inner, start: null, assignmentOffset: null });
560
593
  continue;
561
594
  }
562
595
  const word = words[start.at];
563
- // Only wrappers and assignments: nothing this segment runs.
564
- if (word === undefined)
596
+ if (word === undefined) {
597
+ // Only wrappers and assignments: nothing this statement runs, but the variables it sets
598
+ // persist in the shell the rest of the command runs in.
599
+ if (leading.words.length > 0)
600
+ out.push({
601
+ segment: null,
602
+ assignments: leading.words,
603
+ start: part.start,
604
+ assignmentOffset: part.start + leading.end,
605
+ });
565
606
  continue;
607
+ }
566
608
  const program = programName(word);
567
609
  const args = words.slice(start.at + 1);
568
610
  out.push({
569
- program,
570
- args,
571
- text: [program, ...args].join(' '),
572
- timeoutSeconds: start.timeoutSeconds,
611
+ segment: {
612
+ program,
613
+ args,
614
+ text: [program, ...args].join(' '),
615
+ timeoutSeconds: start.timeoutSeconds,
616
+ },
617
+ assignments: leading.words,
618
+ start: part.start,
619
+ assignmentOffset: part.start + leading.end,
573
620
  });
574
621
  }
575
622
  return out;
576
623
  }
624
+ /**
625
+ * The bound a command puts on its own run: the largest `timeout <N>` any of its segments runs
626
+ * under (`timeout 280 hurl --test …` → 280), or null when it carries none.
627
+ *
628
+ * This is the bound `man timeout` enforces by KILLING the command — so a run that used it did not
629
+ * finish, whatever the pipeline's own exit status says. It is read here, from the same parse every
630
+ * other command reader uses, rather than matched as text: `timeout` in an argument
631
+ * (`grep -n 'timeout 280' notes.md`) bounds nothing, and `-k 5` is the grace before the kill, not
632
+ * the bound ({@link WRAPPER_VALUE_FLAGS}).
633
+ *
634
+ * @param command - The shell command.
635
+ * @returns The seconds the command allows itself, or null.
636
+ */
637
+ export function commandTimeoutSeconds(command) {
638
+ let bound = null;
639
+ for (const segment of commandSegments(command)) {
640
+ if (segment.timeoutSeconds === null)
641
+ continue;
642
+ bound = bound === null ? segment.timeoutSeconds : Math.max(bound, segment.timeoutSeconds);
643
+ }
644
+ return bound;
645
+ }
577
646
  /**
578
647
  * Whether any segment of the command runs a program whose command-position text matches
579
648
  * `pattern` — the one question a runner-kind table asks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
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",