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
package/lib/engine.mjs
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locating the agent CLI + building its argument list — shared by the console
|
|
3
|
+
* (bin/ctg.mjs, lib/tui/app.mjs) and the evaluation runner (evaluate.mjs).
|
|
4
|
+
* carthagent uses a baked-in agent engine in vendor/agent and an isolated state directory
|
|
5
|
+
* (~/.carthagent/agent) to ensure it runs completely fresh without piggybacking on any
|
|
6
|
+
* host engine installation.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync, statSync, openSync, fstatSync, readSync, closeSync } from 'node:fs';
|
|
9
|
+
import { join, dirname, resolve, basename } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import { validateBudgetOptions } from './budget.mjs';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const ROOT = resolve(__dirname, '..');
|
|
16
|
+
|
|
17
|
+
// Ensure an isolated state directory (~/.carthagent/agent) by default:
|
|
18
|
+
const DEFAULT_AGENT_DIR = process.env.CARTHAGENT_AGENT_DIR || process.env.CARTHAGENT_CODING_AGENT_DIR || join(os.homedir(), '.carthagent', 'agent');
|
|
19
|
+
if (!process.env.CARTHAGENT_CODING_AGENT_DIR && !process.env.CARTHAGENT_AGENT_DIR) {
|
|
20
|
+
process.env.CARTHAGENT_CODING_AGENT_DIR = DEFAULT_AGENT_DIR;
|
|
21
|
+
}
|
|
22
|
+
if (!process.env.PI_CODING_AGENT_DIR) {
|
|
23
|
+
process.env.PI_CODING_AGENT_DIR = process.env.CARTHAGENT_CODING_AGENT_DIR || DEFAULT_AGENT_DIR;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function locateEngine(explicit) {
|
|
27
|
+
const candidates = [explicit, process.env.CARTHAGENT_CLI, process.env.PI_CLI].filter(Boolean);
|
|
28
|
+
for (const c of candidates) {
|
|
29
|
+
if (existsSync(c)) return { cmd: process.execPath, args: [c] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Self-contained baked-in agent engine inside the package.
|
|
33
|
+
const baked = join(ROOT, 'vendor', 'agent', 'cli.js');
|
|
34
|
+
if (existsSync(baked)) return { cmd: process.execPath, args: [baked] };
|
|
35
|
+
|
|
36
|
+
throw new Error('Cannot find the baked-in agent engine in vendor/agent/cli.js — pass --agent-cli /path/to/cli.js');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Build engine CLI args for both the TUI (rpc) and headless (-p) paths. */
|
|
40
|
+
export function buildEngineArgs(opts) {
|
|
41
|
+
validateBudgetOptions(opts);
|
|
42
|
+
const args = [];
|
|
43
|
+
if (opts.json) args.push('--mode', 'json');
|
|
44
|
+
if (opts.provider) args.push('--provider', opts.provider);
|
|
45
|
+
if (opts.model) args.push('--model', opts.model);
|
|
46
|
+
if (opts.thinking) args.push('--thinking', opts.thinking);
|
|
47
|
+
// Session selection: --session <path|id> or --continue for the most recent.
|
|
48
|
+
// (--resume is intentionally not forwarded — agent picker is interactive.)
|
|
49
|
+
if (opts.session) args.push('--session', opts.session);
|
|
50
|
+
else if (opts.continue) args.push('--continue');
|
|
51
|
+
if (opts.isolate) args.push('--no-extensions', '--no-skills', '--no-prompt-templates');
|
|
52
|
+
// Product providers load independently of the delivery extension so they are
|
|
53
|
+
// available in the TUI, RPC child, headless runs, and --no-delivery mode.
|
|
54
|
+
args.push('-e', join(ROOT, 'extensions', 'experiential.ts'));
|
|
55
|
+
args.push('-e', join(ROOT, 'extensions', 'subscriptions.ts'));
|
|
56
|
+
args.push('-e', join(ROOT, 'extensions', 'codegraph.ts'));
|
|
57
|
+
if (opts.codegraphMaxFiles !== undefined) {
|
|
58
|
+
if (!/^\d+$/.test(String(opts.codegraphMaxFiles)) || !Number.isSafeInteger(Number(opts.codegraphMaxFiles)) || Number(opts.codegraphMaxFiles) < 1) throw Error('codegraph-max-files must be a positive safe integer');
|
|
59
|
+
args.push('--codegraph-max-files', String(opts.codegraphMaxFiles));
|
|
60
|
+
}
|
|
61
|
+
if (opts.delivery !== false) {
|
|
62
|
+
const workflow = opts.workflow ?? 'standard';
|
|
63
|
+
if (!['standard', 'strict'].includes(workflow)) throw Error('workflow must be standard or strict');
|
|
64
|
+
args.push('-e', join(ROOT, 'extensions', 'delivery.ts'),
|
|
65
|
+
'--skill', join(ROOT, 'skills'),
|
|
66
|
+
'--prompt-template', join(ROOT, 'prompts'));
|
|
67
|
+
args.push('--delivery-workflow', workflow);
|
|
68
|
+
if (opts.strict !== false) args.push('--delivery-strict');
|
|
69
|
+
if (opts.validators) args.push('--delivery-validators', resolve(opts.cwd || process.cwd(), opts.validators));
|
|
70
|
+
if (opts.context) args.push('--delivery-context', resolve(opts.cwd || process.cwd(), opts.context));
|
|
71
|
+
for (const [key, flag] of [['maxTools', 'tools'], ['maxSeconds', 'seconds'], ['maxRepairs', 'repairs']]) {
|
|
72
|
+
if (opts[key] !== undefined) args.push(`--delivery-max-${flag}`, String(opts[key]));
|
|
73
|
+
}
|
|
74
|
+
if (opts.bashCap) args.push('--delivery-bash-cap', String(opts.bashCap));
|
|
75
|
+
// Review is orchestrated by the console, not an engine extension flag.
|
|
76
|
+
// Forwarding --delivery-review makes the bundled engine reject startup.
|
|
77
|
+
}
|
|
78
|
+
return args;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ------------------------------------------------------------- session files
|
|
82
|
+
|
|
83
|
+
/** carthagent's per-cwd session dir: <agentDir>/sessions/--a-b-c--/. Defaults to ~/.carthagent/agent. */
|
|
84
|
+
export function sessionDirFor(cwd = process.cwd(), agentDir = process.env.CARTHAGENT_AGENT_DIR || process.env.CARTHAGENT_CODING_AGENT_DIR || process.env.PI_CODING_AGENT_DIR || join(os.homedir(), '.carthagent', 'agent')) {
|
|
85
|
+
const safe = `--${resolve(cwd).replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
|
|
86
|
+
return join(agentDir, 'sessions', safe);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function textOfContent(content) {
|
|
90
|
+
if (!Array.isArray(content)) return typeof content === 'string' ? content : '';
|
|
91
|
+
return content.filter(c => c?.type === 'text').map(c => c.text).join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Read bounded metadata from one session file: header, latest name, first user message. */
|
|
95
|
+
function readSessionMeta(path, mtime) {
|
|
96
|
+
const meta = { path, id: basename(path, '.jsonl').split('_').pop() || '', name: null, created: null, modified: mtime, messageCount: 0, firstMessage: '' };
|
|
97
|
+
let fd;
|
|
98
|
+
try {
|
|
99
|
+
fd = openSync(path, 'r');
|
|
100
|
+
const size = Math.min(4 * 1024 * 1024, fstatSync(fd).size);
|
|
101
|
+
const buf = Buffer.alloc(size);
|
|
102
|
+
readSync(fd, buf, 0, size, 0);
|
|
103
|
+
for (const line of buf.toString('utf8').split('\n')) {
|
|
104
|
+
if (!line) continue;
|
|
105
|
+
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
106
|
+
if (e.type === 'session') { meta.id = e.id || meta.id; meta.created = e.timestamp || null; }
|
|
107
|
+
else if (e.type === 'session_info' && typeof e.name === 'string' && e.name.trim()) meta.name = e.name.trim();
|
|
108
|
+
else if (e.type === 'message') {
|
|
109
|
+
meta.messageCount++;
|
|
110
|
+
if (!meta.firstMessage && e.message?.role === 'user') {
|
|
111
|
+
const t = textOfContent(e.message.content).replace(/\s+/g, ' ').trim();
|
|
112
|
+
if (t) meta.firstMessage = t.slice(0, 200);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch { /* corrupt/partial file — return what we have */ }
|
|
117
|
+
finally { try { if (fd !== undefined) closeSync(fd); } catch { } }
|
|
118
|
+
return meta;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Sessions stored for a cwd, newest first. `dir` overrides discovery (tests). */
|
|
122
|
+
export function listSessions(cwd = process.cwd(), { limit = 80, dir = sessionDirFor(cwd) } = {}) {
|
|
123
|
+
let files;
|
|
124
|
+
try { files = readdirSync(dir).filter(f => f.endsWith('.jsonl')); } catch { return []; }
|
|
125
|
+
const rows = [];
|
|
126
|
+
for (const f of files) {
|
|
127
|
+
const path = join(dir, f);
|
|
128
|
+
try { rows.push({ path, mtime: statSync(path).mtimeMs }); } catch { }
|
|
129
|
+
}
|
|
130
|
+
rows.sort((a, b) => b.mtime - a.mtime);
|
|
131
|
+
return rows.slice(0, limit).map(r => readSessionMeta(r.path, r.mtime));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Newest session file for a cwd, or null. */
|
|
135
|
+
export function mostRecentSession(cwd = process.cwd(), dir) {
|
|
136
|
+
return listSessions(cwd, { limit: 1, dir })[0]?.path || null;
|
|
137
|
+
}
|
package/lib/guidance.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { unframe } from './tui/framing.mjs';
|
|
5
|
+
|
|
6
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
export const DEFAULT_PROFILE_DIR = join(root, 'guidance', 'profiles');
|
|
8
|
+
|
|
9
|
+
export function loadGuidanceProfiles(dir = DEFAULT_PROFILE_DIR) {
|
|
10
|
+
if (!existsSync(dir)) return [];
|
|
11
|
+
return readdirSync(dir)
|
|
12
|
+
.filter(file => file.endsWith('.json'))
|
|
13
|
+
.sort()
|
|
14
|
+
.map(file => validateProfile(JSON.parse(readFileSync(join(dir, file), 'utf8')), file))
|
|
15
|
+
.sort((a, b) => b.priority - a.priority || a.id.localeCompare(b.id));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function routeGuidance(request, { cwd = process.cwd(), profiles = loadGuidanceProfiles() } = {}) {
|
|
19
|
+
// Harness instructions are not user requirements. Route only the task.
|
|
20
|
+
const text = normalize(unframe(request) ?? request);
|
|
21
|
+
const dependencies = projectDependencies(cwd);
|
|
22
|
+
return profiles.filter(profile => {
|
|
23
|
+
const keywordMatch = (profile.match.keywords || []).some(keyword => contains(text, keyword));
|
|
24
|
+
const allMatch = !(profile.match.allKeywords || []).length || profile.match.allKeywords.every(keyword => contains(text, keyword));
|
|
25
|
+
const excluded = (profile.match.excludeKeywords || []).some(keyword => contains(text, keyword));
|
|
26
|
+
const dependencyMatch = (profile.match.dependencies || []).some(dependency => dependencies.has(dependency.toLowerCase()));
|
|
27
|
+
return !excluded && allMatch && (keywordMatch || (profile.activateOnDependency && dependencyMatch));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatGuidance(profiles) {
|
|
32
|
+
if (!profiles.length) return '';
|
|
33
|
+
const lines = [
|
|
34
|
+
'Additional delivery guidance applies to this request. Apply it during delivery_plan, implementation, and review. Add only checks relevant to the requested change, but do not omit affected boundaries or failure paths.'
|
|
35
|
+
];
|
|
36
|
+
for (const profile of profiles) {
|
|
37
|
+
lines.push(`\n[${profile.id}] ${profile.title}`);
|
|
38
|
+
for (const item of profile.planning) lines.push(`Plan: ${item}`);
|
|
39
|
+
for (const item of profile.checks) lines.push(`Check: ${item}`);
|
|
40
|
+
for (const item of profile.review) lines.push(`Review: ${item}`);
|
|
41
|
+
}
|
|
42
|
+
return lines.join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function projectDependencies(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
48
|
+
return new Set(Object.keys({ ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies }).map(name => name.toLowerCase()));
|
|
49
|
+
} catch {
|
|
50
|
+
return new Set();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateProfile(profile, file) {
|
|
55
|
+
const arrays = ['planning', 'checks', 'review'];
|
|
56
|
+
if (!profile || typeof profile !== 'object' || !/^[a-z][a-z0-9-]{1,39}$/.test(profile.id || '') || typeof profile.title !== 'string') throw Error(`Invalid guidance profile: ${file}`);
|
|
57
|
+
if (!profile.match || !Array.isArray(profile.match.keywords) || !profile.match.keywords.length) throw Error(`Guidance profile ${profile.id} needs match.keywords`);
|
|
58
|
+
for (const key of arrays) if (!Array.isArray(profile[key]) || !profile[key].every(item => typeof item === 'string' && item.trim())) throw Error(`Guidance profile ${profile.id} needs ${key} strings`);
|
|
59
|
+
return { priority: 0, activateOnDependency: false, ...profile };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalize(value) {
|
|
63
|
+
return String(value || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function contains(text, keyword) {
|
|
67
|
+
const value = normalize(keyword);
|
|
68
|
+
if (!value) return false;
|
|
69
|
+
if (/^[a-z0-9]+$/.test(value)) return new RegExp(`(?:^|[^a-z0-9])${value}(?:$|[^a-z0-9])`).test(text);
|
|
70
|
+
return text.includes(value);
|
|
71
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ADVISOR_QUESTIONS } from './plan-advisor.mjs';
|
|
2
|
+
|
|
3
|
+
// HTTP adapter for documented Noul answers. Key/network/time/pricing are injected.
|
|
4
|
+
// No env lookup, implicit opt-in, retries, SDK install or approval authority.
|
|
5
|
+
export function createJevAdvisor({ apiKey, fetchImpl = fetch, model = 'jev-1.13.0', timeoutMs = 3000, inputPricePerMillion = null } = {}) {
|
|
6
|
+
if (typeof apiKey !== 'string' || !apiKey.trim()) throw Error('Explicit Jev key required');
|
|
7
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000) throw Error('Advisor timeout must be 1–30000ms');
|
|
8
|
+
if (inputPricePerMillion !== null && (!Number.isFinite(inputPricePerMillion) || inputPricePerMillion < 0)) throw Error('Explicit nonnegative input pricing required');
|
|
9
|
+
return {
|
|
10
|
+
async classify(plan) {
|
|
11
|
+
const body = JSON.stringify({ model, state: { goal: plan.goal, acceptance: plan.acceptance, checks: plan.checks, outputs: plan.outputs, assumptions: plan.assumptions, design: plan.design, workflow: plan.workflow }, questions: ADVISOR_QUESTIONS });
|
|
12
|
+
if (Buffer.byteLength(body) > 64000) throw Error('Advisor request exceeds conservative 64KB local limit');
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
let timer;
|
|
15
|
+
const timeout = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(Error('Advisor timeout')); }, timeoutMs); });
|
|
16
|
+
try {
|
|
17
|
+
return await Promise.race([timeout, (async () => {
|
|
18
|
+
const response = await fetchImpl('https://api.typesafe.ai/v1/systemone', { method: 'POST', headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' }, body, signal: controller.signal, redirect: 'error' });
|
|
19
|
+
if (!response.ok) throw Error('Advisor HTTP failure');
|
|
20
|
+
const reader = response.body?.getReader();
|
|
21
|
+
if (!reader) throw Error('Advisor response body unavailable');
|
|
22
|
+
const chunks = []; let size = 0;
|
|
23
|
+
try {
|
|
24
|
+
for (;;) {
|
|
25
|
+
const { value, done } = await reader.read();
|
|
26
|
+
if (done) break;
|
|
27
|
+
size += value.byteLength;
|
|
28
|
+
if (size > 65536) { await reader.cancel(); throw Error('Advisor response too large'); }
|
|
29
|
+
chunks.push(Buffer.from(value));
|
|
30
|
+
}
|
|
31
|
+
} finally { reader.releaseLock(); }
|
|
32
|
+
const payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
33
|
+
if (typeof payload.model !== 'string' || !payload.model.trim()) throw Error('Missing advisor model identity');
|
|
34
|
+
const probabilities = {};
|
|
35
|
+
for (const key of Object.keys(ADVISOR_QUESTIONS)) {
|
|
36
|
+
const answer = payload.answers?.[key];
|
|
37
|
+
if (answer?.type !== 'noul' || !Number.isFinite(answer.noul) || answer.noul < 0 || answer.noul > 1) throw Error('Invalid Noul answer');
|
|
38
|
+
probabilities[key] = answer.noul;
|
|
39
|
+
}
|
|
40
|
+
const tokens = payload.usage?.input_tokens;
|
|
41
|
+
const usage = Number.isInteger(tokens) && tokens >= 0 ? { inputTokens: tokens, outputTokens: payload.usage.output_tokens ?? null } : null;
|
|
42
|
+
return { model: payload.model, probabilities, usage, costUsd: usage && inputPricePerMillion !== null ? tokens * inputPricePerMillion / 1e6 : null };
|
|
43
|
+
})()]);
|
|
44
|
+
} finally { clearTimeout(timer); controller.abort(); }
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { constants, closeSync, fstatSync, lstatSync, openSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** @param {string} directory @returns {string[]} */
|
|
5
|
+
function walk(directory) {
|
|
6
|
+
let entries;
|
|
7
|
+
try { entries = readdirSync(directory, { withFileTypes: true }); } catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
8
|
+
const files = [];
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const path = join(directory, entry.name);
|
|
11
|
+
let stat;
|
|
12
|
+
try { stat = lstatSync(path); } catch (error) { if (error.code === 'ENOENT' || error.code === 'ELOOP') continue; throw error; }
|
|
13
|
+
// Do not follow either directory or file symlinks while enumerating an archive.
|
|
14
|
+
if (stat.isSymbolicLink()) continue;
|
|
15
|
+
if (stat.isDirectory()) files.push(...walk(path));
|
|
16
|
+
else if (stat.isFile() && entry.name.endsWith('.jsonl')) files.push(path);
|
|
17
|
+
}
|
|
18
|
+
return files;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isWithin(root, path) {
|
|
22
|
+
const pathFromRoot = relative(root, path);
|
|
23
|
+
return pathFromRoot === '' || (pathFromRoot !== '..' && !pathFromRoot.startsWith(`..${sep}`) && !isAbsolute(pathFromRoot));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonicalRoots(roots = []) {
|
|
27
|
+
return roots.flatMap(root => {
|
|
28
|
+
try { return [realpathSync(resolve(root))]; }
|
|
29
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return []; throw error; }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Open the real target only after checking both its canonical parent and final target.
|
|
34
|
+
* O_NOFOLLOW prevents a final-component symlink swap between validation and open. */
|
|
35
|
+
function readContained(path, allowedRoots) {
|
|
36
|
+
const lexical = resolve(path);
|
|
37
|
+
let realParent;
|
|
38
|
+
try { realParent = realpathSync(dirname(lexical)); } catch (error) { if (error.code === 'ENOENT' || error.code === 'ENOTDIR') throw error; throw Error('Legacy transcript parent is not accessible', { cause: error }); }
|
|
39
|
+
let realTarget;
|
|
40
|
+
try { realTarget = realpathSync(lexical); } catch (error) {
|
|
41
|
+
if ((error.code === 'ENOENT' || error.code === 'ENOTDIR') && allowedRoots.some(root => isWithin(root, realParent))) throw error;
|
|
42
|
+
throw Error('Legacy transcript path is outside configured archive roots or is not accessible', { cause: error });
|
|
43
|
+
}
|
|
44
|
+
if (!allowedRoots.some(root => isWithin(root, realParent) && isWithin(root, realTarget))) throw Error('Legacy transcript path is outside configured archive roots');
|
|
45
|
+
const targetStat = statSync(realTarget);
|
|
46
|
+
if (!realTarget.endsWith('.jsonl') || !targetStat.isFile()) throw Error('Legacy transcript must be a JSONL file');
|
|
47
|
+
let fd;
|
|
48
|
+
try {
|
|
49
|
+
fd = openSync(realTarget, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
50
|
+
const openedStat = fstatSync(fd);
|
|
51
|
+
if (openedStat.dev !== targetStat.dev || openedStat.ino !== targetStat.ino) throw Error('Legacy transcript target changed while opening');
|
|
52
|
+
return readFileSync(fd, 'utf8');
|
|
53
|
+
} finally { if (fd !== undefined) closeSync(fd); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @param {string} text @returns {{ rows: any[], malformed: Array<{ line: number }> }} */
|
|
57
|
+
function parse(text) {
|
|
58
|
+
const rows = [];
|
|
59
|
+
const malformed = [];
|
|
60
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
61
|
+
if (!line.trim()) return;
|
|
62
|
+
try { rows.push(JSON.parse(line)); } catch { malformed.push({ line: index + 1 }); }
|
|
63
|
+
});
|
|
64
|
+
return { rows, malformed };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @param {string} path @param {string[]} roots */
|
|
68
|
+
function sessionInfo(path, roots) {
|
|
69
|
+
const canonical = realpathSync(path);
|
|
70
|
+
const { rows, malformed } = parse(readContained(canonical, roots));
|
|
71
|
+
const header = rows.find(row => row?.type === 'session') || rows[0] || {};
|
|
72
|
+
const messages = rows.filter(row => row?.type === 'message');
|
|
73
|
+
const text = messages.map(row => JSON.stringify(row.message ?? row)).join('\n');
|
|
74
|
+
return { id: header.id || basename(path, '.jsonl'), path: canonical, root: roots.find(root => isWithin(root, canonical)) || null, createdAt: header.timestamp || null, messageCount: messages.length, malformedLines: malformed, searchText: text };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Enumerate legacy Pi transcripts only; this module never imports or mutates them. */
|
|
78
|
+
/** @param {{ roots: string[] }} options */
|
|
79
|
+
export function listLegacyArchive({ roots }) {
|
|
80
|
+
if (!Array.isArray(roots)) throw Error('roots must be an array');
|
|
81
|
+
const allowed = canonicalRoots(roots);
|
|
82
|
+
const files = [...new Set(allowed.flatMap(root => walk(root)))].sort();
|
|
83
|
+
return files.flatMap(path => {
|
|
84
|
+
try { const info = sessionInfo(path, allowed); delete info.searchText; return [info]; }
|
|
85
|
+
catch (error) { if (error.code === 'ENOENT' || error.code === 'ELOOP') return []; throw error; }
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @param {{ roots: string[], query: string }} options */
|
|
90
|
+
export function searchLegacyArchive({ roots, query }) {
|
|
91
|
+
if (!Array.isArray(roots)) throw Error('roots must be an array');
|
|
92
|
+
if (typeof query !== 'string' || !query.trim()) throw Error('Search query is required');
|
|
93
|
+
const allowed = canonicalRoots(roots);
|
|
94
|
+
const needle = query.toLocaleLowerCase();
|
|
95
|
+
return [...new Set(allowed.flatMap(root => walk(root)))].sort().flatMap(path => {
|
|
96
|
+
try {
|
|
97
|
+
const session = sessionInfo(path, allowed);
|
|
98
|
+
const index = session.searchText.toLocaleLowerCase().indexOf(needle);
|
|
99
|
+
if (index < 0) return [];
|
|
100
|
+
return [{ ...session, excerpt: session.searchText.slice(Math.max(0, index - 120), index + needle.length + 180) }];
|
|
101
|
+
} catch (error) { if (error.code === 'ENOENT' || error.code === 'ELOOP') return []; throw error; }
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {{ path: string, roots?: string[] }} options */
|
|
106
|
+
export function readLegacyTranscript({ path, roots }) {
|
|
107
|
+
if (!roots) throw Error('Configured archive roots are required to read a legacy transcript');
|
|
108
|
+
const allowed = canonicalRoots(roots);
|
|
109
|
+
const target = resolve(path);
|
|
110
|
+
const content = readContained(target, allowed);
|
|
111
|
+
const canonical = realpathSync(target);
|
|
112
|
+
const { rows, malformed } = parse(content);
|
|
113
|
+
return { path: canonical, records: rows, malformedLines: malformed };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @param {{ path: string, format?: 'json' | 'jsonl', roots?: string[] }} options */
|
|
117
|
+
export function exportLegacyTranscript({ path, format = 'json', roots }) {
|
|
118
|
+
const transcript = readLegacyTranscript({ path, roots });
|
|
119
|
+
if (format === 'json') return `${JSON.stringify(transcript.records, null, 2)}\n`;
|
|
120
|
+
if (format !== 'jsonl') throw Error('Export format must be json or jsonl');
|
|
121
|
+
return readContained(transcript.path, canonicalRoots(roots));
|
|
122
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { OPENCODE_VERSION, resolveOpenCode } from './runtime.mjs';
|
|
2
|
+
|
|
3
|
+
/** Assert that the installed executable implements the native runtime contract. */
|
|
4
|
+
export async function assertCompatibility(options = {}) {
|
|
5
|
+
const binary = await resolveOpenCode(options);
|
|
6
|
+
return Object.freeze({ binary, version: OPENCODE_VERSION, native: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Keep only portable CTG slash-command metadata for the native TUI launcher. */
|
|
10
|
+
export function normalizeSlashCommand(name, definition) {
|
|
11
|
+
if (!/^[a-z][a-z0-9_-]*$/i.test(name)) throw new TypeError(`Invalid slash command name: ${name}`);
|
|
12
|
+
if (typeof definition === 'string') return { description: name, template: definition };
|
|
13
|
+
if (!definition || typeof definition.template !== 'string') throw new TypeError(`Slash command ${name} requires a template`);
|
|
14
|
+
return { description: definition.description ?? name, template: definition.template, ...(definition.agent ? { agent: definition.agent } : {}) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function nativeCommandConfig(commands = {}) {
|
|
18
|
+
return Object.fromEntries(Object.entries(commands).map(([name, definition]) => [name, normalizeSlashCommand(name, definition)]));
|
|
19
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/** CTG-owned credential helpers. OpenCode's integration store remains the
|
|
2
|
+
* authoritative store; this module only coordinates refresh and supports a
|
|
3
|
+
* one-way migration read from Carthagent's legacy auth.json. */
|
|
4
|
+
import { open, readFile, rename, rm } from 'node:fs/promises';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { shipAgentDir } from '../ship-auth.mjs';
|
|
9
|
+
import { CARTHAGENT_SHIP_PROVIDER_ID } from '../providers/names.mjs';
|
|
10
|
+
import { withFileLock } from './file-lock.mjs';
|
|
11
|
+
|
|
12
|
+
export const CTG_CREDENTIAL_NAMESPACE = 'ctg.credentials/';
|
|
13
|
+
const refreshFlights = new Map();
|
|
14
|
+
|
|
15
|
+
const identity = value => String(value || '').trim();
|
|
16
|
+
const processAlive = pid => {
|
|
17
|
+
try { process.kill(pid, 0); return true; }
|
|
18
|
+
catch (error) { return error.code === 'EPERM'; }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Single-flight by credential ID and refresh-token fingerprint. The callback
|
|
22
|
+
* persists the returned rotated token through the authoritative OpenCode
|
|
23
|
+
* integration refresh path; callers sharing the old credential await it. */
|
|
24
|
+
export function refreshSingleFlight(key, refresh) {
|
|
25
|
+
const flightKey = identity(key);
|
|
26
|
+
if (!flightKey) return Promise.reject(new TypeError('A credential refresh key is required.'));
|
|
27
|
+
if (typeof refresh !== 'function') return Promise.reject(new TypeError('A refresh callback is required.'));
|
|
28
|
+
const active = refreshFlights.get(flightKey);
|
|
29
|
+
if (active) return active;
|
|
30
|
+
const flight = Promise.resolve().then(refresh);
|
|
31
|
+
refreshFlights.set(flightKey, flight);
|
|
32
|
+
return flight.finally(() => {
|
|
33
|
+
if (refreshFlights.get(flightKey) === flight) refreshFlights.delete(flightKey);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Select OpenCode's native profile credential file; never store a credential here. */
|
|
38
|
+
export function nativeCredentialFile(env = process.env, platform = process.platform, home = os.homedir()) {
|
|
39
|
+
if (env.CTG_OPENCODE_CREDENTIAL_FILE) return resolve(env.CTG_OPENCODE_CREDENTIAL_FILE);
|
|
40
|
+
const dataHome = env.XDG_DATA_HOME || (platform === 'darwin'
|
|
41
|
+
? join(home, 'Library', 'Application Support')
|
|
42
|
+
: platform === 'win32'
|
|
43
|
+
? env.APPDATA || join(home, 'AppData', 'Roaming')
|
|
44
|
+
: join(home, '.local', 'share'));
|
|
45
|
+
return join(dataHome, 'opencode', 'auth.json');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Serialize refresh across host processes, rereading OpenCode's vault under the lock.
|
|
49
|
+
* @param {{credential: any, credentialFile: string, reread: () => Promise<any>, refresh: (credential: any) => Promise<any>, timeout?: number}} options
|
|
50
|
+
*/
|
|
51
|
+
export async function refreshNativeCredential({
|
|
52
|
+
credential,
|
|
53
|
+
credentialFile,
|
|
54
|
+
reread,
|
|
55
|
+
refresh,
|
|
56
|
+
timeout = 15_000,
|
|
57
|
+
processAliveImpl = processAlive,
|
|
58
|
+
} = {}) {
|
|
59
|
+
if (!credential?.refresh || typeof reread !== 'function' || typeof refresh !== 'function') {
|
|
60
|
+
throw new TypeError('A refresh credential, vault reread, and refresh callback are required.');
|
|
61
|
+
}
|
|
62
|
+
const profile = resolve(credentialFile);
|
|
63
|
+
// Token material is never written to the lease path or logs.
|
|
64
|
+
const digest = createHash('sha256').update(`${credential.methodID || ''}\0${credential.refresh}`).digest('hex').slice(0, 32);
|
|
65
|
+
const lock = `${profile}.ctg-refresh-${digest}.lock`;
|
|
66
|
+
const pending = `${lock}.pending`;
|
|
67
|
+
const credentialDigest = value => createHash('sha256')
|
|
68
|
+
.update(`${value?.methodID || credential.methodID || ''}\0${value?.refresh || ''}`).digest('hex').slice(0, 32);
|
|
69
|
+
return withFileLock(lock, async () => {
|
|
70
|
+
const acceptPersistedRotation = async () => {
|
|
71
|
+
const latest = await reread();
|
|
72
|
+
if (latest?.type !== 'oauth' || !latest.refresh || credentialDigest(latest) === digest) return undefined;
|
|
73
|
+
const persistedReceipt = await readFile(pending, 'utf8').catch(() => '');
|
|
74
|
+
if (persistedReceipt === receipt?.raw) await rm(pending, { force: true });
|
|
75
|
+
return latest;
|
|
76
|
+
};
|
|
77
|
+
const current = await reread();
|
|
78
|
+
// OpenCode persists refresh callback results only after this callback
|
|
79
|
+
// resolves. Leave a token-free handoff receipt so another host that wins
|
|
80
|
+
// the disk lock during that gap waits for the native vault write instead
|
|
81
|
+
// of presenting the single-use refresh token a second time.
|
|
82
|
+
let receipt;
|
|
83
|
+
try {
|
|
84
|
+
receipt = JSON.parse(await readFile(pending, 'utf8'));
|
|
85
|
+
receipt.raw = JSON.stringify({ pid: receipt.pid, owner: receipt.owner, from: receipt.from, to: receipt.to, stage: receipt.stage, createdAt: receipt.createdAt });
|
|
86
|
+
}
|
|
87
|
+
catch (error) { if (error.code !== 'ENOENT') throw new Error('Invalid pending native credential refresh receipt; inspect the CTG refresh receipt before retrying.'); }
|
|
88
|
+
if (current?.type === 'oauth' && current.refresh && current.refresh !== credential.refresh) {
|
|
89
|
+
if (receipt?.from === digest && receipt.to === credentialDigest(current)) {
|
|
90
|
+
const persistedReceipt = await readFile(pending, 'utf8').catch(() => '');
|
|
91
|
+
if (persistedReceipt === receipt.raw) await rm(pending, { force: true });
|
|
92
|
+
}
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
if (receipt) {
|
|
96
|
+
if (receipt.from !== digest) throw new Error('A different native credential refresh is pending; retry after the active host saves its credential.');
|
|
97
|
+
if (receipt.stage === 'failed') throw new Error('A previous Ship refresh ended with an uncertain result. No second rotation was attempted; reauthenticate to recover safely.');
|
|
98
|
+
if (receipt.stage !== 'complete' && !processAliveImpl(receipt.pid)) {
|
|
99
|
+
throw new Error('A previous native credential refresh ended before its vault write could be confirmed. Inspect the CTG refresh receipt and reauthenticate if necessary; no second rotation was attempted.');
|
|
100
|
+
}
|
|
101
|
+
const until = Date.now() + timeout;
|
|
102
|
+
while (Date.now() < until) {
|
|
103
|
+
const latest = await acceptPersistedRotation();
|
|
104
|
+
if (latest) return latest;
|
|
105
|
+
if (!processAliveImpl(receipt.pid)) {
|
|
106
|
+
// The callback owner can persist the native vault and exit after the
|
|
107
|
+
// preceding read but before this liveness check. Make one final
|
|
108
|
+
// authoritative read before treating the outcome as uncertain.
|
|
109
|
+
const persisted = await acceptPersistedRotation();
|
|
110
|
+
if (persisted) return persisted;
|
|
111
|
+
throw new Error('The native refresh owner exited before OpenCode saved its credential. No second rotation was attempted; inspect the CTG refresh receipt.');
|
|
112
|
+
}
|
|
113
|
+
await new Promise(resolve => setTimeout(resolve, 40));
|
|
114
|
+
}
|
|
115
|
+
throw new Error('Timed out waiting for OpenCode to persist its refreshed credential; no second refresh was attempted.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const owner = randomUUID();
|
|
119
|
+
const initialReceipt = JSON.stringify({ pid: process.pid, owner, from: digest, stage: 'refreshing', createdAt: Date.now() });
|
|
120
|
+
const pendingHandle = await open(pending, 'wx', 0o600);
|
|
121
|
+
try { await pendingHandle.writeFile(initialReceipt); await pendingHandle.sync(); }
|
|
122
|
+
finally { await pendingHandle.close(); }
|
|
123
|
+
const saveReceipt = async value => {
|
|
124
|
+
const temporary = `${pending}.${owner}.${value.stage}.tmp`;
|
|
125
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
126
|
+
try { await handle.writeFile(JSON.stringify(value)); await handle.sync(); }
|
|
127
|
+
finally { await handle.close(); }
|
|
128
|
+
if (await readFile(pending, 'utf8').catch(() => '') !== initialReceipt) {
|
|
129
|
+
await rm(temporary, { force: true });
|
|
130
|
+
throw new Error('Native credential refresh ownership changed unexpectedly; inspect the CTG refresh receipt.');
|
|
131
|
+
}
|
|
132
|
+
await rename(temporary, pending);
|
|
133
|
+
};
|
|
134
|
+
let rotated;
|
|
135
|
+
try {
|
|
136
|
+
rotated = await refresh(current?.type === 'oauth' ? current : credential);
|
|
137
|
+
if (rotated?.type !== 'oauth' || !rotated.refresh || !rotated.access) throw new Error('Ship token refresh returned an invalid OAuth credential.');
|
|
138
|
+
} catch (error) {
|
|
139
|
+
await saveReceipt({ pid: process.pid, owner, from: digest, stage: 'failed', createdAt: Date.now() });
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
await saveReceipt({
|
|
143
|
+
pid: process.pid,
|
|
144
|
+
owner,
|
|
145
|
+
from: digest,
|
|
146
|
+
to: credentialDigest(rotated),
|
|
147
|
+
stage: 'complete',
|
|
148
|
+
createdAt: Date.now(),
|
|
149
|
+
});
|
|
150
|
+
return rotated;
|
|
151
|
+
}, { timeout });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Namespaced plugin-store keys avoid colliding with host/plugin settings. */
|
|
155
|
+
export function credentialStorageKey(integrationID) {
|
|
156
|
+
const id = identity(integrationID);
|
|
157
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,127}$/i.test(id)) throw new TypeError('Invalid CTG integration id.');
|
|
158
|
+
return `${CTG_CREDENTIAL_NAMESPACE}${id}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Read a legacy Pi-compatible auth entry for explicit migration only. Never
|
|
163
|
+
* writes the old file; successful login/refresh must be saved by OpenCode.
|
|
164
|
+
*/
|
|
165
|
+
export async function readLegacyCarthagentCredential({
|
|
166
|
+
agentDir = shipAgentDir(),
|
|
167
|
+
providerID = CARTHAGENT_SHIP_PROVIDER_ID,
|
|
168
|
+
readFileImpl = readFile,
|
|
169
|
+
} = {}) {
|
|
170
|
+
try {
|
|
171
|
+
const parsed = JSON.parse(await readFileImpl(join(agentDir, 'auth.json'), 'utf8'));
|
|
172
|
+
const credential = parsed?.[providerID];
|
|
173
|
+
if (credential?.type === 'oauth'
|
|
174
|
+
&& typeof credential.access === 'string' && credential.access
|
|
175
|
+
&& typeof credential.refresh === 'string' && credential.refresh
|
|
176
|
+
&& Number.isFinite(Number(credential.expires))) {
|
|
177
|
+
return { type: 'oauth', access: credential.access, refresh: credential.refresh, expires: Number(credential.expires) };
|
|
178
|
+
}
|
|
179
|
+
// BYOK used the same provider namespace in Pi and remains a valid direct
|
|
180
|
+
// OpenCode API key; do not reinterpret it as managed Ship OAuth.
|
|
181
|
+
if (credential?.type === 'api_key' && typeof credential.key === 'string' && credential.key.trim()) {
|
|
182
|
+
return { type: 'api_key', key: credential.key.trim() };
|
|
183
|
+
}
|
|
184
|
+
} catch { /* no legacy store or unreadable/malformed data */ }
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Mark only an explicit migration value as eligible for OpenCode OAuth
|
|
189
|
+
* persistence. The actual write is returned from IntegrationOAuthAuthorization
|
|
190
|
+
* and is therefore performed by OpenCode's credential store. */
|
|
191
|
+
export async function legacyOAuthForOpenCode(options = {}) {
|
|
192
|
+
const credential = await readLegacyCarthagentCredential(options);
|
|
193
|
+
if (credential?.type !== 'oauth') return undefined;
|
|
194
|
+
return {
|
|
195
|
+
type: 'oauth',
|
|
196
|
+
methodID: 'carthagent-device',
|
|
197
|
+
access: credential.access,
|
|
198
|
+
refresh: credential.refresh,
|
|
199
|
+
expires: Math.trunc(credential.expires),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function legacyCredentialMigrationGuidance() {
|
|
204
|
+
return 'Existing Carthagent Ship OAuth can be migrated from the legacy auth.json credential into OpenCode integration storage; other provider credentials remain BYOK and may be reconnected through OpenCode if their format is unsupported.';
|
|
205
|
+
}
|