carthagent 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +219 -2
- package/bin/ctg.mjs +736 -0
- package/docs/budgets.md +72 -0
- package/docs/compaction.md +34 -0
- package/docs/context-efficiency-plan.md +24 -0
- package/docs/context-efficiency-review.md +27 -0
- package/docs/context-efficiency.md +49 -0
- package/docs/deepseek-jev-benchmark-plan.md +70 -0
- package/docs/deepseek-jev-benchmark-repair-plan.md +20 -0
- package/docs/deepseek-jev-benchmark-restart.md +9 -0
- package/docs/deepseek-jev-benchmark-results.md +77 -0
- package/docs/deepseek-jev-benchmark-running.md +60 -0
- package/docs/deepseek-jev-benchmark-v3-results.md +41 -0
- package/docs/deepseek-jev-benchmark-v4-protocol.md +15 -0
- package/docs/deepseek-jev-benchmark-v4-results.md +119 -0
- package/docs/deepseek-jev-http400-diagnosis.md +45 -0
- package/docs/delivery-review-evidence.md +53 -0
- package/docs/delivery-timeout-repair-plan.md +51 -0
- package/docs/evaluation-readiness.md +31 -0
- package/docs/evaluation.md +70 -0
- package/docs/extending.md +354 -0
- package/docs/glm-pilot-execution.md +9 -0
- package/docs/glm-pilot-plan.md +15 -0
- package/docs/glm-pilot-results.md +71 -0
- package/docs/graph-context-optimization.md +15 -0
- package/docs/jev-repository-pilot-plan.md +25 -0
- package/docs/jev-repository-pilot-protocol.md +39 -0
- package/docs/jev-repository-pilot-restart.md +7 -0
- package/docs/jev-repository-pilot-results.md +79 -0
- package/docs/jev.md +32 -0
- package/docs/model-registry.md +25 -0
- package/docs/native-compatibility.md +117 -0
- package/docs/opencode-archive.md +9 -0
- package/docs/opencode-migration.md +115 -0
- package/docs/opencode-runtime.md +94 -0
- package/docs/opencode-workspace.md +31 -0
- package/docs/owned-runtime-workflow-registration.md +17 -0
- package/docs/plan-advisor-evaluation.md +38 -0
- package/docs/planning-startup-fix-plan.md +23 -0
- package/docs/planning-startup-fix-review.md +24 -0
- package/docs/planning.md +118 -0
- package/docs/production-merge-review.md +23 -0
- package/docs/quality.md +35 -0
- package/docs/repository-benchmark-design.md +111 -0
- package/docs/review-assurance.md +9 -0
- package/docs/review-experiment-results.md +59 -0
- package/docs/tested-planning-roadmap.md +58 -0
- package/docs/tested-planning.md +58 -0
- package/docs/tui-pickers.md +9 -0
- package/docs/webdriver.md +83 -0
- package/docs/workflow-modes.md +26 -0
- package/docs/workflow.d2 +17 -0
- package/docs/workflow.svg +149 -0
- package/docs/workspace-coordination.md +9 -0
- package/evaluate.mjs +249 -0
- package/extensions/codegraph.ts +36 -0
- package/extensions/delivery.ts +819 -0
- package/extensions/experiential.ts +21 -0
- package/extensions/subscriptions.ts +21 -0
- package/guidance/profiles/authenticated-web.json +24 -0
- package/guidance/profiles/data-visualization.json +25 -0
- package/guidance/profiles/database-change.json +24 -0
- package/guidance/profiles/external-api.json +24 -0
- package/guidance/profiles/transactional-data.json +25 -0
- package/guidance/profiles/web-application.json +26 -0
- package/lib/acceptance-review.mjs +4 -0
- package/lib/browser-bin.mjs +160 -0
- package/lib/browser-errors.mjs +54 -0
- package/lib/browser.mjs +219 -0
- package/lib/budget.mjs +37 -0
- package/lib/cdp.mjs +221 -0
- package/lib/cloud/client.mjs +275 -0
- package/lib/cloud/control-plane.mjs +350 -0
- package/lib/cloud/gateway.mjs +380 -0
- package/lib/cloud/http.mjs +135 -0
- package/lib/cloud/ledger.mjs +215 -0
- package/lib/codegraph-query.mjs +60 -0
- package/lib/codegraph.mjs +378 -0
- package/lib/context-archive.mjs +18 -0
- package/lib/delivery-context.mjs +132 -0
- package/lib/delivery-guidance.mjs +9 -0
- package/lib/delivery-review.mjs +94 -0
- package/lib/delivery.mjs +610 -0
- package/lib/engine.mjs +137 -0
- package/lib/guidance.mjs +71 -0
- package/lib/jev-advisor.mjs +47 -0
- package/lib/opencode/archive.mjs +122 -0
- package/lib/opencode/compatibility.mjs +19 -0
- package/lib/opencode/credentials.mjs +205 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +147 -0
- package/lib/opencode/evaluation.mjs +242 -0
- package/lib/opencode/file-lock.mjs +86 -0
- package/lib/opencode/headless.mjs +200 -0
- package/lib/opencode/review.mjs +213 -0
- package/lib/opencode/runtime.mjs +246 -0
- package/lib/opencode/service.mjs +249 -0
- package/lib/opencode/session-options.mjs +60 -0
- package/lib/opencode/workspace.mjs +133 -0
- package/lib/openrouter-jev.mjs +37 -0
- package/lib/outcome-contract.mjs +21 -0
- package/lib/plan-advisor.mjs +38 -0
- package/lib/planning-access.mjs +7 -0
- package/lib/planning-advice.mjs +28 -0
- package/lib/planning-discovery.mjs +14 -0
- package/lib/planning-guide.mjs +34 -0
- package/lib/planning-view.mjs +57 -0
- package/lib/planning.mjs +155 -0
- package/lib/probe-diagnostics.mjs +13 -0
- package/lib/providers/experiential.mjs +292 -0
- package/lib/providers/model-catalog.json +700 -0
- package/lib/providers/model-catalog.mjs +7 -0
- package/lib/providers/names.mjs +13 -0
- package/lib/providers/subscription-models.mjs +483 -0
- package/lib/reports.mjs +80 -0
- package/lib/review-state.mjs +49 -0
- package/lib/review.mjs +114 -0
- package/lib/scenarios.mjs +99 -0
- package/lib/ship-auth.mjs +44 -0
- package/lib/ship-jev.mjs +59 -0
- package/lib/ship-telemetry.mjs +183 -0
- package/lib/tui/ansi.mjs +255 -0
- package/lib/tui/app.mjs +2445 -0
- package/lib/tui/auth.mjs +249 -0
- package/lib/tui/carthage.mjs +109 -0
- package/lib/tui/clipboard.mjs +122 -0
- package/lib/tui/d2.mjs +431 -0
- package/lib/tui/feed.mjs +467 -0
- package/lib/tui/framing.mjs +80 -0
- package/lib/tui/glyphs.mjs +172 -0
- package/lib/tui/hash-worker.mjs +11 -0
- package/lib/tui/mock-engine.mjs +222 -0
- package/lib/tui/models.mjs +73 -0
- package/lib/tui/picker.mjs +18 -0
- package/lib/tui/theme.mjs +663 -0
- package/lib/tui/title.mjs +7 -0
- package/lib/update-channel.mjs +62 -0
- package/lib/update.mjs +230 -0
- package/lib/webdriver.mjs +82 -0
- package/lib/workflow.mjs +88 -0
- package/lib/workspace.mjs +39 -0
- package/metadata.json +8 -0
- package/package.json +94 -4
- package/plugins/carthagent/index.ts +3 -0
- package/plugins/carthagent/package.json +28 -0
- package/plugins/carthagent/src/archive-rpc.ts +46 -0
- package/plugins/carthagent/src/bootstrap.ts +38 -0
- package/plugins/carthagent/src/index.ts +62 -0
- package/plugins/carthagent/src/pull-request-rpc.ts +54 -0
- package/plugins/carthagent/src/rpc.ts +76 -0
- package/plugins/carthagent/src/server/archive-rpc.ts +327 -0
- package/plugins/carthagent/src/server/budgets.ts +21 -0
- package/plugins/carthagent/src/server/codegraph.ts +46 -0
- package/plugins/carthagent/src/server/delivery.ts +488 -0
- package/plugins/carthagent/src/server/device-auth.ts +91 -0
- package/plugins/carthagent/src/server/native-delivery.ts +26 -0
- package/plugins/carthagent/src/server/orchestration.ts +56 -0
- package/plugins/carthagent/src/server/policy.ts +24 -0
- package/plugins/carthagent/src/server/providers.ts +424 -0
- package/plugins/carthagent/src/server/pull-request-rpc.ts +387 -0
- package/plugins/carthagent/src/server/session-controls.ts +82 -0
- package/plugins/carthagent/src/server/telemetry.ts +92 -0
- package/plugins/carthagent/src/server/workspace-rpc.ts +349 -0
- package/plugins/carthagent/src/session-rpc.ts +31 -0
- package/plugins/carthagent/src/testing.ts +85 -0
- package/plugins/carthagent/src/workspace-rpc.ts +133 -0
- package/plugins/carthagent/test-plugin/index.ts +4 -0
- package/plugins/carthagent/test-plugin/package.json +1 -0
- package/plugins/carthagent/tsconfig.json +19 -0
- package/plugins/carthagent/tui.ts +1 -0
- package/prompts/delivery.md +7 -0
- package/public/index.html +983 -0
- package/scenarios/cli/grade_cli.py +69 -0
- package/scenarios/cli/scenario.json +21 -0
- package/scenarios/game/baseline/minecraft.py +193 -0
- package/scenarios/game/context.md +1 -0
- package/scenarios/game/grade_minecraft_clone.py +603 -0
- package/scenarios/game/requirements.txt +2 -0
- package/scenarios/game/scenario.json +33 -0
- package/server.mjs +341 -0
- package/skills/game-development/SKILL.md +36 -0
- package/skills/game-development/assets/ursina_starter.py +98 -0
- package/skills/game-development/scripts/verify_ursina.py +80 -0
- package/skills/software-delivery/SKILL.md +50 -0
- package/skills/typescript-delivery/SKILL.md +69 -0
- package/tests/acceptance-review.test.mjs +13 -0
- package/tests/auth.test.mjs +234 -0
- package/tests/bounded-summary.test.mjs +68 -0
- package/tests/browser-bin.test.mjs +91 -0
- package/tests/browser.test.mjs +96 -0
- package/tests/budget-engine.test.mjs +110 -0
- package/tests/budget.test.mjs +23 -0
- package/tests/cli.test.mjs +245 -0
- package/tests/clipboard.test.mjs +79 -0
- package/tests/cloud-client.test.mjs +129 -0
- package/tests/cloud-control-plane.test.mjs +144 -0
- package/tests/cloud-gateway.test.mjs +147 -0
- package/tests/cloud-http.test.mjs +102 -0
- package/tests/cloud-ledger.test.mjs +75 -0
- package/tests/codegraph-query.test.mjs +66 -0
- package/tests/codegraph.test.mjs +142 -0
- package/tests/delivery-context.test.mjs +66 -0
- package/tests/delivery-review.test.mjs +139 -0
- package/tests/delivery.test.mjs +343 -0
- package/tests/engine-api.mjs +24 -0
- package/tests/experiential-extension.test.mjs +37 -0
- package/tests/experiential.test.mjs +300 -0
- package/tests/extension.test.mjs +921 -0
- package/tests/fixtures/archive-plugin/index.ts +10 -0
- package/tests/guidance.test.mjs +78 -0
- package/tests/model-registry.test.mjs +68 -0
- package/tests/models.test.mjs +70 -0
- package/tests/openai-idempotency.test.mjs +99 -0
- package/tests/opencode-archive-rpc.test.mjs +82 -0
- package/tests/opencode-delivery.test.mjs +231 -0
- package/tests/opencode-providers.test.mjs +414 -0
- package/tests/opencode-runtime.test.mjs +455 -0
- package/tests/opencode-session-controls.test.mjs +115 -0
- package/tests/opencode-session-options.test.mjs +26 -0
- package/tests/opencode-workspace.test.mjs +339 -0
- package/tests/outcome-contract.test.mjs +17 -0
- package/tests/picker.test.mjs +34 -0
- package/tests/plan-advisor.test.mjs +88 -0
- package/tests/plan-review-loop.test.mjs +53 -0
- package/tests/planning-advice.test.mjs +27 -0
- package/tests/planning-contract.test.mjs +65 -0
- package/tests/planning-startup.test.mjs +38 -0
- package/tests/planning-view.test.mjs +33 -0
- package/tests/planning.test.mjs +223 -0
- package/tests/probe-diagnostics.test.mjs +21 -0
- package/tests/pull-request-rpc.test.mjs +366 -0
- package/tests/review-state.test.mjs +32 -0
- package/tests/review.test.mjs +127 -0
- package/tests/server.test.mjs +252 -0
- package/tests/ship-jev.test.mjs +77 -0
- package/tests/ship-telemetry.test.mjs +333 -0
- package/tests/standard-workflow.test.mjs +28 -0
- package/tests/subscription-models.test.mjs +416 -0
- package/tests/subscriptions-extension.test.mjs +27 -0
- package/tests/terminal-title.test.mjs +13 -0
- package/tests/test_grade_minecraft_clone.py +199 -0
- package/tests/test_starter.py +78 -0
- package/tests/theme.test.mjs +175 -0
- package/tests/tui-copy-paste.test.mjs +54 -0
- package/tests/tui.test.mjs +808 -0
- package/tests/update-channels.test.mjs +129 -0
- package/tests/update.test.mjs +175 -0
- package/tests/webdriver.test.mjs +31 -0
- package/tests/workflow.test.mjs +47 -0
- package/tests/workspace-rpc.test.mjs +236 -0
- package/tests/workspace.test.mjs +24 -0
- package/tools/benchmark/advisor.test.mjs +7 -0
- package/tools/benchmark/advisor.ts +27 -0
- package/tools/benchmark/campaign.mjs +45 -0
- package/tools/benchmark/diagnose-request.mjs +40 -0
- package/tools/benchmark/eval-common.mjs +32 -0
- package/tools/benchmark/eval-ledger.mjs +22 -0
- package/tools/benchmark/eval-shop.mjs +26 -0
- package/tools/benchmark/eval-voxel.mjs +20 -0
- package/tools/benchmark/evaluate.mjs +24 -0
- package/tools/benchmark/evaluators.test.mjs +13 -0
- package/tools/benchmark/fixtures/shop/server.mjs +18 -0
- package/tools/benchmark/fixtures/shop/ui.html +8 -0
- package/tools/benchmark/freeze.mjs +30 -0
- package/tools/benchmark/gateway.mjs +91 -0
- package/tools/benchmark/gateway.test.mjs +38 -0
- package/tools/benchmark/generator.mjs +11 -0
- package/tools/benchmark/generator.test.mjs +26 -0
- package/tools/benchmark/guard.test.mjs +133 -0
- package/tools/benchmark/guard.ts +136 -0
- package/tools/benchmark/native-plugin/index.ts +24 -0
- package/tools/benchmark/planning-startup.test.mjs +47 -0
- package/tools/benchmark/preflight-live.mjs +31 -0
- package/tools/benchmark/preflight.mjs +22 -0
- package/tools/benchmark/replay-diagnostic.mjs +23 -0
- package/tools/benchmark/runner.mjs +141 -0
- package/tools/benchmark/runner.test.mjs +93 -0
- package/tools/benchmark/runtime.mjs +23 -0
- package/tools/benchmark/selftest.mjs +47 -0
- package/tools/benchmark/smoke-jev.mjs +5 -0
- package/tools/benchmark/tasks.mjs +20 -0
- package/tools/benchmark/transport.mjs +65 -0
- package/tools/benchmark/transport.test.mjs +44 -0
- package/tools/benchmark/upstream-error.mjs +18 -0
- package/tools/benchmark/upstream-error.test.mjs +10 -0
- package/tools/browser-check.mjs +109 -0
- package/tools/check-syntax.mjs +27 -0
- package/tools/evaluate-plan-advisor.mjs +20 -0
- package/tools/native-fixture.mjs +194 -0
- package/tools/native-pty-smoke.py +108 -0
- package/tools/native-report-plugin/index.ts +50 -0
- package/tools/native-report-plugin/package.json +7 -0
- package/tools/patch-compaction.mjs +24 -0
- package/tools/patch-model-catalog.mjs +12 -0
- package/tools/patch-print-shutdown.mjs +39 -0
- package/tools/repo-pilot/evaluator.cjs +32 -0
- package/tools/repo-pilot/reference.mjs +43 -0
- package/tools/repo-pilot/runner.mjs +85 -0
- package/tools/repo-pilot/sandbox-entry.mjs +12 -0
- package/tools/repo-pilot/tasks.mjs +20 -0
- package/tools/repo-pilot/type-evaluator.cjs +6 -0
- package/tools/test-native-controls.mjs +84 -0
- package/tools/test-native-delivery-host.mjs +307 -0
- package/tools/test-native-evaluation.mjs +46 -0
- package/tools/test-native-host.mjs +385 -0
- package/tools/test-native-outcomes.mjs +73 -0
- package/tools/test-native-package.mjs +70 -0
- package/tools/test-native-pty.mjs +20 -0
- package/tools/test-native-report.mjs +88 -0
- package/tools/test-native-swe-host.mjs +132 -0
- package/tools/test-native-swe.mjs +122 -0
- package/tools/test-picker-pty.py +45 -0
- package/tools/test-python.mjs +29 -0
- package/tools/test-webdriver.mjs +54 -0
- package/tools/verify-release-version.mjs +37 -0
- package/tools/webdriver-check.mjs +52 -0
- package/vendor/agent/bounded-summary.mjs +88 -0
- package/vendor/agent/chunks/anthropic-messages-VWZZOSJQ.js +6 -0
- package/vendor/agent/chunks/anthropic.js +75 -0
- package/vendor/agent/chunks/azure-openai-responses-BJLAM6CD.js +2 -0
- package/vendor/agent/chunks/bedrock-converse-stream.js +74 -0
- package/vendor/agent/chunks/chord-context.js +96 -0
- package/vendor/agent/chunks/chunk-4L3WN2XY.js +2 -0
- package/vendor/agent/chunks/chunk-6CYCI3EJ.js +12 -0
- package/vendor/agent/chunks/chunk-6R7UI4IY.js +2 -0
- package/vendor/agent/chunks/chunk-7ZC4UXSL.js +2 -0
- package/vendor/agent/chunks/chunk-AXIIZGTV.js +2 -0
- package/vendor/agent/chunks/chunk-CLNPYIDP.js +42 -0
- package/vendor/agent/chunks/chunk-DRQDKNXA.js +2 -0
- package/vendor/agent/chunks/chunk-GMWCTUPB.js +2 -0
- package/vendor/agent/chunks/chunk-IDDQWTHI.js +2 -0
- package/vendor/agent/chunks/chunk-JVUZSMYM.js +1548 -0
- package/vendor/agent/chunks/chunk-L2ZPNLYD.js +2 -0
- package/vendor/agent/chunks/chunk-NUHFSC37.js +25 -0
- package/vendor/agent/chunks/chunk-OUXLLA64.js +11 -0
- package/vendor/agent/chunks/chunk-PGXDEOVV.js +2 -0
- package/vendor/agent/chunks/chunk-PJACZCIR.js +2 -0
- package/vendor/agent/chunks/chunk-PRRNXB7S.js +2 -0
- package/vendor/agent/chunks/chunk-TEPMHNKQ.js +2 -0
- package/vendor/agent/chunks/chunk-TRDNDS6A.js +4 -0
- package/vendor/agent/chunks/chunk-U6ZMQKGD.js +50 -0
- package/vendor/agent/chunks/chunk-UAQELI3K.js +2 -0
- package/vendor/agent/chunks/chunk-USJZMAIR.js +2 -0
- package/vendor/agent/chunks/chunk-XDUWOHPL.js +6 -0
- package/vendor/agent/chunks/chunk-XNGRGP62.js +2 -0
- package/vendor/agent/chunks/github-copilot.js +2 -0
- package/vendor/agent/chunks/google-generative-ai-Q2K7FBDI.js +2 -0
- package/vendor/agent/chunks/google-vertex-7UJBF4S7.js +2 -0
- package/vendor/agent/chunks/https-proxy-agent-2VXB7436.js +13 -0
- package/vendor/agent/chunks/image-resize-worker.js +2 -0
- package/vendor/agent/chunks/kimi-coding.js +2 -0
- package/vendor/agent/chunks/lib-HGQMNYVT.js +9 -0
- package/vendor/agent/chunks/mistral-conversations-4M6BZBX6.js +5 -0
- package/vendor/agent/chunks/multipart-parser-SQ6GDJL7.js +3 -0
- package/vendor/agent/chunks/node-KG362ECQ.js +16 -0
- package/vendor/agent/chunks/node-TWVVAGPO.js +2 -0
- package/vendor/agent/chunks/openai-codex-responses-R6VYZTVY.js +10 -0
- package/vendor/agent/chunks/openai-codex.js +75 -0
- package/vendor/agent/chunks/openai-completions-EKZT2IH2.js +7 -0
- package/vendor/agent/chunks/openai-responses-TFDINO6W.js +2 -0
- package/vendor/agent/chunks/openrouter-images-N4XVVFSD.js +2 -0
- package/vendor/agent/chunks/openrouter.js +75 -0
- package/vendor/agent/chunks/pi-messages-PML4SPVJ.js +8 -0
- package/vendor/agent/chunks/radius.js +75 -0
- package/vendor/agent/chunks/src-UK77SYZE.js +4 -0
- package/vendor/agent/chunks/xai.js +2 -0
- package/vendor/agent/cli.js +3 -0
- package/vendor/agent/dist/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/dist/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/dist/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/index.js +2 -0
- package/vendor/agent/modes/interactive/theme/dark.json +91 -0
- package/vendor/agent/modes/interactive/theme/light.json +90 -0
- package/vendor/agent/modes/interactive/theme/theme-schema.json +356 -0
- package/vendor/agent/package.json +14 -0
- package/vendor/agent/rpc-entry.js +3 -0
- package/vendor/agent/theme/dark.json +91 -0
- package/vendor/agent/theme/light.json +90 -0
- package/vendor/agent/theme/theme-schema.json +356 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { experientialProviderConfig, EXPERIENTIAL_PROVIDER_ID } from '../lib/providers/experiential.mjs';
|
|
3
|
+
import { cloudManagedUsageRecovery } from '../lib/cloud/client.mjs';
|
|
4
|
+
import { noteAgentTurn, noteEngineSession } from '../lib/ship-telemetry.mjs';
|
|
5
|
+
|
|
6
|
+
/** Register Carthagent Ship for the bundled agent in TUI, RPC, and headless modes. */
|
|
7
|
+
export default function experiential(pi: ExtensionAPI) {
|
|
8
|
+
pi.registerProvider(EXPERIENTIAL_PROVIDER_ID, experientialProviderConfig());
|
|
9
|
+
// Ship telemetry: the engine session id is the thread id; each user turn
|
|
10
|
+
// (agent run) advances the counter stamped on managed requests.
|
|
11
|
+
pi.on('session_start', (_event: any, ctx: any) => noteEngineSession(ctx?.sessionManager?.getSessionId?.()));
|
|
12
|
+
pi.on('agent_start', () => noteAgentTurn());
|
|
13
|
+
pi.on('message_end', (event: any, ctx: any) => {
|
|
14
|
+
const message = event?.message;
|
|
15
|
+
if (message?.role !== 'assistant' || message.stopReason !== 'error') return;
|
|
16
|
+
if (message.provider !== EXPERIENTIAL_PROVIDER_ID && ctx?.model?.provider !== EXPERIENTIAL_PROVIDER_ID) return;
|
|
17
|
+
const recovery = cloudManagedUsageRecovery(message.errorMessage);
|
|
18
|
+
if (!recovery) return;
|
|
19
|
+
ctx.ui?.notify?.(recovery, 'warning');
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import {
|
|
4
|
+
SUBSCRIPTION_PROVIDER_IDS,
|
|
5
|
+
registerSubscriptionProviders,
|
|
6
|
+
} from '../lib/providers/subscription-models.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Overlay Claude/ChatGPT subscription model discovery on the builtin
|
|
10
|
+
* providers. Engine startup only runs offline refreshes, so a session-start
|
|
11
|
+
* hook issues a bounded network refresh scoped to the two providers —
|
|
12
|
+
* leaving it out lets new models appear for already-signed-in users.
|
|
13
|
+
*/
|
|
14
|
+
export default function subscriptions(pi: ExtensionAPI) {
|
|
15
|
+
registerSubscriptionProviders(pi as unknown as Parameters<typeof registerSubscriptionProviders>[0], {
|
|
16
|
+
agentDir: getAgentDir(),
|
|
17
|
+
});
|
|
18
|
+
pi.on('session_start', (_event: unknown, ctx: { modelRegistry?: { refresh?: (options: { providers: string[] }) => Promise<unknown> } }) => {
|
|
19
|
+
void Promise.resolve(ctx?.modelRegistry?.refresh?.({ providers: [...SUBSCRIPTION_PROVIDER_IDS] })).catch(() => {});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "authenticated-web",
|
|
3
|
+
"title": "Authenticated web application",
|
|
4
|
+
"priority": 90,
|
|
5
|
+
"match": {
|
|
6
|
+
"keywords": ["auth", "authenticated", "authentication", "authorize", "authorization", "login", "logout", "signup", "account", "oauth", "session", "user", "member"],
|
|
7
|
+
"dependencies": ["next-auth", "@auth/core", "passport", "lucia", "@clerk/nextjs"]
|
|
8
|
+
},
|
|
9
|
+
"planning": [
|
|
10
|
+
"Separate authentication from authorization: every server route must verify both identity and access to the requested resource.",
|
|
11
|
+
"Centralize session and database helpers; do not create a new database client in each route or export framework-invalid values from route modules.",
|
|
12
|
+
"Return only fields the current user is allowed to see, especially participant identity, account, and financial data.",
|
|
13
|
+
"Require explicit confirmation and CSRF-safe methods for destructive account or membership actions."
|
|
14
|
+
],
|
|
15
|
+
"checks": [
|
|
16
|
+
"Test unauthenticated, authenticated-but-not-authorized, missing-resource, and successful requests for each changed route.",
|
|
17
|
+
"Test direct API access independently of page middleware; client redirects are not authorization controls.",
|
|
18
|
+
"Verify secrets remain server-only and required authentication configuration fails clearly when absent."
|
|
19
|
+
],
|
|
20
|
+
"review": [
|
|
21
|
+
"Look for ownership filters that only narrow nested relations while still returning the parent resource.",
|
|
22
|
+
"Look for host-header-derived internal requests, over-broad ORM includes, account deletion without relational handling, and user identifiers sourced from unvalidated client input."
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "data-visualization",
|
|
3
|
+
"title": "Data visualization",
|
|
4
|
+
"priority": 60,
|
|
5
|
+
"match": {
|
|
6
|
+
"keywords": ["chart", "graph", "visualization", "timeseries", "time series", "analytics", "sparkline", "candlestick"],
|
|
7
|
+
"dependencies": ["recharts", "chart.js", "react-chartjs-2", "d3", "echarts", "plotly.js"]
|
|
8
|
+
},
|
|
9
|
+
"planning": [
|
|
10
|
+
"Define the canonical raw data shape and a pure transformation layer before implementing chart rendering.",
|
|
11
|
+
"Preserve machine timestamps and numeric values through sorting and filtering; format labels only at render time with an explicit timezone.",
|
|
12
|
+
"Do not fabricate smoothness by interpolating or backfilling across unknown periods unless the product semantics require it and the UI labels it.",
|
|
13
|
+
"Choose one chart implementation and remove abandoned variants after comparison.",
|
|
14
|
+
"Bound data volume and expensive transforms; derive data with memoization rather than mirrored state and JSON-stringify comparisons."
|
|
15
|
+
],
|
|
16
|
+
"checks": [
|
|
17
|
+
"Test unsorted data, duplicate timestamps, timezone and daylight-saving boundaries, market or business closures, gaps, empty series, and malformed values.",
|
|
18
|
+
"Test rapid symbol or range changes so stale responses cannot replace current data.",
|
|
19
|
+
"Render at narrow and wide dimensions and verify accessible text or tabular summaries for key values."
|
|
20
|
+
],
|
|
21
|
+
"review": [
|
|
22
|
+
"Reject sorting by formatted display labels, fake future prices, unnecessary animation, duplicate chart libraries, and test controls left in production UI.",
|
|
23
|
+
"Confirm tooltip values, axes, units, timestamps, and fallback provenance agree with the underlying data."
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "database-change",
|
|
3
|
+
"title": "Database change",
|
|
4
|
+
"priority": 75,
|
|
5
|
+
"match": {
|
|
6
|
+
"keywords": ["database", "schema", "migration", "prisma", "sql", "foreign key", "cascade", "relation", "delete account", "account deletion", "data model"],
|
|
7
|
+
"dependencies": ["@prisma/client", "drizzle-orm", "sequelize", "typeorm", "mongoose"]
|
|
8
|
+
},
|
|
9
|
+
"planning": [
|
|
10
|
+
"Define data invariants, ownership, uniqueness, deletion behavior, and query access patterns before changing the schema.",
|
|
11
|
+
"Use database constraints for invariants that must survive concurrent requests; application checks alone are insufficient.",
|
|
12
|
+
"Plan a forward migration for existing rows and state whether rollback, backfill, or staged deployment is required.",
|
|
13
|
+
"Centralize the database client and domain queries instead of duplicating ORM access across route handlers."
|
|
14
|
+
],
|
|
15
|
+
"checks": [
|
|
16
|
+
"Apply migrations against both an empty database and representative existing data, then run affected route or service tests.",
|
|
17
|
+
"Test uniqueness, foreign-key, cascade or restrict behavior, transaction rollback, and concurrent writes.",
|
|
18
|
+
"Inspect generated SQL and query shape for missing indexes, over-fetching, and accidental full-table work."
|
|
19
|
+
],
|
|
20
|
+
"review": [
|
|
21
|
+
"Reject schema comments that stand in for migration design, required fields without backfills, and account deletion paths that ignore related rows.",
|
|
22
|
+
"Confirm money, timestamps, enums, and optionality use domain-appropriate database types."
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "external-api",
|
|
3
|
+
"title": "External API integration",
|
|
4
|
+
"priority": 70,
|
|
5
|
+
"match": {
|
|
6
|
+
"keywords": ["third-party api", "external api", "api integration", "provider", "webhook", "oauth api", "payment provider", "email provider", "stripe", "sendgrid", "twilio"],
|
|
7
|
+
"dependencies": ["axios", "stripe", "@sendgrid/mail", "twilio"]
|
|
8
|
+
},
|
|
9
|
+
"planning": [
|
|
10
|
+
"Create one server-side provider adapter that validates configuration and normalizes remote payloads into an internal type.",
|
|
11
|
+
"Define timeout, cancellation, retry, rate-limit, cache, pagination, and partial-failure behavior before adding UI consumers.",
|
|
12
|
+
"Validate symbols, identifiers, date ranges, and other user-controlled values before placing them into provider URLs.",
|
|
13
|
+
"Keep credentials server-only and avoid logging secrets, full upstream payloads, or sensitive customer data."
|
|
14
|
+
],
|
|
15
|
+
"checks": [
|
|
16
|
+
"Test malformed provider payloads, missing credentials, non-2xx responses, timeout, rate limiting, empty data, and partial provider outage.",
|
|
17
|
+
"Assert bounded request counts and pagination termination with deterministic mocked responses.",
|
|
18
|
+
"Verify cached or fallback data carries timestamp and provenance so stale values are not silently presented as current."
|
|
19
|
+
],
|
|
20
|
+
"review": [
|
|
21
|
+
"Reject repeated provider-fetch logic across routes, unbounded sequential calls, arbitrary delays as rate-limit control, and zero-filled placeholder fields presented as real data.",
|
|
22
|
+
"Confirm upstream failures map to stable internal error responses without leaking provider details."
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "transactional-data",
|
|
3
|
+
"title": "Transactional data",
|
|
4
|
+
"priority": 80,
|
|
5
|
+
"match": {
|
|
6
|
+
"keywords": ["transaction", "balance", "money", "price", "payment", "checkout", "order", "purchase", "inventory", "booking", "reservation", "billing", "invoice", "subscription", "refund", "ledger", "wallet", "credit", "debit", "funds", "trade", "portfolio", "stock", "shares", "investment"]
|
|
7
|
+
},
|
|
8
|
+
"planning": [
|
|
9
|
+
"Write domain invariants explicitly before implementation: valid quantities, allowed states, ownership, available balance or inventory, and replay behavior.",
|
|
10
|
+
"Treat client-supplied prices, totals, ownership, and status as untrusted; resolve authoritative values on the server at mutation time.",
|
|
11
|
+
"Use decimal or integer minor-unit storage for money rather than binary floating point.",
|
|
12
|
+
"Make validation and mutation atomic with an appropriate database transaction or conditional update; account for concurrent requests and retries.",
|
|
13
|
+
"Define idempotency and audit requirements for mutations that must not execute twice."
|
|
14
|
+
],
|
|
15
|
+
"checks": [
|
|
16
|
+
"Test zero, negative, fractional, non-finite, malformed, and very large inputs as well as invalid enum values.",
|
|
17
|
+
"Run concurrent mutation tests proving balance, holdings, inventory, or capacity cannot go negative or exceed limits.",
|
|
18
|
+
"Test rollback on downstream failure and replay of the same request where duplicate execution would be harmful.",
|
|
19
|
+
"Verify calculations at rounding boundaries with deterministic fixtures."
|
|
20
|
+
],
|
|
21
|
+
"review": [
|
|
22
|
+
"Reject read-check-write sequences outside a transaction, Float money columns, truthiness-based numeric validation, and duplicate calculation implementations.",
|
|
23
|
+
"Confirm historical reports never use future values as fallback data and never present estimated values as authoritative without labeling them."
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "web-application",
|
|
3
|
+
"title": "Web application",
|
|
4
|
+
"priority": 100,
|
|
5
|
+
"activateOnDependency": true,
|
|
6
|
+
"match": {
|
|
7
|
+
"keywords": ["web", "website", "frontend", "browser", "react", "next.js", "nextjs", "vue", "svelte", "dashboard", "page", "component", "api route"],
|
|
8
|
+
"dependencies": ["next", "react", "react-dom", "vue", "svelte", "@angular/core", "vite"]
|
|
9
|
+
},
|
|
10
|
+
"planning": [
|
|
11
|
+
"Inspect the existing framework, package scripts, route conventions, design system, and neighboring components before choosing an implementation pattern.",
|
|
12
|
+
"Define one end-to-end user path before styling breadth. Keep server data access, validation, domain logic, and presentation at explicit boundaries.",
|
|
13
|
+
"Modify the canonical implementation instead of leaving copy, v1/v2/v3, test-page, or temporary variants in production paths.",
|
|
14
|
+
"Account for loading, empty, error, retry, narrow viewport, keyboard, and accessible-name states where the requested surface can encounter them.",
|
|
15
|
+
"Use shared request/response types or runtime schemas at network boundaries; do not spread untyped API payload assumptions through components."
|
|
16
|
+
],
|
|
17
|
+
"checks": [
|
|
18
|
+
"Run the repository's existing lint, typecheck, test, and production build commands; a dev-server render alone is insufficient.",
|
|
19
|
+
"Exercise the public browser path with realistic API success and failure responses, assert no console errors, and inspect the result at desktop and narrow widths.",
|
|
20
|
+
"Check that effects clean up, stale requests cannot overwrite newer state, non-2xx responses are handled before parsing, and loading always settles."
|
|
21
|
+
],
|
|
22
|
+
"review": [
|
|
23
|
+
"Reject duplicated components, speculative abstractions, placeholder production behavior, dead imports, broad any types, and comments that describe unfinished work.",
|
|
24
|
+
"Confirm the implementation follows the existing visual language instead of adding isolated inline styling or decorative motion not requested by the user."
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export function acceptanceReviewerPrompt(request) {
|
|
2
|
+
if (typeof request !== 'string' || !request.trim()) throw Error('Original request is required');
|
|
3
|
+
return `You are a fresh-context acceptance reviewer. Evaluate the original request below against this workspace. Do not modify product source or existing tests, commit, push, or change branches. Put any temporary evaluator artifacts only in artifacts/acceptance-review. Do not trust the author's summary or passing checks. Inspect actual entrypoints and new/changed code. Execute bounded independent falsification probes of the highest-impact assumptions: producer/consumer representation agreement, real input-to-observable-result correspondence, safety, inverses and boundaries. Test normal execution separately from alternate test modes. Expected results must follow the specification or independent library conventions, not duplicate the implementation. Successful process exit or boolean activity flags do not prove correctness. Never kill unrelated processes. Report concrete prioritized defects with reproduction commands/results; distinguish unavailable checks from failures. End with VERDICT: APPROVE or VERDICT: CHANGES-REQUESTED. This review is model-authored, not an independent correctness guarantee.\n\nOriginal request:\n${request}`;
|
|
4
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled browser provisioning — downloads the lightweight `chrome-headless-shell`
|
|
3
|
+
* build from Chrome for Testing on demand and caches it under the carthagent
|
|
4
|
+
* data dir. Zero npm deps; zip entries are extracted with node:zlib.
|
|
5
|
+
*
|
|
6
|
+
* Resolution order for checks: CARTHAGENT_BROWSER env → bundled shell →
|
|
7
|
+
* system chrome/chromium → Firefox (legacy fallback).
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
|
|
10
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { inflateRawSync } from 'node:zlib';
|
|
14
|
+
|
|
15
|
+
export const BUNDLED_BROWSER_VERSION = process.env.CARTHAGENT_BROWSER_VERSION || '154.0.8037.92';
|
|
16
|
+
|
|
17
|
+
const SHELL_URL = version => `https://storage.googleapis.com/chrome-for-testing-public/${version}/${platformKey()}/chrome-headless-shell-${platformKey()}.zip`;
|
|
18
|
+
|
|
19
|
+
function platformKey() {
|
|
20
|
+
if (process.platform === 'linux') return process.arch === 'arm64' ? 'linux-arm64' : 'linux64';
|
|
21
|
+
if (process.platform === 'darwin') return process.arch === 'arm64' ? 'mac-arm64' : 'mac-x64';
|
|
22
|
+
if (process.platform === 'win32') return process.arch === 'x64' ? 'win64' : 'win32';
|
|
23
|
+
throw Error(`Unsupported platform ${process.platform}/${process.arch} for the bundled browser`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function browserRoot(base = join(homedir(), '.carthagent')) {
|
|
27
|
+
return join(base, 'browser');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Expected path of the bundled shell binary for this platform. */
|
|
31
|
+
export function bundledBinaryPath({ version = BUNDLED_BROWSER_VERSION, base } = {}) {
|
|
32
|
+
const key = platformKey();
|
|
33
|
+
const name = process.platform === 'win32' ? 'chrome-headless-shell.exe' : 'chrome-headless-shell';
|
|
34
|
+
return join(browserRoot(base), version, `chrome-headless-shell-${key}`, name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** True when Chromium's stderr indicates the process sandbox cannot start
|
|
38
|
+
* (Ubuntu ≥23.10 AppArmor, containers without userns). The caller should
|
|
39
|
+
* retry once with --no-sandbox. */
|
|
40
|
+
export function isSandboxFailure(stderr = '') {
|
|
41
|
+
return /unprivileged user namespaces|apparmor|SUID sandbox|cannot run as root|sandbox/i.test(stderr);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Locate a usable headless browser: env → bundled → system chrome/chromium → firefox. */
|
|
45
|
+
export function findBrowser({ env = process.env } = {}) {
|
|
46
|
+
const chromium = findChromium({ env });
|
|
47
|
+
if (chromium) return chromium;
|
|
48
|
+
if (env.FIREFOX && existsSync(env.FIREFOX)) return env.FIREFOX;
|
|
49
|
+
for (const candidate of ['/usr/bin/firefox', '/snap/bin/firefox']) if (existsSync(candidate)) return candidate;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Chromium-family binary only (CDP/flag dialect): env → bundled → system chrome/chromium. */
|
|
54
|
+
export function findChromium({ env = process.env } = {}) {
|
|
55
|
+
if (env.CARTHAGENT_BROWSER && existsSync(env.CARTHAGENT_BROWSER)) return env.CARTHAGENT_BROWSER;
|
|
56
|
+
const bundled = bundledBinaryPath();
|
|
57
|
+
if (existsSync(bundled)) return bundled;
|
|
58
|
+
const names = process.platform === 'win32'
|
|
59
|
+
? ['chrome-headless-shell.exe', 'chrome.exe', 'chromium.exe']
|
|
60
|
+
: ['chrome-headless-shell', 'google-chrome-stable', 'google-chrome', 'chromium', 'chromium-browser'];
|
|
61
|
+
for (const dir of (env.PATH || '').split(delimiter)) {
|
|
62
|
+
for (const name of names) {
|
|
63
|
+
const candidate = join(dir, name);
|
|
64
|
+
if (existsSync(candidate)) return candidate;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Minimal zip extractor (store + deflate). Parses the end-of-central-directory
|
|
72
|
+
* record and central directory, then reads each local file entry. Sufficient
|
|
73
|
+
* for Chrome for Testing archives without shelling out to unzip/tar.
|
|
74
|
+
*/
|
|
75
|
+
export function extractZip(zipPath, destDir) {
|
|
76
|
+
const buf = readFileSync(zipPath);
|
|
77
|
+
const eocdSig = 0x06054b50;
|
|
78
|
+
let eocd = -1;
|
|
79
|
+
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 65536); i--) {
|
|
80
|
+
if (buf.readUInt32LE(i) === eocdSig) { eocd = i; break; }
|
|
81
|
+
}
|
|
82
|
+
if (eocd < 0) throw Error('zip: end of central directory not found');
|
|
83
|
+
const count = buf.readUInt16LE(eocd + 10);
|
|
84
|
+
let offset = buf.readUInt32LE(eocd + 16);
|
|
85
|
+
const written = [];
|
|
86
|
+
for (let i = 0; i < count; i++) {
|
|
87
|
+
if (buf.readUInt32LE(offset) !== 0x02014b50) throw Error('zip: corrupt central directory');
|
|
88
|
+
const method = buf.readUInt16LE(offset + 10);
|
|
89
|
+
const compressedSize = buf.readUInt32LE(offset + 20);
|
|
90
|
+
const nameLen = buf.readUInt16LE(offset + 28);
|
|
91
|
+
const extraLen = buf.readUInt16LE(offset + 30);
|
|
92
|
+
const commentLen = buf.readUInt16LE(offset + 32);
|
|
93
|
+
const name = buf.subarray(offset + 46, offset + 46 + nameLen).toString('utf8');
|
|
94
|
+
const localOffset = buf.readUInt32LE(offset + 42);
|
|
95
|
+
offset += 46 + nameLen + extraLen + commentLen;
|
|
96
|
+
if (!name || name.endsWith('/')) continue;
|
|
97
|
+
const rel = name.split('\\').join('/');
|
|
98
|
+
if (rel.split('/').some(seg => seg === '..' || seg === '' || isDangerousAbsolute(rel))) throw Error(`zip: refusing entry ${rel}`);
|
|
99
|
+
if (buf.readUInt32LE(localOffset) !== 0x04034b50) throw Error('zip: corrupt local header');
|
|
100
|
+
const localNameLen = buf.readUInt16LE(localOffset + 26);
|
|
101
|
+
const localExtraLen = buf.readUInt16LE(localOffset + 28);
|
|
102
|
+
const dataStart = localOffset + 30 + localNameLen + localExtraLen;
|
|
103
|
+
const raw = buf.subarray(dataStart, dataStart + compressedSize);
|
|
104
|
+
const data = method === 0 ? raw : method === 8 ? inflateRawSync(raw) : null;
|
|
105
|
+
if (data == null) throw Error(`zip: unsupported compression method ${method} for ${rel}`);
|
|
106
|
+
const out = join(destDir, rel);
|
|
107
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
108
|
+
writeFileSync(out, data);
|
|
109
|
+
written.push(out);
|
|
110
|
+
}
|
|
111
|
+
return written;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isDangerousAbsolute(rel) {
|
|
115
|
+
return rel.startsWith('/') || /^[a-zA-Z]:\//.test(rel);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Download + cache the bundled headless shell for this platform.
|
|
120
|
+
* `CARTHAGENT_BROWSER_VERSION` pins a specific Chrome for Testing build; the
|
|
121
|
+
* default tracks the last-known-good stable version recorded at build time.
|
|
122
|
+
* Returns the binary path. Download artifacts land in a sibling temp dir and
|
|
123
|
+
* are renamed into place so interrupted installs never leave a half binary.
|
|
124
|
+
*/
|
|
125
|
+
export async function ensureBundledBrowser({ version = BUNDLED_BROWSER_VERSION, fetchImpl = globalThis.fetch, base } = {}) {
|
|
126
|
+
const bin = bundledBinaryPath({ version, base });
|
|
127
|
+
if (existsSync(bin)) return bin;
|
|
128
|
+
if (typeof fetchImpl !== 'function') throw Error('Bundled browser download requires fetch support');
|
|
129
|
+
const root = browserRoot(base);
|
|
130
|
+
mkdirSync(root, { recursive: true });
|
|
131
|
+
// Stage under `root` (not tmpdir) so the final rename stays on one filesystem.
|
|
132
|
+
const stage = mkdtempSync(join(root, '.stage-'));
|
|
133
|
+
try {
|
|
134
|
+
const url = SHELL_URL(version);
|
|
135
|
+
const response = await fetchImpl(url);
|
|
136
|
+
if (!response.ok) throw Error(`Bundled browser download failed (${response.status}) for ${url}`);
|
|
137
|
+
const zip = join(stage, 'shell.zip');
|
|
138
|
+
writeFileSync(zip, Buffer.from(await response.arrayBuffer()));
|
|
139
|
+
const extracted = join(stage, 'out');
|
|
140
|
+
extractZip(zip, extracted);
|
|
141
|
+
const leaf = `chrome-headless-shell-${platformKey()}`;
|
|
142
|
+
const target = join(root, version);
|
|
143
|
+
rmSync(target, { recursive: true, force: true });
|
|
144
|
+
mkdirSync(target, { recursive: true });
|
|
145
|
+
renameSync(join(extracted, leaf), join(target, leaf));
|
|
146
|
+
const name = process.platform === 'win32' ? 'chrome-headless-shell.exe' : 'chrome-headless-shell';
|
|
147
|
+
const binary = join(target, leaf, name);
|
|
148
|
+
if (!existsSync(binary)) throw Error('Bundled browser archive did not contain the shell binary');
|
|
149
|
+
if (process.platform !== 'win32') chmodSync(binary, 0o755);
|
|
150
|
+
// Cheap self-check: the shell should answer --version without a display.
|
|
151
|
+
const probe = spawnSync(binary, ['--version'], { encoding: 'utf8', timeout: 15000 });
|
|
152
|
+
if (probe.status !== 0) {
|
|
153
|
+
rmSync(target, { recursive: true, force: true });
|
|
154
|
+
throw Error(`Bundled browser failed --version probe: ${(probe.stderr || probe.error?.message || '').slice(0, 200)}`);
|
|
155
|
+
}
|
|
156
|
+
return binary;
|
|
157
|
+
} finally {
|
|
158
|
+
rmSync(stage, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Firefox WebDriver BiDi log capture. Subscribe before navigation, never silently degrade. */
|
|
2
|
+
export async function captureBrowserErrors(url) {
|
|
3
|
+
if (!url) throw Error('Driver did not provide a BiDi webSocketUrl; error capture unavailable');
|
|
4
|
+
const socket = new WebSocket(url);
|
|
5
|
+
const pending = new Map();
|
|
6
|
+
const errors = [];
|
|
7
|
+
let dropped = 0;
|
|
8
|
+
let sequence = 0;
|
|
9
|
+
let closed = false;
|
|
10
|
+
socket.addEventListener('message', event => {
|
|
11
|
+
let message;
|
|
12
|
+
try { message = JSON.parse(event.data); } catch { return; }
|
|
13
|
+
if (message.id !== undefined) {
|
|
14
|
+
const handler = pending.get(message.id);
|
|
15
|
+
if (handler) { pending.delete(message.id); handler(message); }
|
|
16
|
+
}
|
|
17
|
+
if (message.method === 'log.entryAdded' && message.params.level === 'error') {
|
|
18
|
+
const entry = message.params;
|
|
19
|
+
if (errors.length === 50) { dropped++; return; }
|
|
20
|
+
errors.push({ type: entry.type, text: String(entry.text ?? '').slice(0, 2000),
|
|
21
|
+
stack: (entry.stackTrace?.callFrames || []).slice(0, 8).map(frame => ({ url: String(frame.url).slice(0, 500), lineNumber: frame.lineNumber, columnNumber: frame.columnNumber })) });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
socket.addEventListener('close', () => { closed = true; });
|
|
25
|
+
async function command(method, params) {
|
|
26
|
+
if (closed) throw Error('Browser error capture connection closed');
|
|
27
|
+
const id = ++sequence;
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const timer = setTimeout(() => { pending.delete(id); reject(Error(`BiDi ${method} timed out`)); }, 5000);
|
|
30
|
+
pending.set(id, message => {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
if (message.type === 'error') reject(Error(`BiDi ${method}: ${message.message}`));
|
|
33
|
+
else resolve(message.result);
|
|
34
|
+
});
|
|
35
|
+
socket.send(JSON.stringify({ id, method, params }));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
await new Promise((resolve, reject) => {
|
|
40
|
+
const timer = setTimeout(() => reject(Error('BiDi connection timed out')), 5000);
|
|
41
|
+
socket.addEventListener('open', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
42
|
+
socket.addEventListener('error', () => { clearTimeout(timer); reject(Error('BiDi connection failed')); }, { once: true });
|
|
43
|
+
});
|
|
44
|
+
await command('session.subscribe', { events: ['log.entryAdded'] });
|
|
45
|
+
} catch (error) { socket.close(); throw error; }
|
|
46
|
+
return {
|
|
47
|
+
async inspect() {
|
|
48
|
+
// Round trip on the same event channel before consuming the observed log.
|
|
49
|
+
await command('session.status', {});
|
|
50
|
+
return { errors: [...errors], dropped };
|
|
51
|
+
},
|
|
52
|
+
close() { socket.close(); }
|
|
53
|
+
};
|
|
54
|
+
}
|
package/lib/browser.mjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser testing helpers for delivery checks — fully repo-contained.
|
|
3
|
+
*
|
|
4
|
+
* Three capability tiers:
|
|
5
|
+
* serve — static file server over node:http (zero deps)
|
|
6
|
+
* dom — jsdom DOM assertions with real inline-script execution (npm dep)
|
|
7
|
+
* shot — headless shell screenshot: bundled chrome-headless-shell, system
|
|
8
|
+
* Chrome/Chromium, or legacy Firefox fallback (zero npm deps)
|
|
9
|
+
*
|
|
10
|
+
* Used by tools/browser-check.mjs; also importable for custom check scripts.
|
|
11
|
+
*/
|
|
12
|
+
import { createServer } from 'node:http';
|
|
13
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
14
|
+
import { delimiter, extname, join, resolve, normalize, isAbsolute, relative } from 'node:path';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { isSandboxFailure } from './browser-bin.mjs';
|
|
18
|
+
// Load jsdom only for DOM checks; static serving and WebDriver need no DOM runtime.
|
|
19
|
+
|
|
20
|
+
const MIME = {
|
|
21
|
+
'.html': 'text/html; charset=utf-8', '.htm': 'text/html; charset=utf-8',
|
|
22
|
+
'.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8',
|
|
23
|
+
'.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8',
|
|
24
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
|
|
25
|
+
'.svg': 'image/svg+xml', '.webp': 'image/webp', '.ico': 'image/x-icon',
|
|
26
|
+
'.wasm': 'application/wasm', '.txt': 'text/plain; charset=utf-8',
|
|
27
|
+
'.woff': 'font/woff', '.woff2': 'font/woff2', '.map': 'application/json',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Start a static file server rooted at `root`. Returns { server, port, url, close }. */
|
|
31
|
+
export function serveDir(root, { port = 0 } = {}) {
|
|
32
|
+
const base = resolve(root);
|
|
33
|
+
const server = createServer((req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(req.url, 'http://x');
|
|
36
|
+
let rel = decodeURIComponent(url.pathname);
|
|
37
|
+
if (rel.endsWith('/')) rel += 'index.html';
|
|
38
|
+
const full = normalize(join(base, rel));
|
|
39
|
+
const local = relative(base, full);
|
|
40
|
+
if (local === '..' || local.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || isAbsolute(local)) { res.writeHead(403); res.end('forbidden'); return; }
|
|
41
|
+
if (!existsSync(full) || !statSync(full).isFile()) { res.writeHead(404); res.end('not found'); return; }
|
|
42
|
+
res.writeHead(200, { 'content-type': MIME[extname(full).toLowerCase()] || 'application/octet-stream' });
|
|
43
|
+
res.end(readFileSync(full));
|
|
44
|
+
} catch (e) { res.writeHead(500); res.end(String(e)); }
|
|
45
|
+
});
|
|
46
|
+
return new Promise((ok, err) => {
|
|
47
|
+
server.once('error', err);
|
|
48
|
+
server.listen(port, '127.0.0.1', () => {
|
|
49
|
+
const p = server.address().port;
|
|
50
|
+
ok({ server, port: p, url: `http://127.0.0.1:${p}`, close: () => server.close() });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Screenshot tier: prefer the bundled/system headless shell
|
|
57
|
+
* (`chrome-headless-shell --screenshot` — no display or driver needed), fall
|
|
58
|
+
* back to Firefox when no Chromium-family binary is found. `browser` is the
|
|
59
|
+
* resolved binary path; pass the result of findBrowser()/ensureBundledBrowser()
|
|
60
|
+
* or a known binary name ('firefox' selects the Firefox flag dialect).
|
|
61
|
+
* Returns { ok, out, engine, stderr }.
|
|
62
|
+
*/
|
|
63
|
+
export function browserScreenshot(url, out, { width = 1280, height = 800, timeoutMs = 45000, waitSeconds, browser } = {}) {
|
|
64
|
+
const isFirefox = !browser || /firefox/i.test(browser);
|
|
65
|
+
if (isFirefox) return firefoxScreenshot(url, out, { width, height, timeoutMs, waitSeconds });
|
|
66
|
+
const abs = isAbsolute(out) ? out : resolve(out);
|
|
67
|
+
const attempt = noSandbox => new Promise((ok) => {
|
|
68
|
+
const profile = mkdtempSync(join(tmpdir(), 'carthagent-chrome-shot-'));
|
|
69
|
+
const args = [
|
|
70
|
+
...(noSandbox ? ['--no-sandbox'] : []),
|
|
71
|
+
'--user-data-dir=' + profile, '--no-first-run', '--no-default-browser-check',
|
|
72
|
+
'--disable-extensions', '--mute-audio', '--hide-scrollbars',
|
|
73
|
+
'--screenshot=' + abs, `--window-size=${width},${height}`,
|
|
74
|
+
];
|
|
75
|
+
// --virtual-time-budget fast-forwards timers/animation deterministically.
|
|
76
|
+
if (waitSeconds != null) args.push(`--virtual-time-budget=${Math.ceil(waitSeconds * 1000)}`);
|
|
77
|
+
args.push(url);
|
|
78
|
+
const child = spawn(browser, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
79
|
+
let stderr = '';
|
|
80
|
+
child.stderr.on('data', d => { stderr += d; });
|
|
81
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); }, timeoutMs);
|
|
82
|
+
const done = extra => { clearTimeout(timer); rmSync(profile, { recursive: true, force: true }); ok({ out: abs, stderr: stderr.slice(-2000), engine: 'chrome-headless-shell', ...extra }); };
|
|
83
|
+
child.on('close', code => done({ ok: code === 0 && existsSync(abs), code }));
|
|
84
|
+
child.on('error', e => done({ ok: false, error: String(e) }));
|
|
85
|
+
});
|
|
86
|
+
// Environments that block Chromium's sandbox (Ubuntu >=23.10 AppArmor,
|
|
87
|
+
// containers) get one retry with --no-sandbox.
|
|
88
|
+
return attempt(false).then(result => (result.ok || !isSandboxFailure(result.stderr || '')) ? result : attempt(true));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Find the firefox binary. Honors FIREFOX env var, then PATH, then common snap paths. */
|
|
92
|
+
export function findFirefox() {
|
|
93
|
+
if (process.env.FIREFOX) return process.env.FIREFOX;
|
|
94
|
+
const names = process.platform === 'win32' ? ['firefox.exe', 'firefox'] : ['firefox'];
|
|
95
|
+
for (const dir of (process.env.PATH || '').split(delimiter)) {
|
|
96
|
+
for (const name of names) {
|
|
97
|
+
const candidate = join(dir, name);
|
|
98
|
+
if (existsSync(candidate)) return candidate;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const candidate of ['/usr/bin/firefox', '/snap/bin/firefox']) if (existsSync(candidate)) return candidate;
|
|
102
|
+
return names[0];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Take a screenshot with the system Firefox (headless). Zero npm deps.
|
|
107
|
+
* Firefox snap can only write inside $HOME — `out` must be an absolute path under HOME.
|
|
108
|
+
* Returns { ok, out, stderr }.
|
|
109
|
+
*/
|
|
110
|
+
export function firefoxScreenshot(url, out, { width = 1280, height = 800, timeoutMs = 45000, waitSeconds } = {}) {
|
|
111
|
+
return new Promise((ok) => {
|
|
112
|
+
const abs = isAbsolute(out) ? out : resolve(out);
|
|
113
|
+
const profile = mkdtempSync(join(tmpdir(), 'carthagent-firefox-'));
|
|
114
|
+
const args = ['--headless', '--no-remote', '--profile', profile, '--screenshot', abs, '--window-size', `${width},${height}`];
|
|
115
|
+
if (waitSeconds != null) args.push('--screenshot-delay', String(waitSeconds));
|
|
116
|
+
args.push(url);
|
|
117
|
+
const child = spawn(findFirefox(), args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
118
|
+
let stderr = '';
|
|
119
|
+
child.stderr.on('data', d => { stderr += d; });
|
|
120
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); }, timeoutMs);
|
|
121
|
+
child.on('close', code => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
rmSync(profile, { recursive: true, force: true });
|
|
124
|
+
ok({ ok: code === 0 && existsSync(abs), out: abs, code, stderr: stderr.slice(-2000) });
|
|
125
|
+
});
|
|
126
|
+
child.on('error', e => {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
rmSync(profile, { recursive: true, force: true });
|
|
129
|
+
ok({ ok: false, out: abs, error: String(e), stderr });
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Load a page in jsdom (executes inline + same-origin scripts), then run assertions.
|
|
136
|
+
*
|
|
137
|
+
* assertions: array of
|
|
138
|
+
* { selector, count? } — element exists / exact count
|
|
139
|
+
* { selector, text?: string|RegExp } — textContent contains/matches
|
|
140
|
+
* { selector, attr, value? } — attribute equals/contains
|
|
141
|
+
* { action:'click', selector, then:assert } — dispatch click, then assert
|
|
142
|
+
* { action:'type', selector, value, then } — set input value + input event
|
|
143
|
+
* { action:'key', key, selector?, then } — KeyboardEvent on element/document
|
|
144
|
+
* { eval: string, expect?: any } — evaluate JS in page context, check result
|
|
145
|
+
* { console: 'error-free' } — no console.error/pageerror captured
|
|
146
|
+
*
|
|
147
|
+
* Module scripts are unsupported by jsdom and fail execution-enabled checks.
|
|
148
|
+
* Use runScripts:false only for static DOM inspection, not application verification.
|
|
149
|
+
* Returns { pass, failures:[...], consoleErrors:[...], document }.
|
|
150
|
+
*/
|
|
151
|
+
export async function domCheck(source, assertions = [], { url, runScripts = true, waitMs = 250 } = {}) {
|
|
152
|
+
const { JSDOM, VirtualConsole } = await import('jsdom');
|
|
153
|
+
const consoleErrors = [];
|
|
154
|
+
const vc = new VirtualConsole();
|
|
155
|
+
vc.on('jsdomError', e => consoleErrors.push(String(e.detail || e)));
|
|
156
|
+
vc.on('error', (...a) => consoleErrors.push(a.join(' ')));
|
|
157
|
+
|
|
158
|
+
const html = source.startsWith('http') ? await (await fetch(source)).text() : source;
|
|
159
|
+
const dom = new JSDOM(html, {
|
|
160
|
+
url: url || (source.startsWith('http') ? source : 'http://localhost/'),
|
|
161
|
+
runScripts: runScripts ? 'dangerously' : 'outside-only',
|
|
162
|
+
resources: 'usable',
|
|
163
|
+
pretendToBeVisual: true,
|
|
164
|
+
virtualConsole: vc,
|
|
165
|
+
});
|
|
166
|
+
if (runScripts) await new Promise(r => setTimeout(r, waitMs)); // let inline scripts settle
|
|
167
|
+
|
|
168
|
+
const { document } = dom.window;
|
|
169
|
+
const failures = [];
|
|
170
|
+
if (runScripts && [...document.querySelectorAll('script')].some(script =>
|
|
171
|
+
(script.getAttribute('type') || '').toLowerCase() === 'module')) {
|
|
172
|
+
failures.push('Unsupported module scripts: jsdom does not execute them. Use a real-browser check for application verification, or runScripts:false for static DOM inspection only.');
|
|
173
|
+
}
|
|
174
|
+
const run = a => {
|
|
175
|
+
try {
|
|
176
|
+
if (a.eval) {
|
|
177
|
+
const result = dom.window.eval(a.eval);
|
|
178
|
+
if ('expect' in a && JSON.stringify(result) !== JSON.stringify(a.expect))
|
|
179
|
+
failures.push(`eval ${JSON.stringify(a.eval)} → ${JSON.stringify(result)}, expected ${JSON.stringify(a.expect)}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (a.console === 'error-free') return; // checked at end
|
|
183
|
+
if (a.action === 'click') {
|
|
184
|
+
const el = document.querySelector(a.selector);
|
|
185
|
+
if (!el) { failures.push(`click: ${a.selector} not found`); return; }
|
|
186
|
+
el.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true }));
|
|
187
|
+
} else if (a.action === 'type') {
|
|
188
|
+
const el = document.querySelector(a.selector);
|
|
189
|
+
if (!el) { failures.push(`type: ${a.selector} not found`); return; }
|
|
190
|
+
el.value = a.value;
|
|
191
|
+
el.dispatchEvent(new dom.window.Event('input', { bubbles: true }));
|
|
192
|
+
} else if (a.action === 'key') {
|
|
193
|
+
const target = a.selector ? document.querySelector(a.selector) : document;
|
|
194
|
+
if (!target) { failures.push(`key: ${a.selector} not found`); return; }
|
|
195
|
+
target.dispatchEvent(new dom.window.KeyboardEvent('keydown', { key: a.key, bubbles: true }));
|
|
196
|
+
}
|
|
197
|
+
if (a.then) run(a.then);
|
|
198
|
+
if (!a.selector) return;
|
|
199
|
+
const els = document.querySelectorAll(a.selector);
|
|
200
|
+
if (a.count != null && els.length !== a.count) { failures.push(`${a.selector}: count ${els.length} ≠ ${a.count}`); return; }
|
|
201
|
+
if (a.count == null && !els.length) { failures.push(`${a.selector}: not found`); return; }
|
|
202
|
+
const el = els[0];
|
|
203
|
+
if (a.text != null) {
|
|
204
|
+
const t = el.textContent || '';
|
|
205
|
+
const okText = a.text instanceof RegExp ? a.text.test(t) : t.includes(a.text);
|
|
206
|
+
if (!okText) failures.push(`${a.selector}: text ${JSON.stringify(t.slice(0, 80))} does not match ${a.text}`);
|
|
207
|
+
}
|
|
208
|
+
if (a.attr != null) {
|
|
209
|
+
const v = el.getAttribute(a.attr);
|
|
210
|
+
const okAttr = a.value == null ? v != null : String(v).includes(a.value);
|
|
211
|
+
if (!okAttr) failures.push(`${a.selector}[${a.attr}]: ${JSON.stringify(v)} does not match ${a.value ?? '(any)'}`);
|
|
212
|
+
}
|
|
213
|
+
} catch (e) { failures.push(`${a.selector || a.eval || '?'}: ${e.message}`); }
|
|
214
|
+
};
|
|
215
|
+
for (const a of assertions) run(a);
|
|
216
|
+
if (assertions.some(a => a.console === 'error-free') && consoleErrors.length)
|
|
217
|
+
failures.push(`console errors: ${consoleErrors.slice(0, 3).join(' | ')}`);
|
|
218
|
+
return { pass: failures.length === 0, failures, consoleErrors, document, window: dom.window };
|
|
219
|
+
}
|