explorbot 0.2.2 → 0.2.4
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 +1 -1
- package/bin/explorbot-cli.ts +59 -38
- package/boat/api-tester/src/apibot.ts +4 -2
- package/boat/api-tester/src/cli.ts +2 -2
- package/boat/api-tester/src/config.ts +43 -10
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/cli.ts +1 -0
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/doc-collector/src/docs-renderer.ts +18 -4
- package/boat/doc-collector/src/state-diagram.ts +61 -14
- package/boat/prima/bin/prima-cli.ts +5 -0
- package/boat/prima/package.json +16 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +251 -0
- package/boat/prima/src/envelope.ts +169 -0
- package/boat/prima/src/prima.ts +1124 -0
- package/boat/prima/src/pw-parser.ts +27 -0
- package/boat/prima/src/pw-registry.ts +74 -0
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +51 -32
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/apibot.js +3 -2
- package/dist/boat/api-tester/src/cli.js +2 -2
- package/dist/boat/api-tester/src/config.js +40 -10
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/cli.js +1 -0
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
- package/dist/boat/doc-collector/src/state-diagram.js +57 -13
- package/dist/boat/prima/bin/prima-cli.js +4 -0
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +220 -0
- package/dist/boat/prima/src/envelope.js +142 -0
- package/dist/boat/prima/src/prima.js +1031 -0
- package/dist/boat/prima/src/pw-parser.js +30 -0
- package/dist/boat/prima/src/pw-registry.js +65 -0
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/models.json +3 -0
- package/dist/package.json +8 -3
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +7 -2
- package/dist/src/action.js +41 -6
- package/dist/src/ai/captain/mixin.js +3 -4
- package/dist/src/ai/captain/web-mode.js +7 -4
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +11 -0
- package/dist/src/ai/navigator.js +21 -12
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/planner.d.ts +1 -0
- package/dist/src/ai/planner.js +6 -0
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +2 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +5 -2
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +33 -20
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- package/dist/src/application-spec-contract.d.ts +8 -0
- package/dist/src/application-spec-contract.js +8 -0
- package/dist/src/application-spec.d.ts +15 -0
- package/dist/src/application-spec.js +71 -0
- package/dist/src/browser-server.d.ts +12 -6
- package/dist/src/browser-server.js +74 -19
- package/dist/src/commands/clean-command.js +2 -7
- package/dist/src/commands/init-command.d.ts +5 -0
- package/dist/src/commands/init-command.js +119 -1
- package/dist/src/commands/navigate-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/sites-command.d.ts +6 -0
- package/dist/src/commands/sites-command.js +23 -0
- package/dist/src/components/InitWizard.d.ts +10 -0
- package/dist/src/components/InitWizard.js +133 -0
- package/dist/src/components/InputReadline.d.ts +1 -0
- package/dist/src/components/InputReadline.js +7 -4
- package/dist/src/config.d.ts +25 -5
- package/dist/src/config.js +158 -40
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +11 -1
- package/dist/src/explorbot.js +27 -6
- package/dist/src/explorer.d.ts +4 -1
- package/dist/src/explorer.js +42 -9
- package/dist/src/global-config.d.ts +22 -0
- package/dist/src/global-config.js +117 -0
- package/dist/src/knowledge-tracker.d.ts +5 -1
- package/dist/src/knowledge-tracker.js +14 -1
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +54 -0
- package/dist/src/remote.js +229 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +2 -0
- package/dist/src/test-plan.js +7 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/cli-name.js +6 -2
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +24 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/test-files.js +1 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +12 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +86 -0
- package/docs/reference/commands.md +994 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/models.json +3 -0
- package/package.json +8 -3
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +47 -7
- package/src/ai/captain/mixin.ts +3 -3
- package/src/ai/captain/web-mode.ts +7 -4
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +26 -15
- package/src/ai/pilot.ts +53 -2
- package/src/ai/planner.ts +7 -0
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +2 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +4 -2
- package/src/ai/tester.ts +32 -18
- package/src/ai/tools.ts +132 -36
- package/src/application-spec-contract.ts +10 -0
- package/src/application-spec.ts +87 -0
- package/src/browser-server.ts +74 -19
- package/src/commands/clean-command.ts +1 -6
- package/src/commands/init-command.ts +146 -1
- package/src/commands/navigate-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/sites-command.ts +27 -0
- package/src/components/InitWizard.tsx +166 -0
- package/src/components/InputReadline.tsx +8 -4
- package/src/config.ts +177 -43
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +34 -7
- package/src/explorer.ts +47 -9
- package/src/global-config.ts +148 -0
- package/src/knowledge-tracker.ts +17 -1
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +238 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +2 -1
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +9 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/cli-name.ts +5 -2
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +24 -1
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/test-files.ts +1 -2
- package/src/utils/url-matcher.ts +13 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
package/dist/src/ai/tools.js
CHANGED
|
@@ -2,8 +2,9 @@ import { tool } from 'ai';
|
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { ActionResult } from "../action-result.js";
|
|
5
|
+
import { Stats } from "../stats.js";
|
|
5
6
|
import { TestResult } from '../test-plan.js';
|
|
6
|
-
import { LARGE_ARIA_CHANGE_THRESHOLD
|
|
7
|
+
import { LARGE_ARIA_CHANGE_THRESHOLD } from "../utils/aria.js";
|
|
7
8
|
import { isFatalBrowserError } from "../utils/browser-errors.js";
|
|
8
9
|
import { createDebug, tag } from '../utils/logger.js';
|
|
9
10
|
import { pause } from '../utils/loop.js';
|
|
@@ -18,6 +19,10 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
18
19
|
description: dedent `
|
|
19
20
|
Click an element by trying multiple CodeceptJS commands in order until one succeeds.
|
|
20
21
|
|
|
22
|
+
Use this only for elements the page context gives you no ref for. When the element shows a ref such as [ref=e14],
|
|
23
|
+
call clickRef with that ref instead — composing a locator for an element that already has a ref is wasted work,
|
|
24
|
+
and a locator can match several elements where a ref cannot.
|
|
25
|
+
|
|
21
26
|
Follow <locator_priority> from system prompt for locator selection.
|
|
22
27
|
|
|
23
28
|
I.click(locator) - click element matching locator
|
|
@@ -36,14 +41,13 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
36
41
|
commands: z.array(z.string()).describe(dedent `
|
|
37
42
|
FALLBACK LOCATORS for ONE element. All commands must click the SAME element.
|
|
38
43
|
Never mix different elements — use separate click() calls instead.
|
|
44
|
+
REQUIRED: include at least one command WITHOUT a container — a wrong or stale container always fails.
|
|
39
45
|
Order by reliability:
|
|
40
|
-
1. I.click(text, container) -
|
|
46
|
+
1. I.click(text, container) - when the container is verified - e.g. I.click("Save", ".modal")
|
|
41
47
|
2. I.click(ARIA, container) - e.g. I.click({"role":"button","text":"Save"}, ".modal")
|
|
42
48
|
3. I.click(CSS, container) - e.g. I.click("#btn", ".modal")
|
|
43
49
|
4. I.click(CSS) or I.click(XPath) - when locator already includes context (ID, XPath)
|
|
44
50
|
5. I.clickXY(x, y) - coordinates fallback
|
|
45
|
-
IMPORTANT: Always include at least one command WITHOUT a container as fallback,
|
|
46
|
-
in case the element moved to a different section (e.g. I.click("Save") without container).
|
|
47
51
|
`),
|
|
48
52
|
explanation: z.string().describe('Why you are clicking this element'),
|
|
49
53
|
}),
|
|
@@ -106,14 +110,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
106
110
|
}
|
|
107
111
|
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, commands[0]);
|
|
108
112
|
await commitNote(activeNote, TestResult.FAILED, toolResult, action);
|
|
109
|
-
|
|
110
|
-
const lastError = attempts[attempts.length - 1]?.error || '';
|
|
111
|
-
if (lastError.includes('was not found') || lastError.includes('not found by text')) {
|
|
112
|
-
suggestion = 'Element was not found in the DOM. Use xpathCheck() to locate it, context() to refresh snapshot, or visualClick() to click by visual appearance.';
|
|
113
|
-
}
|
|
114
|
-
else if (lastError.includes('Timeout') || lastError.includes('intercept')) {
|
|
115
|
-
suggestion = 'Element exists but could not be clicked (possibly covered by overlay or not interactable). Try closing overlapping panels first, or use visualClick().';
|
|
116
|
-
}
|
|
113
|
+
const suggestion = clickFailureSuggestion(attempts);
|
|
117
114
|
return failedToolResult('click', 'All click commands failed', {
|
|
118
115
|
...toolResult,
|
|
119
116
|
attempts,
|
|
@@ -121,6 +118,38 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
121
118
|
}, action.lastError);
|
|
122
119
|
},
|
|
123
120
|
}),
|
|
121
|
+
clickRef: tool({
|
|
122
|
+
description: dedent `
|
|
123
|
+
Click an element by the ref the page context gave it, e.g. [ref=e14].
|
|
124
|
+
|
|
125
|
+
Prefer this over click() whenever the element you want carries a ref. A ref names one exact element, so it
|
|
126
|
+
cannot match several by mistake and never needs disambiguating — it is the fastest way to click.
|
|
127
|
+
Only pass a ref that appears in the page context you were given. Never invent or guess one.
|
|
128
|
+
If it reports the ref is gone, the page has been rebuilt: get fresh context and use the new ref.
|
|
129
|
+
`,
|
|
130
|
+
inputSchema: z.object({
|
|
131
|
+
ref: z.string().describe('The ref exactly as it appears in the page context, e.g. "e14"'),
|
|
132
|
+
element: z.string().describe('Role and name of the element you are clicking, for the record'),
|
|
133
|
+
}),
|
|
134
|
+
execute: async ({ ref, element }) => {
|
|
135
|
+
const activeNote = task.startNote(`Click ${element}`);
|
|
136
|
+
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
137
|
+
const action = explorer.action();
|
|
138
|
+
const named = await describeRef(explorer, ref);
|
|
139
|
+
const run = `I.usePlaywrightTo(${JSON.stringify(`click ${element}`)}, async ({ page }) => page.locator(${JSON.stringify(`aria-ref=${ref}`)}).click())`;
|
|
140
|
+
if (!(await action.attempt(run, `Click ${element}`))) {
|
|
141
|
+
activeNote.commit(TestResult.FAILED);
|
|
142
|
+
return failedToolResult('clickRef', `Ref ${ref} could not be clicked: ${errorText(action.lastError)}`, {
|
|
143
|
+
suggestion: 'The ref may belong to an older version of the page. Get fresh context and use the ref it gives, or fall back to click() with a locator.',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// a ref belongs to this session only, so the run is reported as the locator a later test can replay
|
|
147
|
+
const code = named ? `I.click(${JSON.stringify(named)})` : run;
|
|
148
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, code);
|
|
149
|
+
await commitNote(activeNote, TestResult.PASSED, toolResult, action);
|
|
150
|
+
return successToolResult('clickRef', { ...toolResult, code }, action);
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
124
153
|
hover: tool({
|
|
125
154
|
description: dedent `
|
|
126
155
|
Move the mouse cursor to an element to reveal hover-only controls.
|
|
@@ -245,15 +274,11 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
245
274
|
}
|
|
246
275
|
const focusFreeKeys = new Set(['Escape', 'Esc', 'Tab', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12']);
|
|
247
276
|
const needsFocus = !focusFreeKeys.has(keyToUse) && !modifier;
|
|
248
|
-
if (needsFocus) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return failedToolResult('pressKey', `No element is focused. Key '${keyToUse}' requires a focused element.`, {
|
|
254
|
-
suggestion: 'Click the target element first, then press the key.',
|
|
255
|
-
});
|
|
256
|
-
}
|
|
277
|
+
if (needsFocus && !(await hasFocusedElement(explorer))) {
|
|
278
|
+
activeNote.commit(TestResult.FAILED);
|
|
279
|
+
return failedToolResult('pressKey', `No element is focused. Key '${keyToUse}' requires a focused element.`, {
|
|
280
|
+
suggestion: 'Click the target element first, then press the key.',
|
|
281
|
+
});
|
|
257
282
|
}
|
|
258
283
|
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
259
284
|
const action = explorer.action();
|
|
@@ -296,6 +321,8 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }, task) {
|
|
|
296
321
|
Execute raw CodeceptJS code block with multiple commands.
|
|
297
322
|
USE THIS TOOL for typing text into fields: I.fillField, I.type
|
|
298
323
|
|
|
324
|
+
Do not put a click on a ref-bearing element in here — clickRef with its ref is cheaper and cannot mis-target.
|
|
325
|
+
|
|
299
326
|
Follow <actions> from system prompt for available commands.
|
|
300
327
|
Follow <locator_priority> from system prompt for locator selection.
|
|
301
328
|
|
|
@@ -456,25 +483,25 @@ export function createLearnExperienceTool({ getExperienceTracker, getState }) {
|
|
|
456
483
|
});
|
|
457
484
|
}
|
|
458
485
|
export function createAgentTools({ explorer, stateManager, ai, researcher, navigator, supervisor, withExperience }) {
|
|
459
|
-
let visionDisabled = false;
|
|
460
486
|
const tools = {
|
|
461
487
|
see: tool({
|
|
462
488
|
description: dedent `
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
489
|
+
Answer a question about the page from a screenshot, for things its structure cannot express:
|
|
490
|
+
layout and position, what an image or canvas depicts, colour, and whether something is covered or cut off.
|
|
491
|
+
This runs a second model and is the slowest tool here, so reach for it only when the question is genuinely visual.
|
|
492
|
+
Do NOT use it to confirm an action landed — every action already reports what changed on the page.
|
|
466
493
|
Input schema has exactly one field: request. Do not pass text, reason, assertion, or other fields.
|
|
467
494
|
|
|
468
495
|
<example>
|
|
469
|
-
request: "
|
|
470
|
-
result: "
|
|
496
|
+
request: "Is the save button covered by anything, and does the chart show any plotted data?"
|
|
497
|
+
result: "The save button is partly behind a cookie banner at the bottom. The chart area is empty apart from its axes."
|
|
471
498
|
</example>
|
|
472
499
|
`,
|
|
473
500
|
inputSchema: z.object({
|
|
474
501
|
request: z.string().describe('LLM-friendly description of the page contents to look for. 1-3 sentences. No more than 100 words.'),
|
|
475
502
|
}),
|
|
476
503
|
execute: async ({ request }) => {
|
|
477
|
-
if (visionDisabled) {
|
|
504
|
+
if (Stats.visionDisabled) {
|
|
478
505
|
return failedToolResult('see', 'Vision tools are disabled for this session. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.');
|
|
479
506
|
}
|
|
480
507
|
try {
|
|
@@ -495,8 +522,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
495
522
|
catch (error) {
|
|
496
523
|
throwIfFatalBrowserError(error);
|
|
497
524
|
const errorMessage = errorText(error);
|
|
498
|
-
|
|
499
|
-
tag('warning').log('⚠️ Vision model is not available. Visual checks are disabled for this session.');
|
|
525
|
+
disableVision();
|
|
500
526
|
return failedToolResult('see', `See tool failed: ${errorMessage}`, {
|
|
501
527
|
suggestion: 'Vision is now disabled. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.',
|
|
502
528
|
});
|
|
@@ -576,6 +602,12 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
576
602
|
code: result.successfulCodes.join('\n'),
|
|
577
603
|
}, { assertionSteps: result.assertionSteps });
|
|
578
604
|
}
|
|
605
|
+
if (result.inexpressible) {
|
|
606
|
+
return failedToolResult('verify', `No assertion could express this claim: ${assertion}`, {
|
|
607
|
+
inexpressible: true,
|
|
608
|
+
suggestion: 'This is not evidence the page is wrong — the claim could not be turned into an assertion. Restate it in terms of what is visible or of a control state, or check it with see().',
|
|
609
|
+
});
|
|
610
|
+
}
|
|
579
611
|
return failedToolResult('verify', `Verification failed: ${assertion}`, {
|
|
580
612
|
suggestion: 'The assertion could not be verified. Check if the condition is actually present on the page or try a different assertion.',
|
|
581
613
|
});
|
|
@@ -696,7 +728,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
696
728
|
context: z.string().describe('What you already tried and why it failed - helps with accurate identification'),
|
|
697
729
|
}),
|
|
698
730
|
execute: async ({ element, context }) => {
|
|
699
|
-
if (visionDisabled) {
|
|
731
|
+
if (Stats.visionDisabled) {
|
|
700
732
|
return failedToolResult('visualClick', 'Vision tools are disabled for this session. Use xpathCheck() to find the element, then click() with the discovered locator.');
|
|
701
733
|
}
|
|
702
734
|
try {
|
|
@@ -741,8 +773,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
741
773
|
catch (error) {
|
|
742
774
|
throwIfFatalBrowserError(error);
|
|
743
775
|
const errorMessage = errorText(error);
|
|
744
|
-
|
|
745
|
-
tag('warning').log('⚠️ Vision model is not available. Visual clicks are disabled for this session.');
|
|
776
|
+
disableVision();
|
|
746
777
|
return failedToolResult('visualClick', `visualClick tool failed: ${errorMessage}`, {
|
|
747
778
|
suggestion: 'Vision is now disabled. Use xpathCheck() to find the element, then click() with the discovered locator.',
|
|
748
779
|
});
|
|
@@ -875,6 +906,11 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
875
906
|
},
|
|
876
907
|
}),
|
|
877
908
|
};
|
|
909
|
+
const disableVision = () => {
|
|
910
|
+
Stats.visionDisabled = true;
|
|
911
|
+
withdrawVisionTools(tools);
|
|
912
|
+
tag('warning').log('⚠️ Vision model is not available. Visual tools are disabled for this session.');
|
|
913
|
+
};
|
|
878
914
|
if (withExperience !== false) {
|
|
879
915
|
tools.learnExperience = createLearnExperienceTool({
|
|
880
916
|
getExperienceTracker: () => stateManager.getExperienceTracker(),
|
|
@@ -921,6 +957,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
921
957
|
},
|
|
922
958
|
});
|
|
923
959
|
}
|
|
960
|
+
withdrawVisionTools(tools);
|
|
924
961
|
return tools;
|
|
925
962
|
}
|
|
926
963
|
const PAGE_DIFF_SUGGESTION = 'Analyze page diff. htmlParts shows what changed and WHERE — each part has a container selector. Use the container as context when clicking elements from the diff.';
|
|
@@ -964,13 +1001,27 @@ function errorText(error) {
|
|
|
964
1001
|
return error.toString();
|
|
965
1002
|
return 'Unknown error occurred';
|
|
966
1003
|
}
|
|
967
|
-
async function commitNote(activeNote, result, toolResult, action) {
|
|
1004
|
+
export async function commitNote(activeNote, result, toolResult, action) {
|
|
968
1005
|
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
969
1006
|
activeNote.screenshot = await action.saveScreenshot();
|
|
970
1007
|
}
|
|
971
1008
|
activeNote.commit(result);
|
|
972
1009
|
}
|
|
973
|
-
function
|
|
1010
|
+
async function describeRef(explorer, ref) {
|
|
1011
|
+
return Promise.resolve(explorer?.withPage?.((page) => page.locator(`aria-ref=${ref}`).evaluate((el) => {
|
|
1012
|
+
const tag = el.tagName.toLowerCase();
|
|
1013
|
+
const roles = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' };
|
|
1014
|
+
const role = el.getAttribute('role') || roles[tag] || tag;
|
|
1015
|
+
const text = (el.getAttribute('aria-label') || el.innerText || el.value || '').trim().split('\n')[0];
|
|
1016
|
+
if (!text)
|
|
1017
|
+
return null;
|
|
1018
|
+
return { role, text };
|
|
1019
|
+
}))).catch(() => null);
|
|
1020
|
+
}
|
|
1021
|
+
async function hasFocusedElement(explorer) {
|
|
1022
|
+
return explorer.withPage((page) => page.evaluate(() => !!document.activeElement && document.activeElement !== document.body)).catch(() => true);
|
|
1023
|
+
}
|
|
1024
|
+
export function successToolResult(action, data, source) {
|
|
974
1025
|
const result = { success: true, action, ...data };
|
|
975
1026
|
if (source?.playwrightGroupId) {
|
|
976
1027
|
result.playwrightGroupId = source.playwrightGroupId;
|
|
@@ -1008,7 +1059,7 @@ function hasObservablePageChange(data) {
|
|
|
1008
1059
|
return true;
|
|
1009
1060
|
return Array.isArray(data.pageDiff.htmlParts) && data.pageDiff.htmlParts.length > 0;
|
|
1010
1061
|
}
|
|
1011
|
-
async function failedToolResult(action, message, data, error) {
|
|
1062
|
+
export async function failedToolResult(action, message, data, error) {
|
|
1012
1063
|
const result = { success: false, action, message, ...data };
|
|
1013
1064
|
if (data?.pageDiff) {
|
|
1014
1065
|
result.suggestion = data.suggestion ? `${data.suggestion} ${PAGE_DIFF_SUGGESTION}` : PAGE_DIFF_SUGGESTION;
|
|
@@ -1040,6 +1091,32 @@ function getMultipleElementsSuggestion() {
|
|
|
1040
1091
|
6. Use visualClick() to click the right element by visual appearance
|
|
1041
1092
|
`;
|
|
1042
1093
|
}
|
|
1094
|
+
export function withdrawVisionTools(tools) {
|
|
1095
|
+
if (!Stats.visionDisabled)
|
|
1096
|
+
return;
|
|
1097
|
+
Reflect.deleteProperty(tools, 'see');
|
|
1098
|
+
Reflect.deleteProperty(tools, 'visualClick');
|
|
1099
|
+
}
|
|
1100
|
+
export function clickFailureSuggestion(attempts) {
|
|
1101
|
+
const errors = attempts.map((a) => a.error || '');
|
|
1102
|
+
if (errors.some((e) => e.includes('not enabled'))) {
|
|
1103
|
+
return 'Element exists but is DISABLED — clicking it again cannot work. A precondition is unmet: a required field is empty, nothing is selected, or a dialog is blocking. Satisfy it, then retry.';
|
|
1104
|
+
}
|
|
1105
|
+
if (errors.some((e) => e.includes('intercepts pointer events'))) {
|
|
1106
|
+
return 'Element exists but another element covers it. Close the overlapping panel or dialog, then retry.';
|
|
1107
|
+
}
|
|
1108
|
+
if (errors.some((e) => e.includes('is not visible'))) {
|
|
1109
|
+
return 'Element is in the DOM but not visible. Reveal it first — scroll to it, expand its section, or open the panel holding it.';
|
|
1110
|
+
}
|
|
1111
|
+
const notFound = errors.filter((e) => e.includes('was not found'));
|
|
1112
|
+
if (notFound.length && notFound.every((e) => e.includes('was not found inside element'))) {
|
|
1113
|
+
return 'Element was not found inside that container — the container is wrong or stale, and the element may exist elsewhere on the page. Retry the same locator WITHOUT a container, or verify the container with xpathCheck().';
|
|
1114
|
+
}
|
|
1115
|
+
if (notFound.length) {
|
|
1116
|
+
return 'Element was not found in the DOM. Use xpathCheck() to locate it, context() to refresh snapshot, or visualClick() to click by visual appearance.';
|
|
1117
|
+
}
|
|
1118
|
+
return "Try xpathCheck() to find the element's actual position, see() for visual analysis, or visualClick() to click by visual appearance.";
|
|
1119
|
+
}
|
|
1043
1120
|
const MAX_DISAMBIGUATE_ELEMENTS = 10;
|
|
1044
1121
|
const MULTIPLE_ELEMENTS_PATTERN = 'multiple elements';
|
|
1045
1122
|
async function extractWebElements(error) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const APPLICATION_SPEC_FORMAT = "explorbot-application-spec";
|
|
3
|
+
export declare const APPLICATION_SPEC_VERSION = 1;
|
|
4
|
+
export declare const APPLICATION_SPEC_PAGE_SCHEMA: z.ZodObject<{
|
|
5
|
+
format: z.ZodLiteral<"explorbot-application-spec">;
|
|
6
|
+
version: z.ZodLiteral<1>;
|
|
7
|
+
url: z.ZodString;
|
|
8
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const APPLICATION_SPEC_FORMAT = 'explorbot-application-spec';
|
|
3
|
+
export const APPLICATION_SPEC_VERSION = 1;
|
|
4
|
+
export const APPLICATION_SPEC_PAGE_SCHEMA = z.object({
|
|
5
|
+
format: z.literal(APPLICATION_SPEC_FORMAT),
|
|
6
|
+
version: z.literal(APPLICATION_SPEC_VERSION),
|
|
7
|
+
url: z.string().trim().min(1),
|
|
8
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ActionResult } from './action-result.js';
|
|
2
|
+
export declare class ApplicationSpec {
|
|
3
|
+
pages: ApplicationSpecPage[];
|
|
4
|
+
readonly sourcePath: string;
|
|
5
|
+
constructor(sourcePath: string);
|
|
6
|
+
renderFor(state: ActionResult): string;
|
|
7
|
+
get pageCount(): number;
|
|
8
|
+
load(): void;
|
|
9
|
+
resolveSourcePath(sourcePath: string): string;
|
|
10
|
+
}
|
|
11
|
+
interface ApplicationSpecPage {
|
|
12
|
+
url: string;
|
|
13
|
+
content: string;
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { APPLICATION_SPEC_PAGE_SCHEMA } from "./application-spec-contract.js";
|
|
5
|
+
import { ConfigParser } from "./config.js";
|
|
6
|
+
import { tag } from "./utils/logger.js";
|
|
7
|
+
import { loadMarkdownFiles } from "./utils/markdown-files.js";
|
|
8
|
+
export class ApplicationSpec {
|
|
9
|
+
pages = [];
|
|
10
|
+
sourcePath;
|
|
11
|
+
constructor(sourcePath) {
|
|
12
|
+
this.sourcePath = this.resolveSourcePath(sourcePath);
|
|
13
|
+
this.load();
|
|
14
|
+
}
|
|
15
|
+
renderFor(state) {
|
|
16
|
+
const relevant = this.pages.filter((page) => state.isMatchedBy({ url: page.url }));
|
|
17
|
+
if (relevant.length === 0)
|
|
18
|
+
return '';
|
|
19
|
+
tag('operation').log(`Found application specification for ${state.url}`);
|
|
20
|
+
return dedent `
|
|
21
|
+
<application_spec>
|
|
22
|
+
This is previously collected application documentation. Treat User Can and observed state transitions as supporting context, not as a replacement for the current page state. Treat User Might as unverified possibilities that must be confirmed before use.
|
|
23
|
+
|
|
24
|
+
${relevant.map((page) => page.content).join('\n\n')}
|
|
25
|
+
</application_spec>
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
get pageCount() {
|
|
29
|
+
return this.pages.length;
|
|
30
|
+
}
|
|
31
|
+
load() {
|
|
32
|
+
if (!existsSync(this.sourcePath)) {
|
|
33
|
+
throw new Error(`Application spec not found: ${this.sourcePath}`);
|
|
34
|
+
}
|
|
35
|
+
const isDirectory = statSync(this.sourcePath).isDirectory();
|
|
36
|
+
if (!isDirectory && path.basename(this.sourcePath).toLowerCase() !== 'index.md') {
|
|
37
|
+
throw new Error(`Application spec file must be index.md: ${this.sourcePath}`);
|
|
38
|
+
}
|
|
39
|
+
const bundlePath = isDirectory ? this.sourcePath : path.dirname(this.sourcePath);
|
|
40
|
+
const indexPath = path.join(bundlePath, 'index.md');
|
|
41
|
+
if (!existsSync(indexPath)) {
|
|
42
|
+
throw new Error(`Application spec index not found: ${indexPath}`);
|
|
43
|
+
}
|
|
44
|
+
const pagesPath = path.join(bundlePath, 'pages');
|
|
45
|
+
if (!existsSync(pagesPath)) {
|
|
46
|
+
throw new Error(`Application spec pages directory not found: ${pagesPath}`);
|
|
47
|
+
}
|
|
48
|
+
for (const file of loadMarkdownFiles(pagesPath, { recursive: true })) {
|
|
49
|
+
const parsed = APPLICATION_SPEC_PAGE_SCHEMA.safeParse(file.data);
|
|
50
|
+
if (!parsed.success && parsed.error.issues.some((issue) => issue.path[0] === 'format')) {
|
|
51
|
+
throw new Error(`Invalid application spec format in ${file.filePath}`);
|
|
52
|
+
}
|
|
53
|
+
if (!parsed.success && parsed.error.issues.some((issue) => issue.path[0] === 'version')) {
|
|
54
|
+
throw new Error(`Unsupported application spec version in ${file.filePath}: ${String(file.data.version)}`);
|
|
55
|
+
}
|
|
56
|
+
if (!parsed.success) {
|
|
57
|
+
throw new Error(`Application spec page URL is missing in ${file.filePath}`);
|
|
58
|
+
}
|
|
59
|
+
this.pages.push({ url: parsed.data.url, content: file.content.trim() });
|
|
60
|
+
}
|
|
61
|
+
if (this.pages.length === 0) {
|
|
62
|
+
throw new Error(`Application spec contains no documented pages: ${pagesPath}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
resolveSourcePath(sourcePath) {
|
|
66
|
+
if (path.isAbsolute(sourcePath))
|
|
67
|
+
return path.resolve(sourcePath);
|
|
68
|
+
const configParser = ConfigParser.getInstance();
|
|
69
|
+
return path.resolve(configParser.resolveProjectDir(sourcePath));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
declare function getEndpointFilePath(): string;
|
|
2
|
-
declare function readEndpoint(): string | null;
|
|
3
|
-
declare function removeEndpointFile(): void;
|
|
1
|
+
declare function getEndpointFilePath(instance?: string): string;
|
|
2
|
+
declare function readEndpoint(instance?: string): string | null;
|
|
3
|
+
declare function removeEndpointFile(instance?: string): void;
|
|
4
|
+
declare function listInstances(): Array<{
|
|
5
|
+
name: string;
|
|
6
|
+
endpoint: string;
|
|
7
|
+
}>;
|
|
4
8
|
declare function isServerRunning(wsEndpoint: string): Promise<boolean>;
|
|
5
9
|
declare function launchServer(opts: {
|
|
6
10
|
browser?: string;
|
|
7
11
|
show?: boolean;
|
|
8
|
-
}): Promise<any>;
|
|
9
|
-
declare function
|
|
10
|
-
|
|
12
|
+
}, instance?: string): Promise<any>;
|
|
13
|
+
declare function stopServer(instance?: string): Promise<boolean>;
|
|
14
|
+
declare function keepServerRunning(stop: () => unknown): Promise<never>;
|
|
15
|
+
declare function getAliveEndpoint(instance?: string): Promise<string | null>;
|
|
16
|
+
export { readEndpoint, removeEndpointFile, isServerRunning, launchServer, stopServer, getEndpointFilePath, getAliveEndpoint, listInstances, keepServerRunning };
|
|
@@ -1,35 +1,62 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { chromium, firefox, webkit } from 'playwright
|
|
3
|
+
import { chromium, firefox, webkit } from 'playwright';
|
|
4
4
|
import { ConfigParser } from './config.js';
|
|
5
5
|
import { getCliName } from "./utils/cli-name.js";
|
|
6
6
|
import { log } from './utils/logger.js';
|
|
7
7
|
import { printNextSteps } from "./utils/next-steps.js";
|
|
8
8
|
const ENDPOINT_FILENAME = '.browser-endpoint';
|
|
9
|
-
|
|
9
|
+
const INSTANCE_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
10
|
+
const KEEP_ALIVE_INTERVAL = 1 << 30;
|
|
11
|
+
function getEndpointFilePath(instance = 'default') {
|
|
12
|
+
if (!INSTANCE_NAME_PATTERN.test(instance)) {
|
|
13
|
+
throw new Error(`Invalid browser instance name: "${instance}". Use lowercase letters, digits and dashes.`);
|
|
14
|
+
}
|
|
10
15
|
const configParser = ConfigParser.getInstance();
|
|
11
16
|
const outputDir = configParser.getOutputDir();
|
|
12
|
-
|
|
17
|
+
if (instance === 'default')
|
|
18
|
+
return path.join(outputDir, ENDPOINT_FILENAME);
|
|
19
|
+
return path.join(outputDir, `${ENDPOINT_FILENAME}-${instance}`);
|
|
13
20
|
}
|
|
14
|
-
function readEndpoint() {
|
|
15
|
-
const filePath = getEndpointFilePath();
|
|
21
|
+
function readEndpoint(instance = 'default') {
|
|
22
|
+
const filePath = getEndpointFilePath(instance);
|
|
16
23
|
if (!existsSync(filePath))
|
|
17
24
|
return null;
|
|
18
25
|
return readFileSync(filePath, 'utf8').trim();
|
|
19
26
|
}
|
|
20
|
-
function writeEndpoint(wsEndpoint) {
|
|
21
|
-
const filePath = getEndpointFilePath();
|
|
27
|
+
function writeEndpoint(wsEndpoint, instance = 'default') {
|
|
28
|
+
const filePath = getEndpointFilePath(instance);
|
|
22
29
|
const dir = path.dirname(filePath);
|
|
23
30
|
if (!existsSync(dir)) {
|
|
24
31
|
mkdirSync(dir, { recursive: true });
|
|
25
32
|
}
|
|
26
33
|
writeFileSync(filePath, wsEndpoint, 'utf8');
|
|
27
34
|
}
|
|
28
|
-
function removeEndpointFile() {
|
|
29
|
-
const filePath = getEndpointFilePath();
|
|
35
|
+
function removeEndpointFile(instance = 'default') {
|
|
36
|
+
const filePath = getEndpointFilePath(instance);
|
|
30
37
|
if (existsSync(filePath))
|
|
31
38
|
unlinkSync(filePath);
|
|
32
39
|
}
|
|
40
|
+
function listInstances() {
|
|
41
|
+
const dir = path.dirname(getEndpointFilePath());
|
|
42
|
+
if (!existsSync(dir))
|
|
43
|
+
return [];
|
|
44
|
+
const instances = [];
|
|
45
|
+
for (const fileName of readdirSync(dir)) {
|
|
46
|
+
if (!fileName.startsWith(ENDPOINT_FILENAME))
|
|
47
|
+
continue;
|
|
48
|
+
const suffix = fileName.slice(ENDPOINT_FILENAME.length);
|
|
49
|
+
if (suffix && !suffix.startsWith('-'))
|
|
50
|
+
continue;
|
|
51
|
+
if (suffix === '-')
|
|
52
|
+
continue;
|
|
53
|
+
const name = suffix.slice(1) || 'default';
|
|
54
|
+
if (!INSTANCE_NAME_PATTERN.test(name))
|
|
55
|
+
continue;
|
|
56
|
+
instances.push({ name, endpoint: readFileSync(path.join(dir, fileName), 'utf8').trim() });
|
|
57
|
+
}
|
|
58
|
+
return instances;
|
|
59
|
+
}
|
|
33
60
|
async function isServerRunning(wsEndpoint) {
|
|
34
61
|
try {
|
|
35
62
|
const browser = await chromium.connect(wsEndpoint, { timeout: 3000 });
|
|
@@ -41,7 +68,7 @@ async function isServerRunning(wsEndpoint) {
|
|
|
41
68
|
}
|
|
42
69
|
}
|
|
43
70
|
const BROWSER_LAUNCHERS = { chromium, firefox, webkit };
|
|
44
|
-
async function launchServer(opts) {
|
|
71
|
+
async function launchServer(opts, instance = 'default') {
|
|
45
72
|
const browserName = (opts.browser || 'chromium');
|
|
46
73
|
const launcher = BROWSER_LAUNCHERS[browserName];
|
|
47
74
|
if (!launcher)
|
|
@@ -50,30 +77,58 @@ async function launchServer(opts) {
|
|
|
50
77
|
headless: !opts.show,
|
|
51
78
|
});
|
|
52
79
|
const wsEndpoint = server.wsEndpoint();
|
|
53
|
-
writeEndpoint(wsEndpoint);
|
|
80
|
+
writeEndpoint(wsEndpoint, instance);
|
|
54
81
|
log(`Browser server started: ${browserName} (${opts.show ? 'headed' : 'headless'})`);
|
|
55
82
|
const cli = getCliName();
|
|
83
|
+
let instanceFlag = '';
|
|
84
|
+
if (instance !== 'default')
|
|
85
|
+
instanceFlag = ` --instance ${instance}`;
|
|
56
86
|
const sections = [
|
|
57
87
|
{
|
|
58
88
|
label: 'Browser server',
|
|
59
|
-
path: getEndpointFilePath(),
|
|
89
|
+
path: getEndpointFilePath(instance),
|
|
60
90
|
commands: [
|
|
61
91
|
{ label: 'Endpoint', command: wsEndpoint },
|
|
62
|
-
{ label: 'Status', command: `${cli} browser status` },
|
|
63
|
-
{ label: 'Stop', command: `${cli} browser stop` },
|
|
92
|
+
{ label: 'Status', command: `${cli} browser status${instanceFlag}` },
|
|
93
|
+
{ label: 'Stop', command: `${cli} browser stop${instanceFlag}` },
|
|
64
94
|
],
|
|
65
95
|
},
|
|
66
96
|
];
|
|
67
97
|
printNextSteps(sections);
|
|
68
98
|
return server;
|
|
69
99
|
}
|
|
70
|
-
async function
|
|
71
|
-
const endpoint =
|
|
100
|
+
async function stopServer(instance = 'default') {
|
|
101
|
+
const endpoint = await getAliveEndpoint(instance);
|
|
102
|
+
if (!endpoint)
|
|
103
|
+
return false;
|
|
104
|
+
try {
|
|
105
|
+
const browser = await chromium.connect(endpoint, { timeout: 3000 });
|
|
106
|
+
await browser.close();
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
removeEndpointFile(instance);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
function keepServerRunning(stop) {
|
|
113
|
+
console.log('Browser server is running. Press Ctrl+C to stop.');
|
|
114
|
+
const heartbeat = setInterval(() => { }, KEEP_ALIVE_INTERVAL);
|
|
115
|
+
const cleanup = async () => {
|
|
116
|
+
console.log('\nStopping browser server...');
|
|
117
|
+
clearInterval(heartbeat);
|
|
118
|
+
await stop();
|
|
119
|
+
process.exit(0);
|
|
120
|
+
};
|
|
121
|
+
process.on('SIGINT', cleanup);
|
|
122
|
+
process.on('SIGTERM', cleanup);
|
|
123
|
+
return new Promise(() => { });
|
|
124
|
+
}
|
|
125
|
+
async function getAliveEndpoint(instance = 'default') {
|
|
126
|
+
const endpoint = readEndpoint(instance);
|
|
72
127
|
if (!endpoint)
|
|
73
128
|
return null;
|
|
74
129
|
if (await isServerRunning(endpoint))
|
|
75
130
|
return endpoint;
|
|
76
|
-
removeEndpointFile();
|
|
131
|
+
removeEndpointFile(instance);
|
|
77
132
|
return null;
|
|
78
133
|
}
|
|
79
|
-
export { readEndpoint, removeEndpointFile, isServerRunning, launchServer, getEndpointFilePath, getAliveEndpoint };
|
|
134
|
+
export { readEndpoint, removeEndpointFile, isServerRunning, launchServer, stopServer, getEndpointFilePath, getAliveEndpoint, listInstances, keepServerRunning };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, rmSync, statSync, unlinkSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { join } from 'node:path';
|
|
3
3
|
import { ConfigParser, outputPath } from '../config.js';
|
|
4
4
|
import { tag } from '../utils/logger.js';
|
|
5
5
|
import { BaseCommand } from './base-command.js';
|
|
@@ -13,12 +13,7 @@ export const CLEAN_TARGETS = {
|
|
|
13
13
|
};
|
|
14
14
|
function getExperienceDir() {
|
|
15
15
|
const configParser = ConfigParser.getInstance();
|
|
16
|
-
|
|
17
|
-
const configPath = configParser.getConfigPath();
|
|
18
|
-
if (configPath) {
|
|
19
|
-
return join(dirname(configPath), config.dirs?.experience || 'experience');
|
|
20
|
-
}
|
|
21
|
-
return config.dirs?.experience || 'experience';
|
|
16
|
+
return configParser.resolveProjectDir(configParser.getConfig().dirs?.experience || 'experience');
|
|
22
17
|
}
|
|
23
18
|
function cleanDirectoryContents(dirPath) {
|
|
24
19
|
if (!existsSync(dirPath))
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
export declare function runInit(options: InitCommandOptions): Promise<void>;
|
|
2
|
+
export declare function writeGlobalConfig(provider: string, apiKey?: string): void;
|
|
1
3
|
export declare function runInitCommand(options: InitCommandOptions): void;
|
|
2
4
|
type InitCommandOptions = {
|
|
3
5
|
configPath?: string;
|
|
4
6
|
force?: boolean;
|
|
5
7
|
path?: string;
|
|
8
|
+
global?: boolean;
|
|
9
|
+
provider?: string;
|
|
10
|
+
apiKey?: string;
|
|
6
11
|
};
|
|
7
12
|
export {};
|