explorbot 0.2.3 → 0.2.5

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 (174) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +26 -8
  3. package/boat/api-tester/src/cli.ts +17 -0
  4. package/boat/api-tester/src/config.ts +4 -2
  5. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  6. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  7. package/boat/doc-collector/src/cli.ts +14 -1
  8. package/boat/doc-collector/src/config.ts +4 -2
  9. package/boat/prima/bin/prima-cli.ts +0 -0
  10. package/boat/prima/src/activity-line.ts +33 -0
  11. package/boat/prima/src/cli.ts +127 -86
  12. package/boat/prima/src/envelope.ts +102 -52
  13. package/boat/prima/src/prima.ts +567 -128
  14. package/boat/prima/src/pw-parser.ts +11 -1
  15. package/boat/prima/src/pw-registry.ts +4 -5
  16. package/boat/prima/src/session-log.ts +126 -0
  17. package/dist/bin/explorbot-cli.js +26 -8
  18. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  19. package/dist/boat/api-tester/src/cli.js +17 -0
  20. package/dist/boat/api-tester/src/config.js +4 -2
  21. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  22. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  23. package/dist/boat/doc-collector/src/cli.js +14 -1
  24. package/dist/boat/doc-collector/src/config.js +4 -2
  25. package/dist/boat/prima/src/activity-line.js +30 -0
  26. package/dist/boat/prima/src/cli.js +109 -77
  27. package/dist/boat/prima/src/envelope.js +94 -44
  28. package/dist/boat/prima/src/prima.js +533 -119
  29. package/dist/boat/prima/src/pw-parser.js +13 -1
  30. package/dist/boat/prima/src/pw-registry.js +4 -5
  31. package/dist/boat/prima/src/session-log.js +108 -0
  32. package/dist/package.json +3 -2
  33. package/dist/rules/navigator/verification-actions.md +20 -0
  34. package/dist/src/action-result.d.ts +7 -0
  35. package/dist/src/action-result.js +4 -0
  36. package/dist/src/action.d.ts +2 -0
  37. package/dist/src/action.js +41 -2
  38. package/dist/src/ai/captain/web-mode.js +6 -3
  39. package/dist/src/ai/captain.js +2 -0
  40. package/dist/src/ai/navigator.d.ts +34 -0
  41. package/dist/src/ai/navigator.js +237 -181
  42. package/dist/src/ai/pilot.d.ts +7 -0
  43. package/dist/src/ai/pilot.js +90 -2
  44. package/dist/src/ai/provider.d.ts +2 -2
  45. package/dist/src/ai/provider.js +14 -23
  46. package/dist/src/ai/rerunner.js +2 -1
  47. package/dist/src/ai/researcher/cache.d.ts +2 -0
  48. package/dist/src/ai/researcher/cache.js +10 -2
  49. package/dist/src/ai/researcher.js +3 -2
  50. package/dist/src/ai/rules.js +17 -10
  51. package/dist/src/ai/session-analyst.js +2 -0
  52. package/dist/src/ai/task-agent.js +4 -1
  53. package/dist/src/ai/tester.d.ts +6 -3
  54. package/dist/src/ai/tester.js +50 -46
  55. package/dist/src/ai/tools.d.ts +14 -0
  56. package/dist/src/ai/tools.js +117 -37
  57. package/dist/src/commands/config-command.d.ts +51 -0
  58. package/dist/src/commands/config-command.js +117 -0
  59. package/dist/src/commands/index.js +2 -0
  60. package/dist/src/config.d.ts +9 -1
  61. package/dist/src/config.js +53 -4
  62. package/dist/src/execution-controller.d.ts +2 -0
  63. package/dist/src/execution-controller.js +6 -0
  64. package/dist/src/explorbot.d.ts +2 -1
  65. package/dist/src/explorbot.js +7 -2
  66. package/dist/src/explorer.js +2 -3
  67. package/dist/src/playwright-recorder.js +30 -0
  68. package/dist/src/remote.d.ts +55 -0
  69. package/dist/src/remote.js +235 -0
  70. package/dist/src/reporter.d.ts +1 -0
  71. package/dist/src/reporter.js +7 -1
  72. package/dist/src/state-manager.d.ts +2 -1
  73. package/dist/src/state-manager.js +3 -1
  74. package/dist/src/stats.d.ts +1 -0
  75. package/dist/src/stats.js +1 -0
  76. package/dist/src/test-plan.d.ts +3 -0
  77. package/dist/src/test-plan.js +26 -0
  78. package/dist/src/utils/aria.d.ts +2 -8
  79. package/dist/src/utils/aria.js +69 -40
  80. package/dist/src/utils/html.js +1 -0
  81. package/dist/src/utils/logger.d.ts +7 -1
  82. package/dist/src/utils/logger.js +32 -0
  83. package/dist/src/utils/page-readiness.js +18 -1
  84. package/dist/src/utils/url-matcher.js +3 -0
  85. package/dist/src/utils/web-element.d.ts +2 -0
  86. package/dist/src/utils/web-element.js +8 -0
  87. package/dist/src/utils/web-sandbox.d.ts +1 -1
  88. package/dist/src/utils/web-sandbox.js +2 -3
  89. package/docs/api-testing/basics.md +90 -0
  90. package/docs/api-testing/planning.md +57 -0
  91. package/docs/api-testing/running-tests.md +55 -0
  92. package/docs/assets/cloud-report.png +0 -0
  93. package/docs/assets/html-report.png +0 -0
  94. package/docs/assets/langfuse-trace.png +0 -0
  95. package/docs/assets/successful-explore-run.png +0 -0
  96. package/docs/basics/getting-started.md +140 -0
  97. package/docs/basics/prerequisites.md +63 -0
  98. package/docs/basics/providers.md +362 -0
  99. package/docs/basics/running.md +78 -0
  100. package/docs/contributing/ai-integration-tests.md +57 -0
  101. package/docs/contributing/contributing.md +90 -0
  102. package/docs/contributing/demo-videos.md +36 -0
  103. package/docs/contributing/npm-package.md +138 -0
  104. package/docs/contributing/observability.md +227 -0
  105. package/docs/contributing/regression-tests.md +103 -0
  106. package/docs/contributing/testing.md +95 -0
  107. package/docs/doc-collection/basics.md +128 -0
  108. package/docs/doc-collection/crawling.md +67 -0
  109. package/docs/doc-collection/interactive-mode.md +99 -0
  110. package/docs/index.json +87 -0
  111. package/docs/reference/commands.md +997 -0
  112. package/docs/reference/configuration.md +569 -0
  113. package/docs/reference/scripting.md +303 -0
  114. package/docs/reference/websocket.md +50 -0
  115. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  116. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  117. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  118. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  119. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  120. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  121. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  122. package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
  123. package/docs/web-testing/agents.md +158 -0
  124. package/docs/web-testing/automated-tests.md +134 -0
  125. package/docs/web-testing/basics.md +91 -0
  126. package/docs/web-testing/customization.md +131 -0
  127. package/docs/web-testing/hooks.md +238 -0
  128. package/docs/web-testing/page-interaction.md +84 -0
  129. package/docs/web-testing/planner.md +122 -0
  130. package/docs/web-testing/rerun.md +164 -0
  131. package/docs/web-testing/researcher.md +380 -0
  132. package/docs/workflow/agentic-usage.md +233 -0
  133. package/docs/workflow/application-spec.md +73 -0
  134. package/docs/workflow/ci.md +202 -0
  135. package/docs/workflow/knowledge.md +310 -0
  136. package/docs/workflow/planning-styles.md +67 -0
  137. package/docs/workflow/reporting.md +133 -0
  138. package/docs/workflow/test-plans.md +90 -0
  139. package/package.json +3 -2
  140. package/rules/navigator/verification-actions.md +20 -0
  141. package/src/action-result.ts +11 -0
  142. package/src/action.ts +43 -3
  143. package/src/ai/captain/web-mode.ts +6 -3
  144. package/src/ai/captain.ts +3 -0
  145. package/src/ai/navigator.ts +255 -186
  146. package/src/ai/pilot.ts +104 -2
  147. package/src/ai/provider.ts +14 -24
  148. package/src/ai/rerunner.ts +2 -1
  149. package/src/ai/researcher/cache.ts +12 -2
  150. package/src/ai/researcher.ts +3 -2
  151. package/src/ai/rules.ts +17 -10
  152. package/src/ai/session-analyst.ts +2 -0
  153. package/src/ai/task-agent.ts +3 -1
  154. package/src/ai/tester.ts +52 -45
  155. package/src/ai/tools.ts +136 -37
  156. package/src/commands/config-command.ts +146 -0
  157. package/src/commands/index.ts +2 -0
  158. package/src/config.ts +60 -5
  159. package/src/execution-controller.ts +8 -0
  160. package/src/explorbot.ts +7 -3
  161. package/src/explorer.ts +2 -2
  162. package/src/playwright-recorder.ts +23 -0
  163. package/src/remote.ts +244 -0
  164. package/src/reporter.ts +7 -1
  165. package/src/state-manager.ts +6 -2
  166. package/src/stats.ts +1 -0
  167. package/src/test-plan.ts +29 -0
  168. package/src/utils/aria.ts +65 -45
  169. package/src/utils/html.ts +1 -0
  170. package/src/utils/logger.ts +33 -2
  171. package/src/utils/page-readiness.ts +24 -1
  172. package/src/utils/url-matcher.ts +3 -0
  173. package/src/utils/web-element.ts +9 -0
  174. package/src/utils/web-sandbox.ts +3 -4
@@ -13,5 +13,15 @@ export function isFunctionExpression(expr: string): { valid: boolean; error?: st
13
13
  }
14
14
 
15
15
  export function toCodeceptWrapper(expr: string): string {
16
- return `I.usePlaywrightTo('pw', async (playwright) => (${expr.trim()})(playwright))`;
16
+ return `return I.usePlaywrightTo('pw', async (playwright) => (${expr.trim()})(playwright))`;
17
+ }
18
+
19
+ export function takePwValue(held: unknown): string | undefined {
20
+ if (held === undefined || held === null) return undefined;
21
+ if (typeof held === 'string') return held;
22
+ try {
23
+ return JSON.stringify(held, null, 2);
24
+ } catch {
25
+ return String(held);
26
+ }
17
27
  }
@@ -20,9 +20,8 @@ export function readDescriptors(dir = registryDir()): PwServerDescriptor[] {
20
20
  return descriptors;
21
21
  }
22
22
 
23
- export function selectDescriptor(descriptors: PwServerDescriptor[], opts: { workspaceDir: string; title?: string }): { match?: PwServerDescriptor; candidates: PwServerDescriptor[] } {
24
- const workspaceDir = path.resolve(opts.workspaceDir);
25
- const candidates = descriptors.filter((descriptor) => path.resolve(descriptor.workspaceDir) === workspaceDir);
23
+ export function selectDescriptor(descriptors: PwServerDescriptor[], opts: { title?: string } = {}): { match?: PwServerDescriptor; candidates: PwServerDescriptor[] } {
24
+ const candidates = descriptors;
26
25
  if (!candidates.length) return { candidates };
27
26
 
28
27
  if (opts.title) {
@@ -53,13 +52,13 @@ function parseDescriptor(file: string): PwServerDescriptor | null {
53
52
  return null;
54
53
  }
55
54
 
56
- if (!data?.endpoint || !data?.title || !data?.workspaceDir) return null;
55
+ if (!data?.endpoint || !data?.title) return null;
57
56
 
58
57
  return {
59
58
  file,
60
59
  title: data.title,
61
60
  endpoint: data.endpoint,
62
- workspaceDir: data.workspaceDir,
61
+ workspaceDir: data.workspaceDir || '',
63
62
  browserName: data.browser?.browserName || DEFAULT_BROWSER,
64
63
  playwrightLib: data.playwrightLib || '',
65
64
  };
@@ -0,0 +1,126 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { outputPath } from '../../../src/config.ts';
4
+ import { safeFilename } from '../../../src/utils/strings.ts';
5
+ import { type EnvelopeData, renderEnvelope } from './envelope.ts';
6
+
7
+ const EXPECTATION_STATUS = { passed: 'passed', failed: 'failed', unverified: 'skipped' };
8
+
9
+ export function sessionsDir(): string {
10
+ return outputPath('prima', 'sessions');
11
+ }
12
+
13
+ export function sessionFile(key: string): string {
14
+ return path.join(sessionsDir(), `${safeFilename(key)}.jsonl`);
15
+ }
16
+
17
+ export function latestSessionFile(): string | null {
18
+ const dir = sessionsDir();
19
+ if (!existsSync(dir)) return null;
20
+
21
+ const files = readdirSync(dir)
22
+ .filter((name) => name.endsWith('.jsonl'))
23
+ .map((name) => path.join(dir, name))
24
+ .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
25
+
26
+ return files[0] || null;
27
+ }
28
+
29
+ export function recordCommand(file: string, session: SessionRun, envelope: EnvelopeData, durationMs: number): void {
30
+ mkdirSync(path.dirname(file), { recursive: true });
31
+ if (recordedEndpoint(file) !== session.endpoint) writeFileSync(file, jsonLine({ action: 'createRun', endpoint: session.endpoint, params: { title: session.title } }), 'utf-8');
32
+ appendFileSync(file, jsonLine({ action: 'addTest', testId: commandTest(envelope, durationMs) }), 'utf-8');
33
+ }
34
+
35
+ export function readSession(file: string): { title: string; tests: any[] } {
36
+ const entries = readEntries(file);
37
+ const run = entries.find((entry) => entry.action === 'createRun');
38
+ const tests = entries.filter((entry) => entry.action === 'addTest').map((entry) => entry.testId);
39
+ return { title: run?.params?.title || 'prima session', tests };
40
+ }
41
+
42
+ function commandTest(envelope: EnvelopeData, durationMs: number): any {
43
+ let status = 'failed';
44
+ if (envelope.ok) status = 'passed';
45
+
46
+ const test: any = {
47
+ rid: envelope.status,
48
+ title: envelope.command,
49
+ suite_title: 'Prima session',
50
+ status,
51
+ file: '<prima>',
52
+ description: [`page: ${envelope.page.url}`, envelope.stepFiles && `artifacts: ${envelope.stepFiles}`].filter(Boolean).join('\n'),
53
+ code: (envelope.used || []).join('\n'),
54
+ steps: commandSteps(envelope),
55
+ logs: renderEnvelope(envelope),
56
+ time: Math.round(durationMs),
57
+ };
58
+
59
+ if (envelope.failure) test.error = { message: envelope.failure.error };
60
+ return test;
61
+ }
62
+
63
+ function commandSteps(envelope: EnvelopeData): any[] {
64
+ const steps: any[] = [];
65
+
66
+ for (const step of envelope.steps || []) {
67
+ const entry: any = { category: 'user', title: step.label, status: 'passed', duration: 0, log: step.proof };
68
+ if (!step.ok) {
69
+ entry.status = 'failed';
70
+ entry.error = step.proof;
71
+ }
72
+ steps.push(entry);
73
+ }
74
+
75
+ for (const expectation of envelope.expectations || []) {
76
+ const entry: any = { category: 'user', title: `expected: ${expectation.text}`, status: EXPECTATION_STATUS[expectation.status], duration: 0 };
77
+ if (expectation.status === 'failed') entry.error = 'the run did not reach this outcome';
78
+ steps.push(entry);
79
+ }
80
+
81
+ for (const assertion of envelope.assertions || []) {
82
+ const entry: any = { category: 'user', title: assertion.code, status: 'passed', duration: 0, log: assertion.proof.join('\n') };
83
+ if (!assertion.passed) {
84
+ entry.status = 'failed';
85
+ entry.error = 'the assertion did not hold on the page';
86
+ }
87
+ steps.push(entry);
88
+ }
89
+
90
+ return steps;
91
+ }
92
+
93
+ function recordedEndpoint(file: string): string | null {
94
+ const [first] = readEntries(file);
95
+ return first?.endpoint || null;
96
+ }
97
+
98
+ function readEntries(file: string): any[] {
99
+ if (!existsSync(file)) return [];
100
+
101
+ const entries: any[] = [];
102
+ for (const line of readFileSync(file, 'utf-8').split('\n')) {
103
+ if (!line.trim()) continue;
104
+ const entry = parseLine(line);
105
+ if (entry) entries.push(entry);
106
+ }
107
+ return entries;
108
+ }
109
+
110
+ function parseLine(line: string): any {
111
+ try {
112
+ return JSON.parse(line);
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function jsonLine(entry: unknown): string {
119
+ return `${JSON.stringify(entry)}\n`;
120
+ }
121
+
122
+ export interface SessionRun {
123
+ key: string;
124
+ endpoint: string;
125
+ title: string;
126
+ }
@@ -11,10 +11,11 @@ import { App } from '../src/components/App.js';
11
11
  import { StatusPane } from '../src/components/StatusPane.js';
12
12
  import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
13
13
  import { ExplorBot } from '../src/explorbot.js';
14
+ import { remote } from '../src/remote.js';
14
15
  import { Stats } from '../src/stats.js';
15
16
  import { Plan } from '../src/test-plan.js';
16
17
  import { getCliName } from "../src/utils/cli-name.js";
17
- import { log, setPreserveConsoleLogs } from '../src/utils/logger.js';
18
+ import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode } from '../src/utils/logger.js';
18
19
  import { jsonToTable } from '../src/utils/markdown-parser.js';
19
20
  import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
20
21
  import { printNextSteps, relativeToCwd } from "../src/utils/next-steps.js";
@@ -23,7 +24,8 @@ const cli = getCliName();
23
24
  const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
24
25
  const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
25
26
  program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
26
- if (!process.env.EXPLORBOT_NO_BANNER) {
27
+ remote.registerOption(program);
28
+ if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
27
29
  console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
28
30
  }
29
31
  function buildExplorBotOptions(from, options) {
@@ -74,6 +76,10 @@ async function startTUI(explorBot) {
74
76
  });
75
77
  }
76
78
  async function showStatsAndExit(code) {
79
+ if (remote.isAttached()) {
80
+ await remote.close(code);
81
+ process.exit(code);
82
+ }
77
83
  if (Stats.hasActivity()) {
78
84
  await new Promise((resolve) => {
79
85
  const { unmount } = render(React.createElement(StatusPane, {
@@ -380,6 +386,23 @@ addCommonOptions(program
380
386
  const cmd = new FreesailCommand(explorBot);
381
387
  await cmd.execute(args);
382
388
  });
389
+ program
390
+ .command('config [url]')
391
+ .description('Show models, config file and paths used by this run')
392
+ .option('-c, --config <path>', 'Path to configuration file')
393
+ .option('-p, --path <path>', 'Working directory path')
394
+ .option('--json', 'Print the resolved config as JSON')
395
+ .action(async (url, options) => {
396
+ setQuietMode(!isVerboseMode());
397
+ const { ConfigCommand } = await import('../src/commands/config-command.js');
398
+ try {
399
+ console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
400
+ }
401
+ catch (error) {
402
+ console.error(error instanceof Error ? error.message : 'Unknown error');
403
+ process.exit(1);
404
+ }
405
+ });
383
406
  program
384
407
  .command('init')
385
408
  .description('Initialize configuration for a project or for this machine')
@@ -820,10 +843,5 @@ ${rows}
820
843
  ${cli} explore /login --max-tests 3
821
844
  `;
822
845
  };
823
- const addEnvHelp = (cmd) => {
824
- cmd.addHelpText('after', envHelp);
825
- for (const sub of cmd.commands)
826
- addEnvHelp(sub);
827
- };
828
- addEnvHelp(program);
846
+ program.addHelpText('after', envHelp);
829
847
  program.parse();
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env bun
2
+ import { remote } from "../../../src/remote.js";
2
3
  import { createApiCommands } from "../src/cli.js";
3
4
  const program = createApiCommands('apibot');
5
+ remote.registerOption(program);
4
6
  program.parse();
@@ -1,9 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Command } from 'commander';
4
+ import { ConfigCommand } from "../../../src/commands/config-command.js";
5
+ import { listSites } from "../../../src/global-config.js";
4
6
  import { setPreserveConsoleLogs } from "../../../src/utils/logger.js";
5
7
  import { getStyles } from "./ai/chief/styles.js";
6
8
  import { ApiBot } from "./apibot.js";
9
+ import { ApibotConfigParser } from "./config.js";
7
10
  function buildOptions(options) {
8
11
  return {
9
12
  verbose: options.verbose || options.debug,
@@ -68,6 +71,20 @@ export function createApiCommands(name = 'api') {
68
71
  process.exit(1);
69
72
  }
70
73
  });
74
+ addCommonOptions(cmd.command('config [endpoint]').description('Show models, config file and paths used by this run'))
75
+ .option('--json', 'Print the resolved config as JSON')
76
+ .action(async (endpoint, options) => {
77
+ const parser = ApibotConfigParser.getInstance();
78
+ const [site] = listSites();
79
+ try {
80
+ const config = await parser.loadConfig({ config: options.config, path: options.path, endpoint: endpoint || site?.url });
81
+ console.log(ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json }));
82
+ }
83
+ catch (error) {
84
+ console.error(error instanceof Error ? error.message : 'Unknown error');
85
+ process.exit(1);
86
+ }
87
+ });
71
88
  addCommonOptions(cmd.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1-3, *')).action(async (planfile, index, options) => {
72
89
  setPreserveConsoleLogs(true);
73
90
  try {
@@ -8,6 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import { existsSync, mkdirSync, readFileSync } from 'node:fs';
10
10
  import path, { resolve } from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
11
12
  import { parseEnv } from 'node:util';
12
13
  import { ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot } from "../../../src/config.js";
13
14
  import { findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from "../../../src/global-config.js";
@@ -181,9 +182,10 @@ export class ApibotConfigParser {
181
182
  }
182
183
  async loadConfigModule(configPath) {
183
184
  const ext = configPath.split('.').pop();
185
+ const moduleUrl = pathToFileURL(resolve(configPath)).href;
184
186
  if (ext === 'ts') {
185
187
  try {
186
- return await import(__rewriteRelativeImportExtension(configPath));
188
+ return await import(__rewriteRelativeImportExtension(moduleUrl));
187
189
  }
188
190
  catch {
189
191
  const require = (await import('node:module')).createRequire(import.meta.url);
@@ -191,7 +193,7 @@ export class ApibotConfigParser {
191
193
  }
192
194
  }
193
195
  if (ext === 'js' || ext === 'mjs') {
194
- return await import(__rewriteRelativeImportExtension(configPath));
196
+ return await import(__rewriteRelativeImportExtension(moduleUrl));
195
197
  }
196
198
  const content = readFileSync(configPath, 'utf8');
197
199
  return JSON.parse(content);
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env bun
2
+ import { remote } from "../../../src/remote.js";
2
3
  import { createDocsCommands } from "../src/cli.js";
3
4
  const program = createDocsCommands('doc-collector');
5
+ remote.registerOption(program);
4
6
  program.parse();
@@ -55,13 +55,22 @@ class Documentarian {
55
55
  const message = error instanceof Error ? error.message : String(error);
56
56
  tag('warning').log(`Interactive documentation failed: ${message}.`);
57
57
  if (meaningfulInteractions.length > 0) {
58
- tag('info').log(`Preserving ${meaningfulInteractions.length} observed interaction(s) without AI summary.`);
59
- return this.normalizeDocumentation({
60
- summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
61
- can: [],
62
- might: [],
58
+ tag('info').log(`Retrying static documentation while preserving ${meaningfulInteractions.length} observed interaction(s).`);
59
+ return this.documentStatic(state, research)
60
+ .then((documentation) => this.normalizeDocumentation({
61
+ ...documentation,
63
62
  interactions: meaningfulInteractions,
64
- }, state, research);
63
+ }, state, research))
64
+ .catch((fallbackError) => {
65
+ const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
66
+ tag('warning').log(`Static documentation fallback failed: ${fallbackMessage}. Preserving observed interactions without AI summary.`);
67
+ return this.normalizeDocumentation({
68
+ summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
69
+ can: [],
70
+ might: [],
71
+ interactions: meaningfulInteractions,
72
+ }, state, research);
73
+ });
65
74
  }
66
75
  return this.documentStatic(state, research);
67
76
  }
@@ -94,7 +103,7 @@ class Documentarian {
94
103
  content: this.buildPrompt(state, `${research}${this.buildInteractionContext(interactions)}`),
95
104
  },
96
105
  ];
97
- const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
106
+ const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
98
107
  agentName: 'documentarian',
99
108
  });
100
109
  return this.normalizeDocumentation({
@@ -113,7 +122,7 @@ class Documentarian {
113
122
  content: this.buildPrompt(state, research, simplified),
114
123
  },
115
124
  ];
116
- const response = await this.provider.generateObject(messages, pageDocumentationSchema, undefined, {
125
+ const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
117
126
  agentName: 'documentarian',
118
127
  });
119
128
  return this.normalizeDocumentation(response.object, state, research);
@@ -230,10 +239,10 @@ class Documentarian {
230
239
  normalized.interactions = undefined;
231
240
  }
232
241
  const qualityNotes = this.evaluateDocumentationQuality(normalized);
233
- return {
242
+ return pageDocumentationSchema.parse({
234
243
  ...normalized,
235
244
  qualityNotes,
236
- };
245
+ });
237
246
  }
238
247
  evaluateDocumentationQuality(documentation) {
239
248
  const notes = [];
@@ -296,30 +305,46 @@ const stateTransitionSchema = z.object({
296
305
  action: z.string(),
297
306
  before: z.string(),
298
307
  after: z.string(),
299
- targetUrl: z.string().nullable(),
300
- discoveredUrls: z.array(z.string()).nullable(),
301
- newCapabilities: z.array(z.string()).nullable(),
308
+ targetUrl: z.string().optional(),
309
+ discoveredUrls: z.array(z.string()).optional(),
310
+ newCapabilities: z.array(z.string()).optional(),
302
311
  element: z
303
312
  .object({
304
313
  role: z.string(),
305
314
  name: z.string(),
306
315
  section: z.string(),
307
- container: z.string().nullable(),
308
- locator: z.string().nullable(),
316
+ container: z.string().optional(),
317
+ locator: z.string().optional(),
309
318
  })
310
- .nullable(),
319
+ .optional(),
311
320
  changes: z
312
321
  .object({
313
322
  urlChanged: z.boolean(),
314
323
  newElements: z.number(),
315
324
  removedElements: z.number(),
316
325
  })
317
- .nullable(),
326
+ .optional(),
327
+ targetState: z
328
+ .object({
329
+ kind: z.enum(['page', 'dialog', 'modal', 'section']),
330
+ label: z.string(),
331
+ url: z.string(),
332
+ })
333
+ .optional(),
334
+ screenshot: z
335
+ .object({
336
+ title: z.string(),
337
+ relativePath: z.string(),
338
+ })
339
+ .optional(),
318
340
  });
319
- const pageDocumentationSchema = z.object({
341
+ const generatedPageDocumentationSchema = z.object({
320
342
  summary: z.string(),
321
343
  can: z.array(capabilitySchema),
322
344
  might: z.array(capabilitySchema),
323
- interactions: z.array(stateTransitionSchema).nullable(),
345
+ });
346
+ const pageDocumentationSchema = generatedPageDocumentationSchema.extend({
347
+ interactions: z.array(stateTransitionSchema).optional(),
348
+ qualityNotes: z.array(z.string()).optional(),
324
349
  });
325
350
  export { Documentarian };
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Command } from 'commander';
4
- import { setPreserveConsoleLogs } from "../../../src/utils/logger.js";
4
+ import { ConfigCommand } from "../../../src/commands/config-command.js";
5
+ import { isVerboseMode, setPreserveConsoleLogs, setQuietMode } from "../../../src/utils/logger.js";
5
6
  import { DocBot } from "./docbot.js";
6
7
  function buildOptions(options) {
7
8
  return {
@@ -56,6 +57,18 @@ export function createDocsCommands(name = 'docs') {
56
57
  process.exit(1);
57
58
  }
58
59
  });
60
+ addCommonOptions(cmd.command('config [url]').description('Show models, config file and paths used by this run'))
61
+ .option('--json', 'Print the resolved config as JSON')
62
+ .action(async (url, options) => {
63
+ setQuietMode(!isVerboseMode());
64
+ try {
65
+ console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
66
+ }
67
+ catch (error) {
68
+ console.error(error instanceof Error ? error.message : 'Unknown error');
69
+ process.exit(1);
70
+ }
71
+ });
59
72
  cmd
60
73
  .command('init')
61
74
  .description('Initialize doc collector configuration')
@@ -8,6 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  };
9
9
  import { existsSync, readFileSync } from 'node:fs';
10
10
  import path, { resolve } from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
11
12
  import { parseEnv } from 'node:util';
12
13
  import { ConfigParser } from "../../../src/config.js";
13
14
  class DocbotConfigParser {
@@ -84,9 +85,10 @@ class DocbotConfigParser {
84
85
  }
85
86
  async loadConfigModule(configPath) {
86
87
  const ext = configPath.split('.').pop();
88
+ const moduleUrl = pathToFileURL(resolve(configPath)).href;
87
89
  if (ext === 'ts') {
88
90
  try {
89
- return await import(__rewriteRelativeImportExtension(configPath));
91
+ return await import(__rewriteRelativeImportExtension(moduleUrl));
90
92
  }
91
93
  catch {
92
94
  const require = (await import('node:module')).createRequire(import.meta.url);
@@ -94,7 +96,7 @@ class DocbotConfigParser {
94
96
  }
95
97
  }
96
98
  if (ext === 'js' || ext === 'mjs') {
97
- return await import(__rewriteRelativeImportExtension(configPath));
99
+ return await import(__rewriteRelativeImportExtension(moduleUrl));
98
100
  }
99
101
  return JSON.parse(readFileSync(configPath, 'utf8'));
100
102
  }
@@ -0,0 +1,30 @@
1
+ import chalk from 'chalk';
2
+ import { addActivityListener, removeActivityListener } from "../../../src/activity.js";
3
+ import { isVerboseMode } from "../../../src/utils/logger.js";
4
+ const RESET_LINE = '\r\u001b[2K';
5
+ const stream = process.stderr;
6
+ let tracking = false;
7
+ export function trackActivityLine() {
8
+ if (tracking)
9
+ return;
10
+ if (!stream.isTTY)
11
+ return;
12
+ if (isVerboseMode())
13
+ return;
14
+ tracking = true;
15
+ addActivityListener(writeActivityLine);
16
+ }
17
+ export function clearActivityLine() {
18
+ if (!tracking)
19
+ return;
20
+ tracking = false;
21
+ removeActivityListener(writeActivityLine);
22
+ stream.write(RESET_LINE);
23
+ }
24
+ function writeActivityLine(activity) {
25
+ if (!activity)
26
+ return;
27
+ const width = (stream.columns || 80) - 2;
28
+ const message = Array.from(activity.message.replace(/\s+/g, ' ').trim()).slice(0, width).join('');
29
+ stream.write(`${RESET_LINE}${chalk.gray(message)}`);
30
+ }