explorbot 0.4.9 → 0.5.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.
Files changed (144) hide show
  1. package/README.md +4 -1
  2. package/bin/mdq.ts +18 -0
  3. package/boat/api-tester/src/ai/chief.ts +72 -0
  4. package/boat/api-tester/src/api-client.ts +37 -0
  5. package/boat/prima/src/prima.ts +41 -2
  6. package/dist/bin/mdq.js +19 -0
  7. package/dist/boat/api-tester/src/ai/chief.js +69 -0
  8. package/dist/boat/api-tester/src/api-client.js +26 -0
  9. package/dist/boat/prima/src/prima.js +42 -2
  10. package/dist/package.json +3 -2
  11. package/dist/src/action.js +4 -2
  12. package/dist/src/ai/agent.d.ts +3 -1
  13. package/dist/src/ai/judge-provider.d.ts +17 -0
  14. package/dist/src/ai/judge-provider.js +56 -0
  15. package/dist/src/ai/judge-tool.d.ts +2 -0
  16. package/dist/src/ai/judge-tool.js +33 -0
  17. package/dist/src/ai/judge.d.ts +28 -0
  18. package/dist/src/ai/judge.js +71 -0
  19. package/dist/src/ai/navigator.d.ts +3 -1
  20. package/dist/src/ai/navigator.js +24 -28
  21. package/dist/src/ai/pilot.d.ts +4 -0
  22. package/dist/src/ai/pilot.js +67 -9
  23. package/dist/src/ai/planner.js +9 -6
  24. package/dist/src/ai/provider.d.ts +4 -1
  25. package/dist/src/ai/provider.js +52 -7
  26. package/dist/src/ai/rerunner.js +7 -0
  27. package/dist/src/ai/researcher/deep-analysis.js +2 -2
  28. package/dist/src/ai/researcher/locators.js +2 -2
  29. package/dist/src/ai/researcher/pagination.js +1 -1
  30. package/dist/src/ai/researcher/research-result.js +2 -2
  31. package/dist/src/ai/researcher.js +1 -1
  32. package/dist/src/ai/task-agent.d.ts +2 -0
  33. package/dist/src/ai/task-agent.js +3 -1
  34. package/dist/src/ai/tester.js +19 -15
  35. package/dist/src/ai/tools.d.ts +4 -3
  36. package/dist/src/ai/tools.js +35 -7
  37. package/dist/src/api/request-result.js +2 -1
  38. package/dist/src/api/xhr-capture.js +2 -1
  39. package/dist/src/command-handler.d.ts +1 -0
  40. package/dist/src/command-handler.js +24 -3
  41. package/dist/src/commands/base-command.d.ts +5 -0
  42. package/dist/src/commands/base-command.js +3 -0
  43. package/dist/src/commands/explore-command.d.ts +2 -1
  44. package/dist/src/commands/explore-command.js +12 -1
  45. package/dist/src/commands/freesail-command.js +8 -2
  46. package/dist/src/commands/init-command.js +1 -1
  47. package/dist/src/commands/navigate-command.d.ts +2 -1
  48. package/dist/src/commands/navigate-command.js +6 -0
  49. package/dist/src/commands/plan-load-command.d.ts +2 -1
  50. package/dist/src/commands/plan-load-command.js +4 -0
  51. package/dist/src/commands/plans-command.d.ts +3 -9
  52. package/dist/src/commands/plans-command.js +11 -21
  53. package/dist/src/commands/rerun-command.d.ts +2 -1
  54. package/dist/src/commands/rerun-command.js +5 -1
  55. package/dist/src/commands/research-command.d.ts +2 -1
  56. package/dist/src/commands/research-command.js +6 -0
  57. package/dist/src/commands/test-command.d.ts +2 -1
  58. package/dist/src/commands/test-command.js +4 -1
  59. package/dist/src/components/Autocomplete.js +26 -12
  60. package/dist/src/components/InputReadline.js +10 -1
  61. package/dist/src/config.d.ts +6 -0
  62. package/dist/src/experience-tracker.js +4 -3
  63. package/dist/src/explorbot.d.ts +3 -0
  64. package/dist/src/explorbot.js +8 -0
  65. package/dist/src/explorer.js +2 -3
  66. package/dist/src/knowledge-tracker.js +1 -1
  67. package/dist/src/reporter.js +8 -4
  68. package/dist/src/state-manager.d.ts +2 -0
  69. package/dist/src/state-manager.js +16 -0
  70. package/dist/src/test-plan.d.ts +11 -0
  71. package/dist/src/test-plan.js +54 -2
  72. package/dist/src/utils/aria-ref.js +1 -1
  73. package/dist/src/utils/logger.js +9 -3
  74. package/dist/src/utils/markdown-query.d.ts +1 -48
  75. package/dist/src/utils/markdown-query.js +1 -444
  76. package/dist/src/utils/mdq/cli.d.ts +6 -0
  77. package/dist/src/utils/mdq/cli.js +122 -0
  78. package/dist/src/utils/mdq/edit.d.ts +24 -0
  79. package/dist/src/utils/mdq/edit.js +147 -0
  80. package/dist/src/utils/mdq/query.d.ts +118 -0
  81. package/dist/src/utils/mdq/query.js +451 -0
  82. package/dist/src/utils/step-analyzer.d.ts +3 -0
  83. package/dist/src/utils/step-analyzer.js +7 -0
  84. package/dist/src/utils/strings.d.ts +1 -0
  85. package/dist/src/utils/strings.js +7 -0
  86. package/dist/src/utils/test-files.d.ts +1 -0
  87. package/dist/src/utils/test-files.js +5 -2
  88. package/dist/src/utils/url-matcher.d.ts +1 -0
  89. package/dist/src/utils/url-matcher.js +7 -0
  90. package/docs/api-testing/planning.md +1 -1
  91. package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
  92. package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
  93. package/package.json +3 -2
  94. package/src/action.ts +4 -2
  95. package/src/ai/agent.ts +3 -1
  96. package/src/ai/judge-provider.ts +62 -0
  97. package/src/ai/judge-tool.ts +35 -0
  98. package/src/ai/judge.ts +75 -0
  99. package/src/ai/navigator.ts +24 -27
  100. package/src/ai/pilot.ts +68 -9
  101. package/src/ai/planner.ts +9 -6
  102. package/src/ai/provider.ts +51 -7
  103. package/src/ai/rerunner.ts +4 -0
  104. package/src/ai/researcher/deep-analysis.ts +2 -2
  105. package/src/ai/researcher/locators.ts +2 -2
  106. package/src/ai/researcher/pagination.ts +1 -1
  107. package/src/ai/researcher/research-result.ts +2 -2
  108. package/src/ai/researcher.ts +1 -1
  109. package/src/ai/task-agent.ts +4 -1
  110. package/src/ai/tester.ts +19 -16
  111. package/src/ai/tools.ts +42 -7
  112. package/src/api/request-result.ts +2 -1
  113. package/src/api/xhr-capture.ts +2 -1
  114. package/src/command-handler.ts +28 -3
  115. package/src/commands/base-command.ts +9 -0
  116. package/src/commands/explore-command.ts +15 -2
  117. package/src/commands/freesail-command.ts +9 -2
  118. package/src/commands/init-command.ts +1 -1
  119. package/src/commands/navigate-command.ts +8 -1
  120. package/src/commands/plan-load-command.ts +6 -1
  121. package/src/commands/plans-command.ts +13 -29
  122. package/src/commands/rerun-command.ts +7 -2
  123. package/src/commands/research-command.ts +8 -1
  124. package/src/commands/test-command.ts +6 -2
  125. package/src/components/Autocomplete.tsx +39 -10
  126. package/src/components/InputReadline.tsx +10 -1
  127. package/src/config.ts +1 -0
  128. package/src/experience-tracker.ts +4 -3
  129. package/src/explorbot.ts +8 -0
  130. package/src/explorer.ts +2 -2
  131. package/src/knowledge-tracker.ts +1 -1
  132. package/src/reporter.ts +8 -4
  133. package/src/state-manager.ts +16 -0
  134. package/src/test-plan.ts +67 -2
  135. package/src/utils/aria-ref.ts +1 -1
  136. package/src/utils/logger.ts +7 -2
  137. package/src/utils/markdown-query.ts +1 -519
  138. package/src/utils/mdq/cli.ts +118 -0
  139. package/src/utils/mdq/edit.ts +158 -0
  140. package/src/utils/mdq/query.ts +556 -0
  141. package/src/utils/step-analyzer.ts +8 -0
  142. package/src/utils/strings.ts +7 -0
  143. package/src/utils/test-files.ts +5 -2
  144. package/src/utils/url-matcher.ts +7 -0
@@ -0,0 +1,451 @@
1
+ import { marked } from 'marked';
2
+ import { MarkdownEditor } from "./edit.js";
3
+ export { MarkdownEditor };
4
+ export class MarkdownQuery {
5
+ source;
6
+ matches;
7
+ constructor(source, matches) {
8
+ this.source = source;
9
+ this.matches = matches || buildTokenIndex(source);
10
+ }
11
+ query(selector, matcher) {
12
+ const segments = parseQuery(selector);
13
+ if (matcher !== undefined && segments.length > 0)
14
+ segments[segments.length - 1].textMatch = this.valueMatcher(matcher);
15
+ return new MarkdownQuery(this.source, this.run(this.expandSections(this.matches), segments));
16
+ }
17
+ section(matcher, options) {
18
+ return this.query(`section${options?.depth || ''}`, matcher);
19
+ }
20
+ heading(matcher, options) {
21
+ if (options?.depth)
22
+ return this.query(`h${options.depth}`, matcher);
23
+ return this.query('heading', matcher);
24
+ }
25
+ paragraph(matcher) {
26
+ return this.query('paragraph', matcher);
27
+ }
28
+ table(matcher) {
29
+ return this.query('table', matcher);
30
+ }
31
+ list(matcher) {
32
+ return this.query('list', matcher);
33
+ }
34
+ item(matcher) {
35
+ return this.query('item', matcher);
36
+ }
37
+ code(matcher) {
38
+ return this.query('code', matcher);
39
+ }
40
+ blockquote(matcher) {
41
+ return this.query('blockquote', matcher);
42
+ }
43
+ comment(matcher) {
44
+ return this.query('comment', matcher);
45
+ }
46
+ html(matcher) {
47
+ return this.query('html', matcher);
48
+ }
49
+ hr() {
50
+ return this.query('hr');
51
+ }
52
+ text() {
53
+ return this.matches.map((range) => this.source.slice(range.start, range.start + range.length)).join('');
54
+ }
55
+ nodes() {
56
+ return this.matches.map((range) => {
57
+ const token = range.token;
58
+ if (token.type !== 'heading')
59
+ return { type: token.type, depth: null, text: this.tokenText(range.token) };
60
+ return { type: token.type, depth: token.depth, text: this.tokenText(range.token) };
61
+ });
62
+ }
63
+ rows() {
64
+ const results = [];
65
+ for (const range of this.matches) {
66
+ if (range.token.type !== 'table')
67
+ continue;
68
+ const table = range.token;
69
+ const headers = table.header.map((cell) => cell.text);
70
+ for (const row of table.rows) {
71
+ const entry = {};
72
+ for (let i = 0; i < headers.length; i++) {
73
+ entry[headers[i]] = row[i]?.text ?? '';
74
+ }
75
+ results.push(entry);
76
+ }
77
+ }
78
+ return results;
79
+ }
80
+ entries() {
81
+ const entries = {};
82
+ for (const range of this.matches) {
83
+ for (const line of this.tokenText(range.token).split('\n')) {
84
+ const separator = line.indexOf(':');
85
+ if (separator < 1)
86
+ continue;
87
+ const value = line.slice(separator + 1).trim();
88
+ if (value)
89
+ entries[line.slice(0, separator).trim().toLowerCase()] = value;
90
+ }
91
+ }
92
+ return entries;
93
+ }
94
+ frontmatter() {
95
+ return this.editor().frontmatter();
96
+ }
97
+ count() {
98
+ return this.matches.length;
99
+ }
100
+ exists() {
101
+ return this.matches.length > 0;
102
+ }
103
+ at(index) {
104
+ let resolved = index;
105
+ if (resolved < 0)
106
+ resolved = this.matches.length + resolved;
107
+ if (resolved < 0 || resolved >= this.matches.length)
108
+ return new MarkdownQuery(this.source, []);
109
+ return new MarkdownQuery(this.source, [this.matches[resolved]]);
110
+ }
111
+ slice(from, to) {
112
+ return new MarkdownQuery(this.source, this.matches.slice(from, to));
113
+ }
114
+ first() {
115
+ return this.slice(0, 1);
116
+ }
117
+ last() {
118
+ return this.slice(-1);
119
+ }
120
+ each() {
121
+ return this.matches.map((range) => new MarkdownQuery(this.source, [range]));
122
+ }
123
+ preceding() {
124
+ if (this.matches.length === 0)
125
+ return new MarkdownQuery(this.source, []);
126
+ const cutoff = this.matches[0].start;
127
+ return new MarkdownQuery(this.source, buildTokenIndex(this.source).filter((range) => range.start + range.length <= cutoff));
128
+ }
129
+ following() {
130
+ if (this.matches.length === 0)
131
+ return new MarkdownQuery(this.source, []);
132
+ const last = this.matches[this.matches.length - 1];
133
+ const cutoff = last.start + last.length;
134
+ return new MarkdownQuery(this.source, buildTokenIndex(this.source).filter((range) => range.start >= cutoff));
135
+ }
136
+ replace(content) {
137
+ return this.replaceEach(() => content);
138
+ }
139
+ replaceEach(replacer) {
140
+ return new MarkdownQuery(this.editor().replace(this.matches, (range, index) => String(replacer(new MarkdownQuery(this.source, [range]), index))));
141
+ }
142
+ remove() {
143
+ return new MarkdownQuery(this.editor().remove(this.matches));
144
+ }
145
+ insertBefore(markdown) {
146
+ return this.insertAll((range) => range.start, markdown);
147
+ }
148
+ insertAfter(markdown) {
149
+ return this.insertAll((range) => MarkdownEditor.blockEnd(range), markdown);
150
+ }
151
+ prepend(markdown) {
152
+ return this.insertAll((range) => this.containerStart(range), markdown);
153
+ }
154
+ append(markdown) {
155
+ return this.insertAll((range) => this.containerEnd(range), markdown);
156
+ }
157
+ addRow(row) {
158
+ return new MarkdownQuery(this.editor().replace(this.matches, (range) => {
159
+ if (range.token.type !== 'table')
160
+ throw new MdqOperationError(`addRow needs a table, got ${range.token.type}`);
161
+ const table = range.token;
162
+ const headers = table.header.map((cell) => cell.text);
163
+ const existing = table.rows.map((cells) => headers.map((_, index) => cells[index]?.text || ''));
164
+ return MarkdownEditor.table(headers, [...existing, headers.map((header) => row[header] || '')], table.align);
165
+ }));
166
+ }
167
+ addItem(text) {
168
+ return new MarkdownQuery(this.editor().replace(this.matches, (range) => {
169
+ if (range.token.type !== 'list')
170
+ throw new MdqOperationError(`addItem needs a list, got ${range.token.type}`);
171
+ const raw = (range.token.raw || '').trimEnd();
172
+ return `${raw}\n${MarkdownEditor.item(raw, text)}\n`;
173
+ }));
174
+ }
175
+ setEntry(key, value) {
176
+ return this.replaceEach((match) => {
177
+ const token = match.matches[0].token;
178
+ return MarkdownEditor.entries(this.tokenText(token), key, value, token.type === 'blockquote');
179
+ });
180
+ }
181
+ setFrontmatter(key, value) {
182
+ return new MarkdownQuery(this.editor().setFrontmatter(key, value));
183
+ }
184
+ toString() {
185
+ return this.source;
186
+ }
187
+ valueOf() {
188
+ return this.source;
189
+ }
190
+ /** Compatibility aliases. Each delegates to the canonical name above; prefer those. */
191
+ get() {
192
+ return this.text();
193
+ }
194
+ meta() {
195
+ return this.nodes();
196
+ }
197
+ toJson() {
198
+ return this.rows();
199
+ }
200
+ keyValue() {
201
+ return this.entries();
202
+ }
203
+ setKeyValue(key, value) {
204
+ return this.setEntry(key, value);
205
+ }
206
+ before() {
207
+ return this.preceding();
208
+ }
209
+ after() {
210
+ return this.following();
211
+ }
212
+ editor() {
213
+ return new MarkdownEditor(this.source);
214
+ }
215
+ run(candidates, segments) {
216
+ if (segments.length === 0)
217
+ return candidates;
218
+ const segment = segments[0];
219
+ const remaining = segments.slice(1);
220
+ if (/^section[1-6]?$/.test(segment.selector)) {
221
+ const sections = this.narrow(this.sectionsOf(candidates, segment), segment);
222
+ if (remaining.length === 0)
223
+ return sections;
224
+ const results = [];
225
+ for (const section of sections)
226
+ results.push(...this.run(section.innerTokens || [], remaining));
227
+ return results;
228
+ }
229
+ if (segment.selector === 'comment') {
230
+ const comments = candidates.filter((range) => this.isComment(range.token));
231
+ return this.run(this.narrow(this.filterByText(comments, segment), segment), remaining);
232
+ }
233
+ if (segment.selector === 'item') {
234
+ return this.run(this.narrow(this.filterByText(this.itemsOf(candidates), segment), segment), remaining);
235
+ }
236
+ const depth = segment.selector.match(/^h([1-6])$/);
237
+ let type = TOKEN_ALIASES[segment.selector] || segment.selector;
238
+ if (depth)
239
+ type = 'heading';
240
+ let matches = candidates.filter((range) => range.token.type === type);
241
+ if (depth)
242
+ matches = matches.filter((range) => range.token.depth === Number.parseInt(depth[1], 10));
243
+ return this.run(this.narrow(this.filterByText(matches, segment), segment), remaining);
244
+ }
245
+ sectionsOf(candidates, segment) {
246
+ const wanted = segment.selector.match(/^section([1-6])$/);
247
+ const sections = [];
248
+ for (let i = 0; i < candidates.length; i++) {
249
+ const range = candidates[i];
250
+ if (range.token.type !== 'heading')
251
+ continue;
252
+ const heading = range.token;
253
+ if (wanted && heading.depth !== Number.parseInt(wanted[1], 10))
254
+ continue;
255
+ if (segment.textMatch && !this.matchText(heading.text, segment.textMatch))
256
+ continue;
257
+ const innerTokens = [];
258
+ let end = range.start + range.length;
259
+ for (let j = i + 1; j < candidates.length; j++) {
260
+ const next = candidates[j];
261
+ if (next.token.type === 'heading' && next.token.depth <= heading.depth)
262
+ break;
263
+ innerTokens.push(next);
264
+ end = MarkdownEditor.blockEnd(next);
265
+ }
266
+ sections.push({ token: range.token, start: range.start, length: end - range.start, innerTokens });
267
+ }
268
+ return sections;
269
+ }
270
+ itemsOf(candidates) {
271
+ const items = [];
272
+ for (const range of candidates) {
273
+ if (range.token.type !== 'list')
274
+ continue;
275
+ const raw = range.token.raw;
276
+ let cursor = 0;
277
+ for (const item of range.token.items) {
278
+ const itemRaw = item.raw;
279
+ const at = raw.indexOf(itemRaw, cursor);
280
+ if (at === -1)
281
+ continue;
282
+ items.push({ token: item, start: range.start + at, length: itemRaw.length });
283
+ cursor = at + itemRaw.length;
284
+ }
285
+ }
286
+ return items;
287
+ }
288
+ expandSections(matches) {
289
+ if (!matches.some((range) => range.innerTokens))
290
+ return matches;
291
+ const expanded = [];
292
+ for (const range of matches) {
293
+ if (!range.innerTokens) {
294
+ expanded.push(range);
295
+ continue;
296
+ }
297
+ expanded.push({ token: range.token, start: range.start, length: (range.token.raw || '').length });
298
+ expanded.push(...range.innerTokens);
299
+ }
300
+ return expanded;
301
+ }
302
+ narrow(matches, segment) {
303
+ if (segment.index !== null) {
304
+ let index = segment.index;
305
+ if (index < 0)
306
+ index = matches.length + index;
307
+ if (index < 0 || index >= matches.length)
308
+ return [];
309
+ return [matches[index]];
310
+ }
311
+ if (segment.slice)
312
+ return matches.slice(segment.slice.from, segment.slice.to);
313
+ return matches;
314
+ }
315
+ filterByText(matches, segment) {
316
+ if (!segment.textMatch)
317
+ return matches;
318
+ return matches.filter((range) => this.matchText(this.tokenText(range.token), segment.textMatch));
319
+ }
320
+ matchText(text, matcher) {
321
+ let result = false;
322
+ if (matcher.mode === 'exact')
323
+ result = text === matcher.value;
324
+ if (matcher.mode === 'contains')
325
+ result = text.includes(matcher.value);
326
+ if (matcher.mode === 'regex')
327
+ result = new RegExp(matcher.value, matcher.flags || '').test(text);
328
+ if (matcher.mode === 'predicate')
329
+ result = matcher.predicate(text);
330
+ if (matcher.negated)
331
+ return !result;
332
+ return result;
333
+ }
334
+ valueMatcher(matcher) {
335
+ if (typeof matcher === 'function')
336
+ return { mode: 'predicate', value: '', negated: false, predicate: matcher };
337
+ if (matcher instanceof RegExp)
338
+ return { mode: 'regex', value: matcher.source, negated: false, flags: matcher.flags };
339
+ return { mode: 'exact', value: matcher, negated: false };
340
+ }
341
+ tokenText(token) {
342
+ const value = token;
343
+ if (this.isComment(token))
344
+ return value.raw.trim().replace(/^<!--/, '').replace(/-->$/, '').trim();
345
+ if (token.type === 'html')
346
+ return value.raw || '';
347
+ if (token.type === 'table')
348
+ return [...(value.header || []).map((cell) => cell.text), ...(value.rows || []).flatMap((row) => row.map((cell) => cell.text))].join(', ');
349
+ return value.text || '';
350
+ }
351
+ isComment(token) {
352
+ if (token.type !== 'html')
353
+ return false;
354
+ return (token.raw || '').trimStart().startsWith('<!--');
355
+ }
356
+ insertAll(offsetOf, markdown) {
357
+ return new MarkdownQuery(this.editor().insert(this.matches.map(offsetOf), String(markdown)));
358
+ }
359
+ containerStart(range) {
360
+ if (!range.innerTokens)
361
+ throw new MdqOperationError(`prepend needs a section or list, got ${range.token.type}`);
362
+ return range.start + (range.token.raw || '').length;
363
+ }
364
+ containerEnd(range) {
365
+ if (!range.innerTokens)
366
+ throw new MdqOperationError(`append needs a section or list, got ${range.token.type}`);
367
+ const last = range.innerTokens[range.innerTokens.length - 1];
368
+ if (!last)
369
+ return this.containerStart(range);
370
+ return MarkdownEditor.blockEnd(last);
371
+ }
372
+ }
373
+ export class MdqError extends Error {
374
+ }
375
+ export class MdqSelectorError extends MdqError {
376
+ index;
377
+ constructor(message, index) {
378
+ super(message);
379
+ this.name = 'MdqSelectorError';
380
+ this.index = index;
381
+ }
382
+ }
383
+ export class MdqOperationError extends MdqError {
384
+ constructor(message) {
385
+ super(message);
386
+ this.name = 'MdqOperationError';
387
+ }
388
+ }
389
+ export function mdq(source) {
390
+ return new MarkdownQuery(String(source));
391
+ }
392
+ export function parseQuery(input) {
393
+ const segments = [];
394
+ SEGMENT.lastIndex = 0;
395
+ while (SEGMENT.lastIndex < input.length) {
396
+ const at = SEGMENT.lastIndex;
397
+ const match = SEGMENT.exec(input);
398
+ if (!match)
399
+ throw new MdqSelectorError(`Unexpected character "${input[at]}" in selector`, at);
400
+ const [, prefix, name, negated, contains, quoted, pattern, flags, brackets] = match;
401
+ if (!SELECTOR_NAMES.has(name) && !/^(?:h|section)[1-6]$/.test(name))
402
+ throw new MdqSelectorError(`Unknown selector "${name}"`, at + prefix.length);
403
+ const segment = { selector: name, index: null, slice: null };
404
+ if (quoted !== undefined) {
405
+ let mode = 'exact';
406
+ if (contains)
407
+ mode = 'contains';
408
+ segment.textMatch = { mode, value: quoted.replace(/\\(.)/g, '$1'), negated: Boolean(negated) };
409
+ }
410
+ if (pattern !== undefined) {
411
+ segment.textMatch = { mode: 'regex', value: pattern, negated: Boolean(negated), flags };
412
+ }
413
+ for (const [, bounds] of brackets.matchAll(/\[([^\]]*)\]/g)) {
414
+ if (!bounds || !/^-?\d*(?::-?\d*)?$/.test(bounds))
415
+ continue;
416
+ if (!bounds.includes(':')) {
417
+ segment.index = Number.parseInt(bounds, 10);
418
+ continue;
419
+ }
420
+ const [from, to] = bounds.split(':');
421
+ segment.slice = {};
422
+ if (from)
423
+ segment.slice.from = Number.parseInt(from, 10);
424
+ if (to)
425
+ segment.slice.to = Number.parseInt(to, 10);
426
+ }
427
+ segments.push(segment);
428
+ }
429
+ return segments;
430
+ }
431
+ export function buildTokenIndex(source) {
432
+ const { body, offset } = MarkdownEditor.splitFrontmatter(source);
433
+ const ranges = [];
434
+ let cursor = offset;
435
+ for (const token of marked.lexer(body)) {
436
+ const raw = token.raw || '';
437
+ if (token.type === 'space') {
438
+ const previous = ranges[ranges.length - 1];
439
+ if (previous)
440
+ previous.trailing = { start: cursor, length: raw.length };
441
+ cursor += raw.length;
442
+ continue;
443
+ }
444
+ ranges.push({ token, start: cursor, length: raw.length });
445
+ cursor += raw.length;
446
+ }
447
+ return ranges;
448
+ }
449
+ const SELECTOR_NAMES = new Set(['section', 'heading', 'paragraph', 'table', 'list', 'item', 'code', 'blockquote', 'hr', 'html', 'comment']);
450
+ const TOKEN_ALIASES = { item: 'list_item' };
451
+ const SEGMENT = /(\s*\.?)([A-Za-z]\w*)(?:\((!?)(~?)(?:"((?:[^"\\]|\\.)*)"|\/((?:[^/\\]|\\.)*)\/([a-z]*))\))?((?:\[[^\]]*\])*)\s*/y;
@@ -9,3 +9,6 @@ export declare function stripComments(code: string): string;
9
9
  export declare function isNonReusableCode(code: string): boolean;
10
10
  export declare function toReusableSessionStep(step: StepData): SessionStep | null;
11
11
  export declare function mergeUniqueStepsByCode(primary: SessionStep[], secondary: SessionStep[]): SessionStep[];
12
+ export declare function isInternalStep(step: {
13
+ title?: string;
14
+ }): boolean;
@@ -1,5 +1,6 @@
1
1
  import { isDynamicId } from "./xpath.js";
2
2
  export const CODECEPT_TOOLS = ['click', 'hover', 'pressKey', 'form'];
3
+ const INTERNAL_STEP_PREFIXES = ['grab', 'save'];
3
4
  const CODECEPT_FORM_COMMANDS = ['I.fillField', 'I.type', 'I.selectOption', 'I.attachFile', 'I.checkOption', 'I.uncheckOption'];
4
5
  export function isCodeceptToolName(toolName) {
5
6
  return CODECEPT_TOOLS.includes(toolName);
@@ -66,3 +67,9 @@ export function mergeUniqueStepsByCode(primary, secondary) {
66
67
  }
67
68
  return merged;
68
69
  }
70
+ export function isInternalStep(step) {
71
+ const title = step?.title;
72
+ if (!title)
73
+ return false;
74
+ return INTERNAL_STEP_PREFIXES.some((prefix) => title.startsWith(prefix));
75
+ }
@@ -4,4 +4,5 @@ export declare function normalizeInlineText(text: string): string;
4
4
  export declare function sanitizeFilename(name: string): string;
5
5
  export declare function safeFilename(name: string, ext?: string, maxBytes?: number): string;
6
6
  export declare function truncate(text: string, max: number): string;
7
+ export declare function truncateMiddle(text: string, max: number): string;
7
8
  export declare function compactErrorMessage(error: unknown): string;
@@ -42,6 +42,13 @@ export function truncate(text, max) {
42
42
  return text;
43
43
  return `${text.slice(0, max - 3)}...`;
44
44
  }
45
+ export function truncateMiddle(text, max) {
46
+ if (text.length <= max)
47
+ return text;
48
+ const head = Math.ceil((max - 3) / 2);
49
+ const tail = max - 3 - head;
50
+ return `${text.slice(0, head)}...${text.slice(text.length - tail)}`;
51
+ }
45
52
  const MAX_COMPACT_ERROR = 400;
46
53
  export function compactErrorMessage(error) {
47
54
  let text = stripAnsi(String(error));
@@ -1,3 +1,4 @@
1
+ export declare function listTestFiles(testsDir: string): string[];
1
2
  export declare function loadTestSuites(testsDir: string): any[];
2
3
  export declare function printTestList(suites: any[]): void;
3
4
  export declare function dryRunTestFile(filePath: string): Promise<void>;
@@ -8,12 +8,15 @@ import storeListener from 'codeceptjs/lib/listener/store';
8
8
  import store from 'codeceptjs/lib/store';
9
9
  import figureSet from 'figures';
10
10
  import { ConfigParser } from "../config.js";
11
- export function loadTestSuites(testsDir) {
11
+ export function listTestFiles(testsDir) {
12
12
  if (!existsSync(testsDir))
13
13
  return [];
14
- const jsFiles = readdirSync(testsDir)
14
+ return readdirSync(testsDir)
15
15
  .filter((f) => f.endsWith('.js'))
16
16
  .map((f) => path.resolve(testsDir, f));
17
+ }
18
+ export function loadTestSuites(testsDir) {
19
+ const jsFiles = listTestFiles(testsDir);
17
20
  if (jsFiles.length === 0)
18
21
  return [];
19
22
  codeceptjs.container.createMocha();
@@ -6,3 +6,4 @@ export declare function generalizeUrl(url: string, replaceSegment?: (segment: st
6
6
  export declare function matchesUrl(pattern: string, path: string): boolean;
7
7
  export declare function extractStatePath(url: string): string;
8
8
  export declare function matchesNavigationUrl(expected: string, current: string): boolean;
9
+ export declare function isSameHostFamily(urlA: string, urlB: string): boolean;
@@ -145,3 +145,10 @@ export function matchesNavigationUrl(expected, current) {
145
145
  .filter(Boolean);
146
146
  return recordSegments.length > 0 && recordSegments.every(isDynamicSegment);
147
147
  }
148
+ export function isSameHostFamily(urlA, urlB) {
149
+ const hostA = URL.parse(urlA)?.host.toLowerCase();
150
+ const hostB = URL.parse(urlB)?.host.toLowerCase();
151
+ if (!hostA || !hostB)
152
+ return false;
153
+ return hostA === hostB || hostA.endsWith(`.${hostB}`) || hostB.endsWith(`.${hostA}`);
154
+ }
@@ -21,7 +21,7 @@ endpoint: "/users"
21
21
  CRUD for users. Admin role required for writes. IDs are UUIDs.
22
22
  ```
23
23
 
24
- Chief loads knowledge matching the endpoint it's planning, and Curler loads it again for the endpoint it's testing, so auth headers and payload rules reach the requests themselves. Running `know` again on the same endpoint appends to the file. See [knowledge](../workflow/knowledge.md) for how matching and files work.
24
+ Chief loads knowledge matching the endpoint it's planning, and Curler loads it again for the endpoint it's testing. When `plan` or `explore` runs, Chief first picks out the headers, query parameters and body fields the knowledge says to send, and every request whose path matches the file's `endpoint:` pattern carries them, the sample data fetch included. Headers from `api.headers` or `-H` take precedence over knowledge. Running `know` again on the same endpoint appends to the file. See [knowledge](../workflow/knowledge.md) for how matching and files work.
25
25
 
26
26
  For a fact that should not be stored — a token, a one-off fixture — pass `--knowledge` instead. It applies to the run only:
27
27