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,118 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { Command } from 'commander';
3
+ import { type MarkdownQuery, MdqError, mdq } from './query.ts';
4
+
5
+ const EDIT_FLAGS = ['remove', 'replace', 'insertBefore', 'insertAfter', 'prepend', 'append', 'addRow', 'addItem', 'set'] as const;
6
+
7
+ export async function runMdq(argv: string[], readStdin: StdinReader): Promise<CliResult> {
8
+ const program = new Command();
9
+ program
10
+ .name('mdq')
11
+ .description('query and edit markdown')
12
+ .argument('[selector]', 'markdown selector')
13
+ .argument('[file]', 'file to read; stdin when omitted')
14
+ .option('-j, --json', 'output rows as JSON')
15
+ .option('-c, --count', 'print the number of matches')
16
+ .option('-t, --text', 'print unwrapped text')
17
+ .option('--frontmatter', 'print frontmatter as JSON')
18
+ .option('-i, --in-place', 'write the result back to the file')
19
+ .option('--remove', 'delete matched blocks')
20
+ .option('--replace <markdown>', 'replace matched blocks')
21
+ .option('--insert-before <markdown>', 'insert before each match')
22
+ .option('--insert-after <markdown>', 'insert after each match')
23
+ .option('--prepend <markdown>', 'insert at the start of each match')
24
+ .option('--append <markdown>', 'insert at the end of each match')
25
+ .option('--add-row <json>', 'append a table row')
26
+ .option('--add-item <text>', 'append a list item')
27
+ .option('--set <key=value>', 'set an entry; omit the value to delete it')
28
+ .exitOverride()
29
+ .configureOutput({ writeOut: () => {}, writeErr: () => {} });
30
+
31
+ try {
32
+ program.parse(argv, { from: 'user' });
33
+ } catch (error) {
34
+ return { output: String((error as Error).message), code: 2 };
35
+ }
36
+
37
+ const options = program.opts();
38
+ let [selector, file] = program.args;
39
+ if (options.frontmatter && selector && !file) {
40
+ file = selector;
41
+ selector = '';
42
+ }
43
+
44
+ let source = '';
45
+ if (!file) source = await readStdin();
46
+ if (file) {
47
+ try {
48
+ source = readFileSync(file, 'utf8');
49
+ } catch {
50
+ return { output: `Cannot read ${file}`, code: 2 };
51
+ }
52
+ }
53
+
54
+ try {
55
+ return await apply(mdq(source), selector, options, file);
56
+ } catch (error) {
57
+ if (error instanceof MdqError) return { output: error.message, code: 2 };
58
+ throw error;
59
+ }
60
+ }
61
+
62
+ async function apply(doc: MarkdownQuery, selector: string, options: Record<string, any>, file?: string): Promise<CliResult> {
63
+ if (options.frontmatter) return { output: `${JSON.stringify(doc.frontmatter(), null, 2)}\n`, code: 0 };
64
+ if (!selector) return { output: 'A selector is required', code: 2 };
65
+
66
+ const chosen = EDIT_FLAGS.filter((flag) => options[flag] !== undefined);
67
+ if (chosen.length > 1) return { output: 'Only one edit at a time', code: 2 };
68
+
69
+ const selection = doc.query(selector);
70
+ if (chosen.length === 0) return read(selection, options);
71
+
72
+ if (!selection.exists()) return finish(String(doc), 1, options, file);
73
+ return finish(String(edit(selection, chosen[0], options)), 0, options, file);
74
+ }
75
+
76
+ function read(selection: MarkdownQuery, options: Record<string, any>): CliResult {
77
+ let code = 1;
78
+ if (selection.exists()) code = 0;
79
+ if (options.count) return { output: `${selection.count()}\n`, code: 0 };
80
+ if (options.json) return { output: `${JSON.stringify(selection.rows(), null, 2)}\n`, code };
81
+ if (options.text)
82
+ return {
83
+ output: selection
84
+ .nodes()
85
+ .map((node) => node.text)
86
+ .join('\n\n'),
87
+ code,
88
+ };
89
+ return { output: selection.text(), code };
90
+ }
91
+
92
+ function edit(selection: MarkdownQuery, flag: string, options: Record<string, any>): MarkdownQuery {
93
+ if (flag === 'remove') return selection.remove();
94
+ if (flag === 'replace') return selection.replace(options.replace);
95
+ if (flag === 'insertBefore') return selection.insertBefore(options.insertBefore);
96
+ if (flag === 'insertAfter') return selection.insertAfter(options.insertAfter);
97
+ if (flag === 'prepend') return selection.prepend(options.prepend);
98
+ if (flag === 'append') return selection.append(options.append);
99
+ if (flag === 'addItem') return selection.addItem(options.addItem);
100
+ if (flag === 'addRow') return selection.addRow(JSON.parse(options.addRow));
101
+ const separator = options.set.indexOf('=');
102
+ if (separator < 0) return selection.setEntry(options.set, null);
103
+ return selection.setEntry(options.set.slice(0, separator), options.set.slice(separator + 1) || null);
104
+ }
105
+
106
+ function finish(output: string, code: number, options: Record<string, any>, file?: string): CliResult {
107
+ if (!options.inPlace) return { output, code };
108
+ if (!file) return { output: '--in-place needs a file', code: 2 };
109
+ writeFileSync(file, output);
110
+ return { output: '', code };
111
+ }
112
+
113
+ export type StdinReader = () => Promise<string>;
114
+
115
+ export interface CliResult {
116
+ output: string;
117
+ code: number;
118
+ }
@@ -0,0 +1,158 @@
1
+ import YAML from 'yaml';
2
+ import type { MatchedRange } from './query.ts';
3
+
4
+ export class MarkdownEditor {
5
+ private source: string;
6
+
7
+ constructor(source: string) {
8
+ this.source = source;
9
+ }
10
+
11
+ static splitFrontmatter(source: string): FrontmatterSplit {
12
+ if (!source.startsWith('---')) return { raw: '', body: source, offset: 0 };
13
+ const match = source.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/);
14
+ if (!match) return { raw: '', body: source, offset: 0 };
15
+ return { raw: match[1], body: source.slice(match[0].length), offset: match[0].length };
16
+ }
17
+
18
+ static blockEnd(range: MatchedRange): number {
19
+ if (range.trailing) return range.trailing.start + range.trailing.length;
20
+ return range.start + range.length;
21
+ }
22
+
23
+ static table(headers: string[], rows: string[][], align: (string | null)[]): string {
24
+ const widths = headers.map((header, index) => Math.max(header.length, 3, ...rows.map((row) => (row[index] || '').length)));
25
+ const line = (cells: string[]) => `| ${cells.map((cell, index) => (cell || '').padEnd(widths[index])).join(' | ')} |`;
26
+ const divider = `| ${widths.map((width, index) => MarkdownEditor.dashes(align[index], width)).join(' | ')} |`;
27
+ return `${[line(headers), divider, ...rows.map(line)].join('\n')}\n`;
28
+ }
29
+
30
+ static item(listRaw: string, text: string): string {
31
+ const lines = listRaw.split('\n').filter((line) => line.trim());
32
+ const last = lines[lines.length - 1] || '- x';
33
+ const ordered = last.match(/^(\s*)(\d+)([.)])\s/);
34
+ if (ordered) return `${ordered[1]}${Number.parseInt(ordered[2], 10) + 1}${ordered[3]} ${text}`;
35
+ const bullet = last.match(/^(\s*)([-*+])\s/);
36
+ if (!bullet) return `- ${text}`;
37
+ return `${bullet[1]}${bullet[2]} ${text}`;
38
+ }
39
+
40
+ static entries(text: string, key: string, value: string | null, isBlockquote: boolean): string {
41
+ const lines = text
42
+ .split('\n')
43
+ .map((line) => line.trim())
44
+ .filter(Boolean);
45
+
46
+ const index = lines.findIndex((line) => MarkdownEditor.entryKey(line) === key.toLowerCase());
47
+ if (index < 0 && value) lines.push(`${key}: ${value}`);
48
+ if (index >= 0 && value) lines[index] = `${key}: ${value}`;
49
+ if (index >= 0 && !value) lines.splice(index, 1);
50
+
51
+ if (!isBlockquote) return lines.join('\n');
52
+ return lines.map((line) => `> ${line}`).join('\n');
53
+ }
54
+
55
+ remove(ranges: MatchedRange[]): string {
56
+ const ordered = this.dedupe(ranges);
57
+ let result = this.source;
58
+
59
+ for (let i = ordered.length - 1; i >= 0; i--) {
60
+ const range = ordered[i];
61
+ const head = result.slice(0, range.start);
62
+ const tail = result.slice(MarkdownEditor.blockEnd(range));
63
+ if (tail) {
64
+ result = head + tail;
65
+ continue;
66
+ }
67
+ if (!head) {
68
+ result = '';
69
+ continue;
70
+ }
71
+ result = `${head.trimEnd()}\n`;
72
+ }
73
+
74
+ return result;
75
+ }
76
+
77
+ replace(ranges: MatchedRange[], render: (range: MatchedRange, index: number) => string): string {
78
+ const ordered = this.dedupe(ranges);
79
+ const rendered = ordered.map(render);
80
+ let result = this.source;
81
+
82
+ for (let i = ordered.length - 1; i >= 0; i--) {
83
+ const range = ordered[i];
84
+ result = result.slice(0, range.start) + rendered[i] + result.slice(range.start + range.length);
85
+ }
86
+
87
+ return result;
88
+ }
89
+
90
+ insert(offsets: number[], markdown: string): string {
91
+ const ordered = [...offsets].sort((a, b) => a - b);
92
+ let result = this.source;
93
+ for (let i = ordered.length - 1; i >= 0; i--) {
94
+ result = MarkdownEditor.spliceIn(result, ordered[i], markdown);
95
+ }
96
+ return result;
97
+ }
98
+
99
+ frontmatter(): Record<string, unknown> {
100
+ const { raw } = MarkdownEditor.splitFrontmatter(this.source);
101
+ if (!raw) return {};
102
+ return (YAML.parseDocument(raw).toJS() as Record<string, unknown>) || {};
103
+ }
104
+
105
+ setFrontmatter(key: string, value: unknown): string {
106
+ const { raw, body, offset } = MarkdownEditor.splitFrontmatter(this.source);
107
+ let document = new YAML.Document({});
108
+ if (raw) document = YAML.parseDocument(raw);
109
+ if (value === null) document.delete(key);
110
+ if (value !== null) document.set(key, value);
111
+
112
+ const rendered = document.toString().trimEnd();
113
+ if (!offset) return `---\n${rendered}\n---\n\n${this.source}`;
114
+ return `---\n${rendered}\n---\n${body}`;
115
+ }
116
+
117
+ private static spliceIn(source: string, offset: number, markdown: string): string {
118
+ const block = `${markdown.trimEnd()}\n`;
119
+ const before = source.slice(0, offset).replace(/\n+$/, '');
120
+ const after = source.slice(offset).replace(/^\n+/, '');
121
+ if (!before) return `${block}\n${after}`;
122
+ if (!after) return `${before}\n\n${block}`;
123
+ return `${before}\n\n${block}\n${after}`;
124
+ }
125
+
126
+ private static dashes(alignment: string | null, width: number): string {
127
+ if (alignment === 'center') return `:${'-'.repeat(Math.max(width - 2, 1))}:`;
128
+ if (alignment === 'left') return `:${'-'.repeat(Math.max(width - 1, 1))}`;
129
+ if (alignment === 'right') return `${'-'.repeat(Math.max(width - 1, 1))}:`;
130
+ return '-'.repeat(width);
131
+ }
132
+
133
+ private static entryKey(line: string): string | null {
134
+ const separator = line.indexOf(':');
135
+ if (separator < 1) return null;
136
+ return line.slice(0, separator).trim().toLowerCase();
137
+ }
138
+
139
+ private dedupe(ranges: MatchedRange[]): MatchedRange[] {
140
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
141
+ const kept: MatchedRange[] = [];
142
+ let lastEnd = -1;
143
+
144
+ for (const range of sorted) {
145
+ if (range.start < lastEnd) continue;
146
+ kept.push(range);
147
+ lastEnd = range.start + range.length;
148
+ }
149
+
150
+ return kept;
151
+ }
152
+ }
153
+
154
+ export interface FrontmatterSplit {
155
+ raw: string;
156
+ body: string;
157
+ offset: number;
158
+ }