explorbot 0.4.6 → 0.4.7
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/boat/api-tester/src/ai/curler.ts +70 -66
- package/boat/api-tester/src/apibot.ts +1 -0
- package/boat/api-tester/src/cli.ts +2 -0
- package/boat/api-tester/src/config.ts +18 -1
- package/dist/boat/api-tester/src/ai/curler.js +55 -56
- package/dist/boat/api-tester/src/apibot.js +1 -0
- package/dist/boat/api-tester/src/cli.js +2 -0
- package/dist/boat/api-tester/src/config.js +3 -1
- package/dist/package.json +2 -2
- package/dist/rules/researcher/pagination.md +6 -0
- package/dist/src/action-result.d.ts +6 -0
- package/dist/src/action-result.js +12 -0
- package/dist/src/ai/planner.js +4 -0
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/pagination.d.ts +16 -0
- package/dist/src/ai/researcher/pagination.js +62 -0
- package/dist/src/ai/researcher/parser.d.ts +3 -0
- package/dist/src/ai/researcher/parser.js +22 -6
- package/dist/src/ai/researcher/sections.js +1 -1
- package/dist/src/ai/researcher.js +7 -2
- package/dist/src/ai/rules.js +16 -0
- package/dist/src/ai/scout.js +8 -2
- package/dist/src/ai/tools.js +10 -3
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +14 -0
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +14 -11
- package/dist/src/remote.d.ts +2 -0
- package/dist/src/remote.js +23 -16
- package/dist/src/utils/aria.d.ts +2 -0
- package/dist/src/utils/aria.js +6 -1
- package/dist/src/utils/markdown-query.d.ts +2 -0
- package/dist/src/utils/markdown-query.js +39 -0
- package/dist/src/utils/pagination.d.ts +16 -0
- package/dist/src/utils/pagination.js +20 -0
- package/docs/superpowers/plans/2026-09-10-pagination.md +1420 -0
- package/docs/superpowers/specs/2026-09-09-pagination-rule-design.md +125 -97
- package/package.json +2 -2
- package/rules/researcher/pagination.md +6 -0
- package/src/action-result.ts +16 -0
- package/src/ai/planner.ts +4 -0
- package/src/ai/researcher/locators.ts +1 -1
- package/src/ai/researcher/pagination.ts +68 -0
- package/src/ai/researcher/parser.ts +23 -5
- package/src/ai/researcher/sections.ts +1 -1
- package/src/ai/researcher.ts +9 -3
- package/src/ai/rules.ts +16 -0
- package/src/ai/scout.ts +9 -2
- package/src/ai/tools.ts +7 -3
- package/src/commands/options/ws-option.ts +14 -0
- package/src/config.ts +15 -11
- package/src/remote.ts +22 -15
- package/src/utils/aria.ts +8 -1
- package/src/utils/markdown-query.ts +39 -0
- package/src/utils/pagination.ts +36 -0
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import type { AIProvider } from '../../../../src/ai/provider.ts';
|
|
4
4
|
import type { RequestStore } from '../../../../src/api/request-store.ts';
|
|
5
5
|
import type { KnowledgeTracker } from '../../../../src/knowledge-tracker.ts';
|
|
6
|
+
import { Observability } from '../../../../src/observability.ts';
|
|
6
7
|
import type { Reporter } from '../../../../src/reporter.ts';
|
|
7
8
|
import { type Test, TestResult } from '../../../../src/test-plan.ts';
|
|
8
9
|
import { createDebug, tag } from '../../../../src/utils/logger.ts';
|
|
@@ -46,75 +47,77 @@ export class Curler {
|
|
|
46
47
|
const initialPrompt = this.buildTestPrompt(test, opts?.specDefinition, opts?.baseEndpoint);
|
|
47
48
|
conversation.addUserText(initialPrompt);
|
|
48
49
|
|
|
49
|
-
await
|
|
50
|
-
|
|
51
|
-
debugLog(`Iteration ${iteration}`);
|
|
52
|
-
|
|
53
|
-
if (iteration > 1) {
|
|
54
|
-
const requestLog = this.requestState.toLog();
|
|
55
|
-
const nextStep = dedent`
|
|
56
|
-
<request_log>
|
|
57
|
-
${requestLog || 'No requests made yet'}
|
|
58
|
-
</request_log>
|
|
59
|
-
|
|
60
|
-
<task>
|
|
61
|
-
Continue testing. Review the request log above and proceed with the next step.
|
|
62
|
-
</task>
|
|
63
|
-
|
|
64
|
-
<notes>
|
|
65
|
-
${test.notesToString() || 'No notes yet'}
|
|
66
|
-
</notes>
|
|
67
|
-
`;
|
|
68
|
-
conversation.addUserText(nextStep);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const result = await this.provider.invokeConversation(conversation, tools, {
|
|
72
|
-
maxToolRoundtrips: 5,
|
|
73
|
-
toolChoice: 'required',
|
|
74
|
-
agentName: 'curler',
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (!result) throw new Error('Failed to get response from provider');
|
|
78
|
-
|
|
79
|
-
const toolNames = result.toolExecutions?.map((e: any) => e.toolName) || [];
|
|
80
|
-
debugLog('Tool calls:', toolNames.join(', '));
|
|
81
|
-
|
|
82
|
-
if (test.hasFinished) {
|
|
83
|
-
stop();
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (iteration >= MAX_ITERATIONS) {
|
|
88
|
-
tag('warning').log('Max iterations reached, running final review...');
|
|
89
|
-
stop();
|
|
90
|
-
}
|
|
91
|
-
},
|
|
50
|
+
await Observability.run(
|
|
51
|
+
`curler: ${test.scenario}`,
|
|
92
52
|
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
input: {
|
|
100
|
-
scenario: test.scenario,
|
|
101
|
-
startUrl: test.startUrl,
|
|
102
|
-
expected: test.expected,
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
catch: async ({ error, stop }) => {
|
|
107
|
-
tag('error').log(`Test execution error: ${error}`);
|
|
108
|
-
stop();
|
|
53
|
+
sessionId: test.sessionName,
|
|
54
|
+
tags: ['curler'],
|
|
55
|
+
input: {
|
|
56
|
+
scenario: test.scenario,
|
|
57
|
+
startUrl: test.startUrl,
|
|
58
|
+
expected: test.expected,
|
|
109
59
|
},
|
|
60
|
+
},
|
|
61
|
+
async () => {
|
|
62
|
+
await loop(
|
|
63
|
+
async ({ stop, iteration }) => {
|
|
64
|
+
debugLog(`Iteration ${iteration}`);
|
|
65
|
+
|
|
66
|
+
if (iteration > 1) {
|
|
67
|
+
const requestLog = this.requestState.toLog();
|
|
68
|
+
const nextStep = dedent`
|
|
69
|
+
<request_log>
|
|
70
|
+
${requestLog || 'No requests made yet'}
|
|
71
|
+
</request_log>
|
|
72
|
+
|
|
73
|
+
<task>
|
|
74
|
+
Continue testing. Review the request log above and proceed with the next step.
|
|
75
|
+
</task>
|
|
76
|
+
|
|
77
|
+
<notes>
|
|
78
|
+
${test.notesToString() || 'No notes yet'}
|
|
79
|
+
</notes>
|
|
80
|
+
`;
|
|
81
|
+
conversation.addUserText(nextStep);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = await this.provider.invokeConversation(conversation, tools, {
|
|
85
|
+
maxToolRoundtrips: 5,
|
|
86
|
+
toolChoice: 'required',
|
|
87
|
+
agentName: 'curler',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!result) throw new Error('Failed to get response from provider');
|
|
91
|
+
|
|
92
|
+
const toolNames = result.toolExecutions?.map((e: any) => e.toolName) || [];
|
|
93
|
+
debugLog('Tool calls:', toolNames.join(', '));
|
|
94
|
+
|
|
95
|
+
if (test.hasFinished) {
|
|
96
|
+
stop();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (iteration >= MAX_ITERATIONS) {
|
|
101
|
+
tag('warning').log('Max iterations reached, running final review...');
|
|
102
|
+
stop();
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
maxAttempts: MAX_ITERATIONS,
|
|
107
|
+
catch: async ({ error, stop }) => {
|
|
108
|
+
tag('error').log(`Test execution error: ${error}`);
|
|
109
|
+
stop();
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await this.finalReview(test);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
tag('error').log(`Final review failed: ${error}`);
|
|
118
|
+
}
|
|
110
119
|
}
|
|
111
120
|
);
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
await this.finalReview(test);
|
|
115
|
-
} catch (error) {
|
|
116
|
-
tag('error').log(`Final review failed: ${error}`);
|
|
117
|
-
}
|
|
118
121
|
this.finishTest(test);
|
|
119
122
|
const meta: Record<string, string | undefined> = {
|
|
120
123
|
endpoint: test.startUrl,
|
|
@@ -190,7 +193,8 @@ export class Curler {
|
|
|
190
193
|
},
|
|
191
194
|
],
|
|
192
195
|
schema,
|
|
193
|
-
model
|
|
196
|
+
model,
|
|
197
|
+
{ agentName: 'curler', telemetryFunctionId: 'curler.finalReview' }
|
|
194
198
|
);
|
|
195
199
|
|
|
196
200
|
const result = response?.object;
|
|
@@ -85,6 +85,7 @@ export class ApiBot {
|
|
|
85
85
|
async stop(): Promise<void> {
|
|
86
86
|
await this.reporter?.finishRun();
|
|
87
87
|
await this.apiClient?.teardown();
|
|
88
|
+
await this.provider?.stop();
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
createAgent<T>(factory: (deps: { ai: AIProvider; config: ApibotConfig; apiClient: ApiClient; requestState: RequestStore; knowledge: KnowledgeTracker }) => T): T {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { flushTelemetry } from '../../../src/ai/provider.ts';
|
|
2
3
|
import { ConfigCommand } from '../../../src/commands/config-command.ts';
|
|
3
4
|
import { RecommendedModelsCommand } from '../../../src/commands/recommended-models-command.ts';
|
|
4
5
|
import { listSites } from '../../../src/global-config.ts';
|
|
@@ -111,6 +112,7 @@ async function run(name: string, options: any, endpoint: string | undefined, bod
|
|
|
111
112
|
process.exit(code);
|
|
112
113
|
} catch (error) {
|
|
113
114
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
115
|
+
await flushTelemetry();
|
|
114
116
|
process.exit(1);
|
|
115
117
|
}
|
|
116
118
|
}
|
|
@@ -2,7 +2,22 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import path, { resolve } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { parseEnv } from 'node:util';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type AIConfig,
|
|
7
|
+
type ApiHookFn,
|
|
8
|
+
type ApiConfig as BaseApiConfig,
|
|
9
|
+
ConfigMissingError,
|
|
10
|
+
EXPLORBOT_CONFIG_PATHS,
|
|
11
|
+
createModel,
|
|
12
|
+
envConfigRequested,
|
|
13
|
+
materializeKnowledge,
|
|
14
|
+
missingConfigMessage,
|
|
15
|
+
resolveConfigModels,
|
|
16
|
+
resolveLangfuse,
|
|
17
|
+
resolveModel,
|
|
18
|
+
resolveOutputRoot,
|
|
19
|
+
setOutputDir,
|
|
20
|
+
} from '../../../src/config.ts';
|
|
6
21
|
import { type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from '../../../src/global-config.ts';
|
|
7
22
|
|
|
8
23
|
export type { AIConfig };
|
|
@@ -101,6 +116,7 @@ export class ApibotConfigParser {
|
|
|
101
116
|
this.applyEnvHeaders(this.config.api);
|
|
102
117
|
if (options?.baseEndpoint) this.config.api.baseEndpoint = options.baseEndpoint.replace(/\/$/, '');
|
|
103
118
|
await resolveConfigModels(this.config.ai);
|
|
119
|
+
resolveLangfuse(this.config.ai);
|
|
104
120
|
this.configPath = resolvedPath;
|
|
105
121
|
this.site = null;
|
|
106
122
|
|
|
@@ -236,6 +252,7 @@ export class ApibotConfigParser {
|
|
|
236
252
|
api,
|
|
237
253
|
dirs: { output: '.', knowledge: 'knowledge' },
|
|
238
254
|
};
|
|
255
|
+
resolveLangfuse(this.config.ai);
|
|
239
256
|
this.configPath = path.join(outputRoot, 'apibot.config.js');
|
|
240
257
|
this.validateConfig(this.config);
|
|
241
258
|
setOutputDir(this.getOutputDir());
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { Observability } from "../../../../src/observability.js";
|
|
3
4
|
import { TestResult } from "../../../../src/test-plan.js";
|
|
4
5
|
import { createDebug, tag } from "../../../../src/utils/logger.js";
|
|
5
6
|
import { loop } from "../../../../src/utils/loop.js";
|
|
@@ -32,67 +33,65 @@ export class Curler {
|
|
|
32
33
|
conversation.addUserText(knowledge);
|
|
33
34
|
const initialPrompt = this.buildTestPrompt(test, opts?.specDefinition, opts?.baseEndpoint);
|
|
34
35
|
conversation.addUserText(initialPrompt);
|
|
35
|
-
await
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
await Observability.run(`curler: ${test.scenario}`, {
|
|
37
|
+
sessionId: test.sessionName,
|
|
38
|
+
tags: ['curler'],
|
|
39
|
+
input: {
|
|
40
|
+
scenario: test.scenario,
|
|
41
|
+
startUrl: test.startUrl,
|
|
42
|
+
expected: test.expected,
|
|
43
|
+
},
|
|
44
|
+
}, async () => {
|
|
45
|
+
await loop(async ({ stop, iteration }) => {
|
|
46
|
+
debugLog(`Iteration ${iteration}`);
|
|
47
|
+
if (iteration > 1) {
|
|
48
|
+
const requestLog = this.requestState.toLog();
|
|
49
|
+
const nextStep = dedent `
|
|
50
|
+
<request_log>
|
|
51
|
+
${requestLog || 'No requests made yet'}
|
|
52
|
+
</request_log>
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
<task>
|
|
55
|
+
Continue testing. Review the request log above and proceed with the next step.
|
|
56
|
+
</task>
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
<notes>
|
|
59
|
+
${test.notesToString() || 'No notes yet'}
|
|
60
|
+
</notes>
|
|
61
|
+
`;
|
|
62
|
+
conversation.addUserText(nextStep);
|
|
63
|
+
}
|
|
64
|
+
const result = await this.provider.invokeConversation(conversation, tools, {
|
|
65
|
+
maxToolRoundtrips: 5,
|
|
66
|
+
toolChoice: 'required',
|
|
67
|
+
agentName: 'curler',
|
|
68
|
+
});
|
|
69
|
+
if (!result)
|
|
70
|
+
throw new Error('Failed to get response from provider');
|
|
71
|
+
const toolNames = result.toolExecutions?.map((e) => e.toolName) || [];
|
|
72
|
+
debugLog('Tool calls:', toolNames.join(', '));
|
|
73
|
+
if (test.hasFinished) {
|
|
74
|
+
stop();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (iteration >= MAX_ITERATIONS) {
|
|
78
|
+
tag('warning').log('Max iterations reached, running final review...');
|
|
79
|
+
stop();
|
|
80
|
+
}
|
|
81
|
+
}, {
|
|
82
|
+
maxAttempts: MAX_ITERATIONS,
|
|
83
|
+
catch: async ({ error, stop }) => {
|
|
84
|
+
tag('error').log(`Test execution error: ${error}`);
|
|
85
|
+
stop();
|
|
86
|
+
},
|
|
58
87
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const toolNames = result.toolExecutions?.map((e) => e.toolName) || [];
|
|
62
|
-
debugLog('Tool calls:', toolNames.join(', '));
|
|
63
|
-
if (test.hasFinished) {
|
|
64
|
-
stop();
|
|
65
|
-
return;
|
|
88
|
+
try {
|
|
89
|
+
await this.finalReview(test);
|
|
66
90
|
}
|
|
67
|
-
|
|
68
|
-
tag('
|
|
69
|
-
stop();
|
|
91
|
+
catch (error) {
|
|
92
|
+
tag('error').log(`Final review failed: ${error}`);
|
|
70
93
|
}
|
|
71
|
-
}, {
|
|
72
|
-
maxAttempts: MAX_ITERATIONS,
|
|
73
|
-
observability: {
|
|
74
|
-
name: `curler: ${test.scenario}`,
|
|
75
|
-
agent: 'curler',
|
|
76
|
-
sessionId: test.sessionName,
|
|
77
|
-
metadata: {
|
|
78
|
-
input: {
|
|
79
|
-
scenario: test.scenario,
|
|
80
|
-
startUrl: test.startUrl,
|
|
81
|
-
expected: test.expected,
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
catch: async ({ error, stop }) => {
|
|
86
|
-
tag('error').log(`Test execution error: ${error}`);
|
|
87
|
-
stop();
|
|
88
|
-
},
|
|
89
94
|
});
|
|
90
|
-
try {
|
|
91
|
-
await this.finalReview(test);
|
|
92
|
-
}
|
|
93
|
-
catch (error) {
|
|
94
|
-
tag('error').log(`Final review failed: ${error}`);
|
|
95
|
-
}
|
|
96
95
|
this.finishTest(test);
|
|
97
96
|
const meta = {
|
|
98
97
|
endpoint: test.startUrl,
|
|
@@ -160,7 +159,7 @@ export class Curler {
|
|
|
160
159
|
3. Should the test pass or fail overall?
|
|
161
160
|
`,
|
|
162
161
|
},
|
|
163
|
-
], schema, model);
|
|
162
|
+
], schema, model, { agentName: 'curler', telemetryFunctionId: 'curler.finalReview' });
|
|
164
163
|
const result = response?.object;
|
|
165
164
|
if (!result)
|
|
166
165
|
return;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { flushTelemetry } from "../../../src/ai/provider.js";
|
|
2
3
|
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
3
4
|
import { RecommendedModelsCommand } from "../../../src/commands/recommended-models-command.js";
|
|
4
5
|
import { listSites } from "../../../src/global-config.js";
|
|
@@ -108,6 +109,7 @@ async function run(name, options, endpoint, body) {
|
|
|
108
109
|
}
|
|
109
110
|
catch (error) {
|
|
110
111
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
112
|
+
await flushTelemetry();
|
|
111
113
|
process.exit(1);
|
|
112
114
|
}
|
|
113
115
|
}
|
|
@@ -10,7 +10,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
10
10
|
import path, { resolve } from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { parseEnv } from 'node:util';
|
|
13
|
-
import { ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot, setOutputDir } from "../../../src/config.js";
|
|
13
|
+
import { ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveLangfuse, resolveModel, resolveOutputRoot, setOutputDir, } from "../../../src/config.js";
|
|
14
14
|
import { findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from "../../../src/global-config.js";
|
|
15
15
|
function isAbsoluteEndpoint(value) {
|
|
16
16
|
return !!value && (value.startsWith('http://') || value.startsWith('https://'));
|
|
@@ -82,6 +82,7 @@ export class ApibotConfigParser {
|
|
|
82
82
|
if (options?.baseEndpoint)
|
|
83
83
|
this.config.api.baseEndpoint = options.baseEndpoint.replace(/\/$/, '');
|
|
84
84
|
await resolveConfigModels(this.config.ai);
|
|
85
|
+
resolveLangfuse(this.config.ai);
|
|
85
86
|
this.configPath = resolvedPath;
|
|
86
87
|
this.site = null;
|
|
87
88
|
if (isGlobalConfigPath(resolvedPath)) {
|
|
@@ -208,6 +209,7 @@ export class ApibotConfigParser {
|
|
|
208
209
|
api,
|
|
209
210
|
dirs: { output: '.', knowledge: 'knowledge' },
|
|
210
211
|
};
|
|
212
|
+
resolveLangfuse(this.config.ai);
|
|
211
213
|
this.configPath = path.join(outputRoot, 'apibot.config.js');
|
|
212
214
|
this.validateConfig(this.config);
|
|
213
215
|
setOutputDir(this.getOutputDir());
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"bash-tool": "^1.3.15",
|
|
102
102
|
"chalk": "^5.6.2",
|
|
103
103
|
"cli-highlight": "^2.1.11",
|
|
104
|
-
"codeceptjs": "4.2.0-beta.
|
|
104
|
+
"codeceptjs": "^4.2.0-beta.3",
|
|
105
105
|
"commander": "^14.0.1",
|
|
106
106
|
"debug": "^4.4.3",
|
|
107
107
|
"dedent": "^1.6.0",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<pagination>
|
|
2
|
+
When a section is a list that continues beyond what is shown, add one line under its `> Container:` line:
|
|
3
|
+
`> Pagination: controls` — it has page numbers (1, 2, 3), prev/next arrows, or a "load more" button.
|
|
4
|
+
`> Pagination: infinite` — it has none of those and loads more as it is scrolled.
|
|
5
|
+
Sorting, filtering and switching tabs are not pagination — omit the line then.
|
|
6
|
+
</pagination>
|
|
@@ -37,6 +37,8 @@ export interface PageDiff {
|
|
|
37
37
|
currentUrl: string;
|
|
38
38
|
ariaChanges?: string | null;
|
|
39
39
|
ariaChangeCount?: number;
|
|
40
|
+
ariaAdded?: number;
|
|
41
|
+
ariaRemoved?: number;
|
|
40
42
|
messages?: string[];
|
|
41
43
|
requests?: NetworkCall[];
|
|
42
44
|
consoleErrors?: string[];
|
|
@@ -137,6 +139,8 @@ export declare class Diff {
|
|
|
137
139
|
_messages: string[];
|
|
138
140
|
_ariaDiffResult: string | null;
|
|
139
141
|
_ariaChangeCount: number;
|
|
142
|
+
_ariaAdded: number;
|
|
143
|
+
_ariaRemoved: number;
|
|
140
144
|
_isSameUrl: boolean;
|
|
141
145
|
constructor(current: ActionResult, previous: ActionResult | null);
|
|
142
146
|
static create(current: ActionResult, previous: ActionResult | null): Promise<Diff>;
|
|
@@ -147,6 +151,8 @@ export declare class Diff {
|
|
|
147
151
|
cleanedHtmlParts(): Promise<HtmlDiffPart[]>;
|
|
148
152
|
get ariaChanged(): string | null;
|
|
149
153
|
get ariaChangeCount(): number;
|
|
154
|
+
get ariaAdded(): number;
|
|
155
|
+
get ariaRemoved(): number;
|
|
150
156
|
get htmlDiff(): HtmlDiffResult | null;
|
|
151
157
|
get messages(): string[];
|
|
152
158
|
get similarity(): number;
|
|
@@ -451,6 +451,8 @@ export class ActionResult {
|
|
|
451
451
|
if (diff.ariaChanged) {
|
|
452
452
|
pageDiff.ariaChanges = diff.ariaChanged;
|
|
453
453
|
pageDiff.ariaChangeCount = diff.ariaChangeCount;
|
|
454
|
+
pageDiff.ariaAdded = diff.ariaAdded;
|
|
455
|
+
pageDiff.ariaRemoved = diff.ariaRemoved;
|
|
454
456
|
}
|
|
455
457
|
if (this.overlay.isOpen && (!previousState.overlay.isOpen || previousState.overlay.name !== this.overlay.name)) {
|
|
456
458
|
pageDiff.areaOfInterest = this.overlay.describe();
|
|
@@ -542,6 +544,8 @@ export class Diff {
|
|
|
542
544
|
_messages = [];
|
|
543
545
|
_ariaDiffResult = null;
|
|
544
546
|
_ariaChangeCount = 0;
|
|
547
|
+
_ariaAdded = 0;
|
|
548
|
+
_ariaRemoved = 0;
|
|
545
549
|
_isSameUrl;
|
|
546
550
|
constructor(current, previous) {
|
|
547
551
|
this.current = current;
|
|
@@ -590,6 +594,12 @@ export class Diff {
|
|
|
590
594
|
get ariaChangeCount() {
|
|
591
595
|
return this._ariaChangeCount;
|
|
592
596
|
}
|
|
597
|
+
get ariaAdded() {
|
|
598
|
+
return this._ariaAdded;
|
|
599
|
+
}
|
|
600
|
+
get ariaRemoved() {
|
|
601
|
+
return this._ariaRemoved;
|
|
602
|
+
}
|
|
593
603
|
get htmlDiff() {
|
|
594
604
|
return this._htmlDiffResult;
|
|
595
605
|
}
|
|
@@ -614,5 +624,7 @@ export class Diff {
|
|
|
614
624
|
const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
|
|
615
625
|
this._ariaDiffResult = ariaDiff.text;
|
|
616
626
|
this._ariaChangeCount = ariaDiff.count;
|
|
627
|
+
this._ariaAdded = ariaDiff.added;
|
|
628
|
+
this._ariaRemoved = ariaDiff.removed;
|
|
617
629
|
}
|
|
618
630
|
}
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -404,12 +404,16 @@ export class Planner extends PlannerBase {
|
|
|
404
404
|
`);
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
|
+
let activeRegion = '';
|
|
408
|
+
if (state.overlay.isOpen)
|
|
409
|
+
activeRegion = `Active region: ${state.overlay.describe()} — the user's current focus area. Plan tests for the controls inside it first.`;
|
|
407
410
|
conversation.addUserText(dedent `
|
|
408
411
|
${this.buildApproach(style)}
|
|
409
412
|
|
|
410
413
|
<context>
|
|
411
414
|
URL: ${state.url || 'Unknown'}
|
|
412
415
|
Title: ${state.title || 'Unknown'}
|
|
416
|
+
${activeRegion}
|
|
413
417
|
</context>
|
|
414
418
|
`);
|
|
415
419
|
if (this.fisherman) {
|
|
@@ -281,7 +281,7 @@ export function WithLocators(Base) {
|
|
|
281
281
|
if (sectionQuery.count() === 0)
|
|
282
282
|
sectionQuery = mdq(result.text).query(`section3(~"${escaped}")`);
|
|
283
283
|
if (newCss) {
|
|
284
|
-
result.text = sectionQuery.query('blockquote[0]').
|
|
284
|
+
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Container', `'${newCss}'`);
|
|
285
285
|
}
|
|
286
286
|
else {
|
|
287
287
|
result.text = sectionQuery.query('blockquote[0]').replace('');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type Explorer from '../../explorer.js';
|
|
2
|
+
import { type ListMeasure, type PaginationStrategy } from '../../utils/pagination.js';
|
|
3
|
+
import { type Constructor } from './mixin.js';
|
|
4
|
+
import type { ResearchResult } from './research-result.js';
|
|
5
|
+
export declare function WithPagination<T extends Constructor>(Base: T): {
|
|
6
|
+
new (...args: any[]): {
|
|
7
|
+
explorer: Explorer;
|
|
8
|
+
detectPagination(result: ResearchResult): Promise<void>;
|
|
9
|
+
probeSection(css: string): Promise<PaginationStrategy | null>;
|
|
10
|
+
measure(css: string): Promise<ListMeasure | null>;
|
|
11
|
+
recordPagination(result: ResearchResult, name: string, strategy: PaginationStrategy): void;
|
|
12
|
+
};
|
|
13
|
+
} & T;
|
|
14
|
+
export interface PaginationMethods {
|
|
15
|
+
detectPagination(result: ResearchResult): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mdq } from "../../utils/markdown-query.js";
|
|
2
|
+
import { inspectList, restoreScroll } from "../../utils/pagination.js";
|
|
3
|
+
import { debugLog } from "./mixin.js";
|
|
4
|
+
import { extractPaginationFromBlockquote, parseDataSections, parseResearchSections } from "./parser.js";
|
|
5
|
+
export function WithPagination(Base) {
|
|
6
|
+
return class extends Base {
|
|
7
|
+
async detectPagination(result) {
|
|
8
|
+
const sections = [...parseResearchSections(result.text), ...parseDataSections(result.text)];
|
|
9
|
+
for (const section of sections) {
|
|
10
|
+
const css = section.containerCss;
|
|
11
|
+
if (!css)
|
|
12
|
+
continue;
|
|
13
|
+
if (extractPaginationFromBlockquote(section.rawMarkdown))
|
|
14
|
+
continue;
|
|
15
|
+
const strategy = await this.probeSection(css);
|
|
16
|
+
if (!strategy)
|
|
17
|
+
continue;
|
|
18
|
+
this.recordPagination(result, section.name, strategy);
|
|
19
|
+
debugLog(`Pagination in "${section.name}": ${strategy}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async probeSection(css) {
|
|
23
|
+
const before = await this.measure(css);
|
|
24
|
+
if (!before)
|
|
25
|
+
return null;
|
|
26
|
+
if (before.hasPagingControls)
|
|
27
|
+
return 'controls';
|
|
28
|
+
if (before.isFeed)
|
|
29
|
+
return 'infinite';
|
|
30
|
+
if (!before.scrolls)
|
|
31
|
+
return null;
|
|
32
|
+
const action = this.explorer.action();
|
|
33
|
+
const scrolled = await action.attempt(`I.scrollTo('${css} > *:last-child')`).catch(() => false);
|
|
34
|
+
if (!scrolled)
|
|
35
|
+
return null;
|
|
36
|
+
const after = await this.measure(css);
|
|
37
|
+
await this.explorer.withPage((page) => page.evaluate(restoreScroll, { css, scrollTop: before.scrollTop, pageScrollY: before.pageScrollY })).catch(() => { });
|
|
38
|
+
if (!after)
|
|
39
|
+
return null;
|
|
40
|
+
if (after.items > before.items)
|
|
41
|
+
return 'infinite';
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
measure(css) {
|
|
45
|
+
return this.explorer
|
|
46
|
+
.withPage((page) => page.evaluate(inspectList, css))
|
|
47
|
+
.catch((err) => {
|
|
48
|
+
debugLog(`List measurement failed for '${css}': ${err.message}`);
|
|
49
|
+
return null;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
recordPagination(result, name, strategy) {
|
|
53
|
+
const escaped = name.replace(/"/g, '\\"');
|
|
54
|
+
let sectionQuery = mdq(result.text).query(`section2(~"${escaped}")`);
|
|
55
|
+
if (sectionQuery.count() === 0)
|
|
56
|
+
sectionQuery = mdq(result.text).query(`section3(~"${escaped}")`);
|
|
57
|
+
if (sectionQuery.count() === 0)
|
|
58
|
+
return;
|
|
59
|
+
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Pagination', strategy);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PaginationStrategy } from '../../utils/pagination.js';
|
|
1
2
|
export interface ResearchElement {
|
|
2
3
|
name: string;
|
|
3
4
|
type: string | null;
|
|
@@ -23,6 +24,8 @@ export declare const RESEARCH_COLUMN_ORDER: string[];
|
|
|
23
24
|
export declare function mapRowToElement(row: Record<string, string>): ResearchElement | null;
|
|
24
25
|
export declare function extractContainerFromBlockquote(sectionMarkdown: string): string | null;
|
|
25
26
|
export declare function parseResearchSections(markdown: string): ResearchSection[];
|
|
27
|
+
export declare function parseDataSections(markdown: string): ResearchSection[];
|
|
28
|
+
export declare function extractPaginationFromBlockquote(sectionMarkdown: string): PaginationStrategy | null;
|
|
26
29
|
export declare function extractValidContainers(researchText: string, opts?: {
|
|
27
30
|
exclude?: string[];
|
|
28
31
|
}): Array<{
|
|
@@ -57,13 +57,10 @@ export function mapRowToElement(row) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
export function extractContainerFromBlockquote(sectionMarkdown) {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
60
|
+
const entry = mdq(sectionMarkdown).query('blockquote[0]').keyValue().container;
|
|
61
|
+
if (!entry)
|
|
62
62
|
return null;
|
|
63
|
-
const
|
|
64
|
-
if (!match)
|
|
65
|
-
return null;
|
|
66
|
-
const css = normalizeLocatorValue(match[1]);
|
|
63
|
+
const css = normalizeLocatorValue(entry);
|
|
67
64
|
if (!css || !/^[.#\[\w]/.test(css))
|
|
68
65
|
return null;
|
|
69
66
|
return css;
|
|
@@ -80,6 +77,25 @@ export function parseResearchSections(markdown) {
|
|
|
80
77
|
return { name: section.name, containerCss, elements, rawMarkdown: section.rawMarkdown, isExtended };
|
|
81
78
|
});
|
|
82
79
|
}
|
|
80
|
+
export function parseDataSections(markdown) {
|
|
81
|
+
return parseSections(markdown)
|
|
82
|
+
.filter((s) => s.name.toLowerCase().startsWith('data:'))
|
|
83
|
+
.map((section) => ({
|
|
84
|
+
name: section.name,
|
|
85
|
+
containerCss: extractContainerFromBlockquote(section.rawMarkdown),
|
|
86
|
+
elements: [],
|
|
87
|
+
rawMarkdown: section.rawMarkdown,
|
|
88
|
+
isExtended: false,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
export function extractPaginationFromBlockquote(sectionMarkdown) {
|
|
92
|
+
const value = mdq(sectionMarkdown).query('blockquote[0]').keyValue().pagination?.toLowerCase();
|
|
93
|
+
if (value === 'controls')
|
|
94
|
+
return 'controls';
|
|
95
|
+
if (value === 'infinite')
|
|
96
|
+
return 'infinite';
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
83
99
|
export function extractValidContainers(researchText, opts) {
|
|
84
100
|
const exclude = opts?.exclude || [];
|
|
85
101
|
return parseResearchSections(researchText)
|