explorbot 0.4.10 → 0.5.1
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 +4 -1
- package/bin/explorbot-cli.ts +2 -1
- package/bin/mdq.ts +18 -0
- package/boat/api-tester/src/ai/chief.ts +72 -0
- package/boat/api-tester/src/api-client.ts +37 -0
- package/boat/prima/README.md +31 -4
- package/boat/prima/bin/prima-cli.ts +2 -1
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/explorbot-cli.js +2 -1
- package/dist/bin/mdq.js +19 -0
- package/dist/boat/api-tester/src/ai/chief.js +69 -0
- package/dist/boat/api-tester/src/api-client.js +26 -0
- package/dist/boat/prima/bin/prima-cli.js +2 -1
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +4 -3
- package/dist/src/ai/agent.d.ts +3 -1
- package/dist/src/ai/judge-provider.d.ts +17 -0
- package/dist/src/ai/judge-provider.js +56 -0
- package/dist/src/ai/judge-tool.d.ts +2 -0
- package/dist/src/ai/judge-tool.js +33 -0
- package/dist/src/ai/judge.d.ts +28 -0
- package/dist/src/ai/judge.js +71 -0
- package/dist/src/ai/navigator.d.ts +3 -0
- package/dist/src/ai/navigator.js +13 -4
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +57 -5
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +53 -8
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/researcher/locators.js +2 -2
- package/dist/src/ai/researcher/pagination.js +1 -1
- package/dist/src/ai/researcher/research-result.js +2 -2
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/rules.js +13 -0
- package/dist/src/ai/task-agent.d.ts +2 -0
- package/dist/src/ai/task-agent.js +3 -1
- package/dist/src/ai/tester.js +19 -15
- package/dist/src/ai/tools.d.ts +4 -3
- package/dist/src/ai/tools.js +35 -7
- package/dist/src/api/request-result.js +2 -1
- package/dist/src/command-handler.d.ts +1 -0
- package/dist/src/command-handler.js +24 -3
- package/dist/src/commands/base-command.d.ts +5 -0
- package/dist/src/commands/base-command.js +3 -0
- package/dist/src/commands/explore-command.d.ts +2 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/freesail-command.js +8 -2
- package/dist/src/commands/init-command.js +1 -1
- package/dist/src/commands/navigate-command.d.ts +2 -1
- package/dist/src/commands/navigate-command.js +6 -0
- package/dist/src/commands/options/decision-model-option.d.ts +6 -0
- package/dist/src/commands/options/decision-model-option.js +10 -0
- package/dist/src/commands/options/index.d.ts +2 -0
- package/dist/src/commands/options/index.js +2 -0
- package/dist/src/commands/plan-load-command.d.ts +2 -1
- package/dist/src/commands/plan-load-command.js +4 -0
- package/dist/src/commands/plans-command.d.ts +3 -9
- package/dist/src/commands/plans-command.js +11 -21
- package/dist/src/commands/rerun-command.d.ts +2 -1
- package/dist/src/commands/rerun-command.js +5 -1
- package/dist/src/commands/research-command.d.ts +2 -1
- package/dist/src/commands/research-command.js +6 -0
- package/dist/src/commands/test-command.d.ts +2 -1
- package/dist/src/commands/test-command.js +4 -1
- package/dist/src/components/Autocomplete.js +26 -12
- package/dist/src/components/InputReadline.js +10 -1
- package/dist/src/config.d.ts +7 -0
- package/dist/src/config.js +11 -0
- package/dist/src/experience-tracker.js +4 -3
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +8 -0
- package/dist/src/knowledge-tracker.js +1 -1
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +16 -0
- package/dist/src/test-plan.d.ts +11 -0
- package/dist/src/test-plan.js +54 -2
- package/dist/src/utils/aria-ref.js +1 -1
- package/dist/src/utils/logger.js +8 -2
- package/dist/src/utils/markdown-query.d.ts +1 -48
- package/dist/src/utils/markdown-query.js +1 -444
- package/dist/src/utils/mdq/cli.d.ts +6 -0
- package/dist/src/utils/mdq/cli.js +122 -0
- package/dist/src/utils/mdq/edit.d.ts +24 -0
- package/dist/src/utils/mdq/edit.js +147 -0
- package/dist/src/utils/mdq/query.d.ts +118 -0
- package/dist/src/utils/mdq/query.js +451 -0
- package/dist/src/utils/strings.d.ts +1 -0
- package/dist/src/utils/strings.js +7 -0
- package/dist/src/utils/test-files.d.ts +1 -0
- package/dist/src/utils/test-files.js +5 -2
- package/docs/api-testing/planning.md +1 -1
- package/docs/basics/providers.md +37 -0
- package/docs/index.json +1 -0
- package/docs/reference/commands.md +1 -0
- package/docs/reference/configuration.md +2 -0
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
- package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
- package/docs/web-testing/decisions.md +108 -0
- package/docs/workflow/agentic-usage.md +1 -0
- package/package.json +4 -3
- package/src/ai/agent.ts +3 -1
- package/src/ai/judge-provider.ts +62 -0
- package/src/ai/judge-tool.ts +35 -0
- package/src/ai/judge.ts +75 -0
- package/src/ai/navigator.ts +15 -4
- package/src/ai/pilot.ts +58 -5
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +52 -8
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/pagination.ts +1 -1
- package/src/ai/researcher/research-result.ts +2 -2
- package/src/ai/researcher.ts +1 -1
- package/src/ai/rules.ts +13 -0
- package/src/ai/task-agent.ts +4 -1
- package/src/ai/tester.ts +19 -16
- package/src/ai/tools.ts +42 -7
- package/src/api/request-result.ts +2 -1
- package/src/command-handler.ts +28 -3
- package/src/commands/base-command.ts +9 -0
- package/src/commands/explore-command.ts +15 -2
- package/src/commands/freesail-command.ts +9 -2
- package/src/commands/init-command.ts +1 -1
- package/src/commands/navigate-command.ts +8 -1
- package/src/commands/options/decision-model-option.ts +11 -0
- package/src/commands/options/index.ts +2 -0
- package/src/commands/plan-load-command.ts +6 -1
- package/src/commands/plans-command.ts +13 -29
- package/src/commands/rerun-command.ts +7 -2
- package/src/commands/research-command.ts +8 -1
- package/src/commands/test-command.ts +6 -2
- package/src/components/Autocomplete.tsx +39 -10
- package/src/components/InputReadline.tsx +10 -1
- package/src/config.ts +11 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/knowledge-tracker.ts +1 -1
- package/src/state-manager.ts +16 -0
- package/src/test-plan.ts +67 -2
- package/src/utils/aria-ref.ts +1 -1
- package/src/utils/logger.ts +6 -1
- package/src/utils/markdown-query.ts +1 -519
- package/src/utils/mdq/cli.ts +118 -0
- package/src/utils/mdq/edit.ts +158 -0
- package/src/utils/mdq/query.ts +556 -0
- package/src/utils/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
|
@@ -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;
|
|
@@ -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));
|
|
@@ -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
|
|
11
|
+
export function listTestFiles(testsDir) {
|
|
12
12
|
if (!existsSync(testsDir))
|
|
13
13
|
return [];
|
|
14
|
-
|
|
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();
|
|
@@ -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,
|
|
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
|
|
package/docs/basics/providers.md
CHANGED
|
@@ -22,6 +22,8 @@ Explorbot uses three roles:
|
|
|
22
22
|
|
|
23
23
|
Pick a fast, cheap model for the first two and a stronger one for the third. When a provider has no recommended model for one of these roles, combine it with another provider for that role.
|
|
24
24
|
|
|
25
|
+
An optional [decision model](#decision-model) can answer narrow yes/no and pick-one questions so the other models are called less often.
|
|
26
|
+
|
|
25
27
|
### OpenRouter
|
|
26
28
|
|
|
27
29
|
Start with OpenRouter. One key reaches [many providers and models](https://openrouter.ai/models).
|
|
@@ -308,6 +310,40 @@ Laguna XS is an agentic coding model — fast, cheap, and reliable at tool calli
|
|
|
308
310
|
|
|
309
311
|
Keep `agenticModel` on another provider. Poolside's endpoint accepts `response_format: json_schema` but does not enforce it, so structured-output calls depend on the model volunteering valid JSON. Laguna XS usually does; Laguna S answers in prose instead, which makes it unusable for the Planner, Pilot, and Captain. Laguna S is also slow enough under page-sized prompts to hit Explorbot's request timeouts, so it is not a substitute for Laguna XS in the `model` role either.
|
|
310
312
|
|
|
313
|
+
## Decision Model
|
|
314
|
+
|
|
315
|
+
A decision model is an optional fourth role. It does not generate text or call tools. It receives the current state and one closed question, and returns an answer with a probability. Explorbot uses it for narrow choices that would otherwise take a call to a larger model or a guess.
|
|
316
|
+
|
|
317
|
+
Explorbot supports TypeSafe's Jev, reached through OpenRouter or through TypeSafe's API directly:
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
export default {
|
|
321
|
+
ai: {
|
|
322
|
+
// ...your model, visionModel, agenticModel...
|
|
323
|
+
decisionModel: { provider: 'openrouter', model: 'typesafe/jev-1.13' },
|
|
324
|
+
// or: decisionModel: { provider: 'typesafe', model: 'jev-latest' },
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
| Option | Type | Description |
|
|
330
|
+
|--------|------|-------------|
|
|
331
|
+
| `provider` | `'openrouter' \| 'typesafe'` | Authenticates with `OPENROUTER_API_KEY` or `TYPESAFE_API_KEY` |
|
|
332
|
+
| `model` | `string` | Model ID at that provider |
|
|
333
|
+
| `tool` | `boolean` | Give Tester and Pilot a `judge` tool to ask their own questions. Default: `true` |
|
|
334
|
+
| `direct` | `boolean` | Consult the model at Explorbot's built-in decision points. Default: `true` |
|
|
335
|
+
|
|
336
|
+
Without a config file, or to turn it on for one run, set `EXPLORBOT_DECISION_MODEL` or pass `--decision-model` as `provider/model-id`. It overrides `provider` and `model` in the config and keeps `tool` and `direct`:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
EXPLORBOT_DECISION_MODEL=openrouter/typesafe/jev-1.13 npx explorbot explore
|
|
340
|
+
npx prima-cli go "Settings" --decision-model typesafe/jev-latest
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Leave `decisionModel` unset and Explorbot never calls it. An unknown provider or a missing API key stops Explorbot at startup.
|
|
344
|
+
|
|
345
|
+
Explorbot acts on an answer only when it is more than 70% sure, and otherwise does what it would have done without the decision model. See [Decisions](../web-testing/decisions.md) for where it is consulted, the `judge` tool, and what happens when an answer is rejected.
|
|
346
|
+
|
|
311
347
|
## Multi-Provider Configuration
|
|
312
348
|
|
|
313
349
|
Mix clients the same way you assign `model`, `visionModel`, and `agenticModel`. Each field can use a different provider instance — a fast provider does the token-heavy reading while a stronger one makes the decisions:
|
|
@@ -359,4 +395,5 @@ export OPENAI_API_KEY=your-key-here
|
|
|
359
395
|
export ANTHROPIC_API_KEY=your-key-here
|
|
360
396
|
export GOOGLE_API_KEY=your-key-here
|
|
361
397
|
export MISTRAL_API_KEY=your-key-here
|
|
398
|
+
export TYPESAFE_API_KEY=your-key-here
|
|
362
399
|
```
|
package/docs/index.json
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
{ "title": "Planner", "file": "web-testing/planner.md", "description": "Tune what gets tested" },
|
|
23
23
|
{ "title": "Researcher", "file": "web-testing/researcher.md", "description": "How pages are analyzed, and how to tune it" },
|
|
24
24
|
{ "title": "Page interaction", "file": "web-testing/page-interaction.md", "description": "How agents read and act on a page" },
|
|
25
|
+
{ "title": "Decisions", "file": "web-testing/decisions.md", "description": "The optional decision model: built-in decision points, the judge tool, fallbacks" },
|
|
25
26
|
{ "title": "Automated tests", "file": "web-testing/automated-tests.md", "description": "The runnable test files you keep" },
|
|
26
27
|
{ "title": "Rerun", "file": "web-testing/rerun.md", "description": "Re-run generated tests with AI healing" },
|
|
27
28
|
{ "title": "Agents", "file": "web-testing/agents.md", "description": "What each agent does under the hood" },
|
|
@@ -112,6 +112,7 @@ EXPLORBOT_AI_PROVIDER=openrouter \
|
|
|
112
112
|
| `EXPLORBOT_URL` | Base URL to test; the API boat reads it as the base endpoint |
|
|
113
113
|
| `EXPLORBOT_VISION_MODEL` | Screenshot analysis; overrides the provider recommendation |
|
|
114
114
|
| `EXPLORBOT_AGENTIC_MODEL` | Captain and Pilot decisions; overrides the provider recommendation |
|
|
115
|
+
| `EXPLORBOT_DECISION_MODEL` | Turns on the decision model, as provider/model-id (openrouter/typesafe/jev-1.13 or typesafe/jev-latest); overrides ai.decisionModel |
|
|
115
116
|
| `EXPLORBOT_OUTPUT` | Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites |
|
|
116
117
|
| `EXPLORBOT_EPHEMERAL` | Keep no state between runs — output goes to a fresh temp directory instead of the site dir |
|
|
117
118
|
| `EXPLORBOT_KNOWLEDGE` | Inline knowledge text, applied to every page |
|
|
@@ -537,6 +537,7 @@ export default {
|
|
|
537
537
|
ai: {
|
|
538
538
|
model: groq('openai/gpt-oss-20b'), // Default model instance (required)
|
|
539
539
|
visionModel: groq('meta-llama/llama-4-scout-17b-16e-instruct'), // Model for screenshot analysis; setting it enables vision features
|
|
540
|
+
decisionModel: { provider: 'openrouter', model: 'typesafe/jev-1.13' }, // Optional; see providers docs
|
|
540
541
|
config: {}, // Additional provider config
|
|
541
542
|
langfuse: { // Observability settings
|
|
542
543
|
enabled: true,
|
|
@@ -624,6 +625,7 @@ export default {
|
|
|
624
625
|
|
|
625
626
|
- [API testing](../api-testing/basics.md) — API testing setup and commands
|
|
626
627
|
- [AI providers](../basics/providers.md) — provider setup examples
|
|
628
|
+
- [Decisions](../web-testing/decisions.md) — the optional decision model and its fallbacks
|
|
627
629
|
- [Agents](../web-testing/agents.md) — agent descriptions and workflows
|
|
628
630
|
- [Agent hooks](../web-testing/hooks.md) — custom code before and after an agent runs
|
|
629
631
|
- [Researcher agent](../web-testing/researcher.md) — Researcher configuration and usage
|