@moondeck/station 0.0.1 → 1.0.1
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 +149 -2
- package/bin/uai-host.mjs +130 -0
- package/db/migrations/0000_host_tasks.sql +12 -0
- package/db/migrations/0001_host_ui.sql +11 -0
- package/db/migrations/0002_host_github_tokens.sql +8 -0
- package/db/migrations/0003_host_ssh_keys.sql +8 -0
- package/db/migrations/0004_host_owner_name.sql +1 -0
- package/db/migrations/0005_host_env_vars.sql +5 -0
- package/db/migrations/0006_host_project_env.sql +9 -0
- package/db/migrations/0007_host_github_token_kind.sql +7 -0
- package/db/migrations/0008_host_mcp_connections.sql +18 -0
- package/db/migrations/0009_host_agent_sessions.sql +15 -0
- package/db/migrations/0010_host_engine_accounts.sql +13 -0
- package/db/migrations/0011_codex_thread_id.sql +5 -0
- package/db/migrations/0012_agent_session_attach_key.sql +4 -0
- package/db/migrations/0013_github_credential_generations.sql +11 -0
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/0017_engine_account_scope.sql +7 -0
- package/db/migrations/0018_engine_account_position.sql +3 -0
- package/db/migrations/0019_engine_account_store_ref.sql +3 -0
- package/db/migrations/0020_user_credential_store_ref.sql +6 -0
- package/db/migrations/0021_project_env_store_ref.sql +3 -0
- package/db/migrations/0022_task_project_ids.sql +5 -0
- package/db/migrations/0023_project_env_org.sql +4 -0
- package/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/0029_task_shared_files_mode.sql +5 -0
- package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
- package/db/migrations/0031_first_turn_receipts.sql +9 -0
- package/db/migrations/meta/_journal.json +230 -0
- package/db/schema.ts +871 -0
- package/images/standard/Dockerfile +347 -0
- package/images/standard/README.md +167 -0
- package/images/standard/container/code-server-settings.json +63 -0
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +704 -0
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/images/standard/tool-versions +2 -0
- package/lib/__fixtures__/skill-index/SKILL.md +10 -0
- package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
- package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
- package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
- package/lib/agent-cli.ts +1507 -0
- package/lib/agent-errors.ts +22 -0
- package/lib/agent.ts +672 -0
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/claude.ts +918 -0
- package/lib/agents/codex-token-usage.fixture.json +130 -0
- package/lib/agents/codex.ts +1392 -0
- package/lib/agents/cursor-period-usage.ts +253 -0
- package/lib/agents/cursor.ts +562 -0
- package/lib/agents/dispatch.ts +78 -0
- package/lib/agents/durable-proc.ts +334 -0
- package/lib/agents/factory.ts +57 -0
- package/lib/agents/grok.ts +381 -0
- package/lib/agents/kimi.ts +329 -0
- package/lib/agents/machine-durable.ts +604 -0
- package/lib/agents/mock.ts +133 -0
- package/lib/agents/mode.ts +64 -0
- package/lib/agents/opencode.ts +508 -0
- package/lib/agents/output-source.ts +3 -0
- package/lib/agents/proc.ts +221 -0
- package/lib/agents/rate-limit.ts +337 -0
- package/lib/agents/registry.ts +182 -0
- package/lib/agents/session-state.ts +197 -0
- package/lib/agents/skill-index-preamble.fixture.ts +26 -0
- package/lib/agents/transport.ts +741 -0
- package/lib/agents/types.ts +254 -0
- package/lib/agents/uai.ts +106 -0
- package/lib/agents/usage.ts +44 -0
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +237 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/assignment-evidence.ts +94 -0
- package/lib/attachments.ts +48 -0
- package/lib/browser-testing.ts +1537 -0
- package/lib/capture-flow.ts +332 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2587 -0
- package/lib/cloud-credential-runtime.ts +1357 -0
- package/lib/cloud-state.ts +56 -0
- package/lib/codex-auth.ts +456 -0
- package/lib/command-db.ts +310 -0
- package/lib/compose-network-recovery.ts +283 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/container-runtime.ts +1470 -0
- package/lib/credential-cleanup-inventory.ts +490 -0
- package/lib/credential-consumers/engine-anchor.ts +1880 -0
- package/lib/credential-consumers/engines.ts +797 -0
- package/lib/credential-consumers/files.ts +934 -0
- package/lib/credential-consumers/github.ts +702 -0
- package/lib/credential-consumers/mcp.ts +389 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-consumers/task-secrets.ts +10 -0
- package/lib/credential-store-pickup.ts +149 -0
- package/lib/credential-store-scope.ts +112 -0
- package/lib/credential-store.ts +932 -0
- package/lib/credentials-activation-authority.ts +709 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +384 -0
- package/lib/custom-connection-runtime.ts +141 -0
- package/lib/db.ts +80 -0
- package/lib/disk-pressure.ts +42 -0
- package/lib/docker-exec.ts +156 -0
- package/lib/engine-accounts.ts +1970 -0
- package/lib/engine-connect.ts +321 -0
- package/lib/engine-credential-env.ts +52 -0
- package/lib/engine-login.ts +2933 -0
- package/lib/engines.ts +1342 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +246 -0
- package/lib/env-schema.ts +937 -0
- package/lib/env.ts +195 -0
- package/lib/first-turn-receipts.ts +62 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +530 -0
- package/lib/git-identity.ts +715 -0
- package/lib/github-git-auth.ts +207 -0
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +2439 -0
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-env.ts +424 -0
- package/lib/host-logs.ts +965 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/launch-mode.ts +25 -0
- package/lib/lease.ts +239 -0
- package/lib/legacy-host.ts +769 -0
- package/lib/legacy-runtime-retirement.ts +424 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/machine-exec.ts +238 -0
- package/lib/machine-gateway-tunnel.ts +344 -0
- package/lib/machine-keys.ts +65 -0
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/machine-provider-aws.ts +923 -0
- package/lib/machine-provider-local.ts +338 -0
- package/lib/machine-provider.ts +90 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +57 -0
- package/lib/managed-runtime.ts +4296 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-capture.ts +865 -0
- package/lib/mcp-config-lock.ts +2 -0
- package/lib/mcp-connections.ts +959 -0
- package/lib/mcp-gateway.ts +2594 -0
- package/lib/obs.ts +512 -0
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +11409 -0
- package/lib/person-capture.ts +327 -0
- package/lib/preview-ports.ts +85 -0
- package/lib/preview-sidecar.ts +331 -0
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-loader.ts +242 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/release-feed.ts +61 -0
- package/lib/release-manifest.ts +710 -0
- package/lib/release-trust.ts +46 -0
- package/lib/repo-clone.ts +38 -0
- package/lib/routing/shadow.ts +261 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +279 -0
- package/lib/runtime-archive.ts +1216 -0
- package/lib/runtime-authority.ts +166 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +1033 -0
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/address.ts +95 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +57 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +150 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +605 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/secrets.ts +71 -0
- package/lib/shared-files-s3.ts +976 -0
- package/lib/shared-files.ts +156 -0
- package/lib/sidecar-assign.ts +49 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +178 -0
- package/lib/sidecar.ts +69 -0
- package/lib/signin-box-credentials.ts +465 -0
- package/lib/signin-box.ts +114 -0
- package/lib/skill-index-example.fixture.ts +56 -0
- package/lib/skill-layout.ts +417 -0
- package/lib/skills.ts +489 -0
- package/lib/ssh.ts +401 -0
- package/lib/standard-image-cleanup.ts +117 -0
- package/lib/standard-image-pin.ts +207 -0
- package/lib/standard-image.ts +1697 -0
- package/lib/stopped-task-status-queue.ts +120 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-credential-cleanup.ts +752 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +91 -0
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +249 -0
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +860 -0
- package/lib/task-environment/docker.ts +1344 -0
- package/lib/task-environment/index.ts +466 -0
- package/lib/task-environment/legacy-adoption.ts +467 -0
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +1227 -0
- package/lib/task-environment/machine.ts +974 -0
- package/lib/task-environment/registry.ts +59 -0
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +566 -0
- package/lib/task-environment/types.ts +642 -0
- package/lib/task-environment/workspace-files.ts +685 -0
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-files-machine-retirement-outbox.ts +592 -0
- package/lib/task-files-machine-retirement.ts +116 -0
- package/lib/task-files-readiness.ts +127 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +47 -0
- package/lib/task-gateway-state.ts +512 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +682 -0
- package/lib/task-machine-credentials-activation.ts +95 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/task-person-credentials.ts +76 -0
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1062 -0
- package/lib/task-secret-staging.ts +16 -0
- package/lib/task-status.ts +46 -0
- package/lib/task-token.ts +46 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +105 -0
- package/lib/token-broker-local.ts +392 -0
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +525 -0
- package/lib/token-broker.ts +218 -0
- package/lib/token-lanes/openai-oauth.ts +123 -0
- package/lib/token-lanes/registry.ts +45 -0
- package/lib/token-lanes/types.ts +163 -0
- package/lib/token-lanes/xai-oauth.ts +165 -0
- package/lib/transcript.ts +128 -0
- package/lib/tunnel-registry.ts +220 -0
- package/lib/tunnel-runtime.ts +235 -0
- package/lib/ulid.ts +16 -0
- package/lib/user-bus-env.ts +116 -0
- package/lib/user-credentials-store.ts +435 -0
- package/lib/vnode-pressure.ts +142 -0
- package/package.json +115 -8
- package/runner/runner.mjs +442 -0
- package/scripts/agent/_common.sh +633 -0
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/moondeck-cli.sh +5 -0
- package/scripts/agent/task-down.sh +291 -0
- package/scripts/agent/task-status.sh +70 -0
- package/scripts/agent/task-up.sh +3761 -0
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/scripts/install/darwin.ts +1048 -0
- package/scripts/install/linux.ts +958 -0
- package/scripts/install/types.ts +78 -0
- package/scripts/install/util.ts +254 -0
- package/scripts/install/win.ts +143 -0
- package/src/activation-respawn.ts +155 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/bridge-diagnostics.ts +39 -0
- package/src/capture-wire.ts +102 -0
- package/src/cli.ts +2585 -0
- package/src/cloud-credentials.ts +2144 -0
- package/src/cloud-origins.ts +23 -0
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +237 -0
- package/src/engine-account-paths.ts +11 -0
- package/src/engine-models.ts +39 -0
- package/src/engine-usage.ts +104 -0
- package/src/enrollment-next-steps.ts +24 -0
- package/src/event-outbox.ts +185 -0
- package/src/index.ts +2907 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6363 -0
- package/src/never-proxied-domains.ts +31 -0
- package/src/outbound-frame.ts +31 -0
- package/src/paths.ts +81 -0
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +396 -0
- package/src/project-env-wording.ts +53 -0
- package/src/protocol.ts +5550 -0
- package/src/runtime-bootstrap.ts +165 -0
- package/src/station-identity.ts +96 -0
- package/src/task-env-names.ts +28 -0
- package/src/task-secrets.ts +75 -0
- package/src/tunnel-dial.ts +102 -0
- package/src/ui/server.ts +852 -0
- package/src/ui/types.ts +173 -0
- package/src/workspace-path-policy.ts +41 -0
- package/ui/app.js +1102 -0
- package/ui/index.html +90 -0
- package/ui/style.css +685 -0
- package/ui/uai-favicon.svg +21 -0
- package/ui/uai-logo-black.svg +9 -0
- package/ui/uai-wheel.svg +9 -0
- package/bin/moondeck-station.mjs +0 -3
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CursorSession — a real AgentSession backed by **Cursor Agent** (`cursor-agent`),
|
|
3
|
+
* run inside the task environment:
|
|
4
|
+
*
|
|
5
|
+
* CURSOR_API_KEY=… /home/node/.local/bin/cursor-agent -p "<prompt>" \
|
|
6
|
+
* --output-format stream-json --stream-partial-output --force --trust \
|
|
7
|
+
* --model <model> [--resume <sessionId>]
|
|
8
|
+
*
|
|
9
|
+
* One-shot per turn (like Kimi/Grok); sends serialized; continuity via
|
|
10
|
+
* `--resume <sessionId>` captured from the stream. Cursor's stream-json is
|
|
11
|
+
* essentially Claude Code's — the RICHEST of the extra engines (streaming text
|
|
12
|
+
* AND tool cards):
|
|
13
|
+
*
|
|
14
|
+
* {"type":"system","subtype":"init","session_id":…} → session id
|
|
15
|
+
* {"type":"thinking","subtype":"delta"|"completed",…} → skipped
|
|
16
|
+
* {"type":"assistant","message":{content:[{text}]},"timestamp_ms":…}
|
|
17
|
+
* → message_delta
|
|
18
|
+
* {"type":"tool_call","subtype":"started","call_id","tool_call":{…}}
|
|
19
|
+
* → tool_call card
|
|
20
|
+
* {"type":"result","subtype":"success"|"error","result",session_id}
|
|
21
|
+
* → message_complete + end
|
|
22
|
+
*
|
|
23
|
+
* AUTH is a Cursor API key (`CURSOR_API_KEY`) — env-injected at exec, like the
|
|
24
|
+
* Claude token (no config-dir copy). Cloud never sees it (ADR-015). `-p` runs
|
|
25
|
+
* headless with `--force` (auto-run tools; the container is the sandbox) and
|
|
26
|
+
* `--trust` (skip the workspace-trust prompt). Cursor has no system-prompt
|
|
27
|
+
* flag, so the briefing folds into the first turn's prompt.
|
|
28
|
+
*/
|
|
29
|
+
import { stripVTControlCharacters } from "node:util";
|
|
30
|
+
|
|
31
|
+
import { ENGINE_ACCOUNT_ENV } from "../credential-consumers/engine-anchor";
|
|
32
|
+
import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
33
|
+
import type {
|
|
34
|
+
TaskEnvironmentAgentSessionSurface,
|
|
35
|
+
TaskEnvironmentSessionRequest,
|
|
36
|
+
} from "../task-environment/types";
|
|
37
|
+
import { newId } from "../ulid";
|
|
38
|
+
import { register } from "./registry";
|
|
39
|
+
import {
|
|
40
|
+
AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
41
|
+
spawnEnvironmentProcess,
|
|
42
|
+
type AgentEnvironmentProcess,
|
|
43
|
+
} from "./proc";
|
|
44
|
+
import {
|
|
45
|
+
CURSOR_PERIOD_PROBE_TIMEOUT_MS,
|
|
46
|
+
CURSOR_PERIOD_USAGE_LOG,
|
|
47
|
+
cursorAccessTokenFromAuthJson,
|
|
48
|
+
cursorPeriodProbeDue,
|
|
49
|
+
markCursorPeriodProbed,
|
|
50
|
+
readCursorAuthFromEnvironment,
|
|
51
|
+
readCursorPeriodUsage,
|
|
52
|
+
} from "./cursor-period-usage";
|
|
53
|
+
import { rateLimitErrorUsage } from "./rate-limit";
|
|
54
|
+
import { extractResultUsage } from "./usage";
|
|
55
|
+
import type {
|
|
56
|
+
AgentAccountUsage,
|
|
57
|
+
AgentEvent,
|
|
58
|
+
AgentEventHandler,
|
|
59
|
+
AgentKind,
|
|
60
|
+
AgentSession,
|
|
61
|
+
AgentUsage,
|
|
62
|
+
RosterAgent,
|
|
63
|
+
} from "./types";
|
|
64
|
+
|
|
65
|
+
const CURSOR_BIN = "/home/node/.local/bin/cursor-agent";
|
|
66
|
+
|
|
67
|
+
// Cursor replays its captured sandbox environment immediately before it
|
|
68
|
+
// evaluates each shell command. In loader tasks this restore hook is the last
|
|
69
|
+
// step in that replay, so the command sees the current project environment
|
|
70
|
+
// rather than Cursor's stale launch snapshot.
|
|
71
|
+
export const CURSOR_SANDBOX_ENV_RESTORE =
|
|
72
|
+
". /usr/local/share/uai/env-hook.sh; __uai_env_sync";
|
|
73
|
+
|
|
74
|
+
// A curated fallback slice of Cursor's signed-in 2026.10.22 catalog. The
|
|
75
|
+
// newest version of each tier comes first; older known-good ids follow.
|
|
76
|
+
export const CURSOR_MODELS = [
|
|
77
|
+
"grok-4.7-high",
|
|
78
|
+
"grok-4.7-high-fast",
|
|
79
|
+
"composer-2.5",
|
|
80
|
+
"composer-2.5-fast",
|
|
81
|
+
"claude-opus-5-5-high",
|
|
82
|
+
"claude-fable-5-1-thinking-high",
|
|
83
|
+
"claude-sonnet-5-5-high",
|
|
84
|
+
"gpt-5.6-sol-high",
|
|
85
|
+
"gemini-4-argon-high",
|
|
86
|
+
"kimi-k3-high",
|
|
87
|
+
"auto",
|
|
88
|
+
"claude-opus-4-8-thinking-high",
|
|
89
|
+
"cursor-grok-4.5-high",
|
|
90
|
+
"claude-fable-5-thinking-high",
|
|
91
|
+
];
|
|
92
|
+
const CURSOR_DEFAULT_MODEL = "grok-4.7-high";
|
|
93
|
+
const CURSOR_EFFORTS: string[] = []; // effort is baked into the model id
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Pure protocol mapping.
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
function assistantText(msg: Record<string, unknown>): string {
|
|
100
|
+
const message = msg.message as { content?: unknown } | undefined;
|
|
101
|
+
const content = Array.isArray(message?.content) ? message!.content : [];
|
|
102
|
+
let out = "";
|
|
103
|
+
for (const raw of content) {
|
|
104
|
+
const block = raw as { type?: unknown; text?: unknown };
|
|
105
|
+
if (block.type === "text" && typeof block.text === "string") out += block.text;
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** "shellToolCall" → "shell", "readFileToolCall" → "readFile". */
|
|
111
|
+
function prettyTool(name: string): string {
|
|
112
|
+
return name.replace(/ToolCall$/, "") || "tool";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface MappedCursorLine {
|
|
116
|
+
textDelta?: string;
|
|
117
|
+
toolCall?: { id: string; title: string; detail: string };
|
|
118
|
+
end?: boolean;
|
|
119
|
+
finalText?: string;
|
|
120
|
+
sessionId?: string;
|
|
121
|
+
errorText?: string;
|
|
122
|
+
/** This model needs paid Cursor credits; the account itself is still usable. */
|
|
123
|
+
creditRefusal?: boolean;
|
|
124
|
+
/** Set when `errorText` is a usage cap. Secret-free; source `error-text`. */
|
|
125
|
+
accountUsage?: AgentAccountUsage;
|
|
126
|
+
/** Tokens from the result envelope. Claude-shaped lines carry cost;
|
|
127
|
+
* cursor-agent 2026.10.x carries camelCase counts and no cost. */
|
|
128
|
+
usage?: AgentUsage;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Cursor Premium can refuse one paid-credit model while the same account can
|
|
132
|
+
* still run Cursor's Grok and Composer models. This is not account exhaustion:
|
|
133
|
+
* keep it non-retryable so the orchestrator neither rotates nor cools the
|
|
134
|
+
* account, and give the person an actionable model-specific error. */
|
|
135
|
+
export function cursorCreditsRequiredMessage(message: string): string | null {
|
|
136
|
+
const match = stripVTControlCharacters(message).match(
|
|
137
|
+
/(?:ActionRequiredError:\s*)?([^\r\n]+?)\s+is not included in your weekly usage\b/i,
|
|
138
|
+
);
|
|
139
|
+
const model = match?.[1]?.trim();
|
|
140
|
+
return model
|
|
141
|
+
? `${model} needs Cursor credits on this plan; use a Grok or Composer model`
|
|
142
|
+
: null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function mapCursorLine(line: string, now: number = Date.now()): MappedCursorLine {
|
|
146
|
+
const t = line.trim();
|
|
147
|
+
if (!t.startsWith("{")) return {};
|
|
148
|
+
let m: Record<string, unknown>;
|
|
149
|
+
try {
|
|
150
|
+
m = JSON.parse(t) as Record<string, unknown>;
|
|
151
|
+
} catch {
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (m.type === "system") {
|
|
156
|
+
return typeof m.session_id === "string" ? { sessionId: m.session_id } : {};
|
|
157
|
+
}
|
|
158
|
+
if (m.type === "assistant") {
|
|
159
|
+
// Streaming partials carry timestamp_ms; the final full message (finalized
|
|
160
|
+
// by `result`) does not — only stream the partials to avoid doubling.
|
|
161
|
+
if (typeof m.timestamp_ms === "number") {
|
|
162
|
+
const text = assistantText(m);
|
|
163
|
+
if (text) return { textDelta: text };
|
|
164
|
+
}
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
if (m.type === "tool_call" && m.subtype === "started") {
|
|
168
|
+
const tc = (m.tool_call ?? {}) as Record<string, unknown>;
|
|
169
|
+
const [name, spec] = Object.entries(tc)[0] ?? ["tool", {}];
|
|
170
|
+
const args = ((spec as Record<string, unknown>)?.args ?? {}) as Record<string, unknown>;
|
|
171
|
+
const pick = args.command ?? args.path ?? args.filePath ?? args.file_path ?? args.query;
|
|
172
|
+
return {
|
|
173
|
+
toolCall: {
|
|
174
|
+
id: typeof m.call_id === "string" ? m.call_id : newId(),
|
|
175
|
+
title: prettyTool(name),
|
|
176
|
+
detail: typeof pick === "string" ? pick : "",
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (m.type === "result") {
|
|
181
|
+
const errored = m.is_error === true;
|
|
182
|
+
const rawErrorText = errored
|
|
183
|
+
? typeof m.result === "string"
|
|
184
|
+
? m.result
|
|
185
|
+
: "cursor turn failed"
|
|
186
|
+
: undefined;
|
|
187
|
+
const creditError = rawErrorText
|
|
188
|
+
? cursorCreditsRequiredMessage(rawErrorText)
|
|
189
|
+
: null;
|
|
190
|
+
const errorText = creditError ?? rawErrorText;
|
|
191
|
+
const accountUsage = errorText ? rateLimitErrorUsage(errorText, now) : null;
|
|
192
|
+
return {
|
|
193
|
+
end: true,
|
|
194
|
+
sessionId: typeof m.session_id === "string" ? m.session_id : undefined,
|
|
195
|
+
finalText: typeof m.result === "string" ? m.result : undefined,
|
|
196
|
+
errorText,
|
|
197
|
+
...(creditError ? { creditRefusal: true } : {}),
|
|
198
|
+
...(accountUsage ? { accountUsage } : {}),
|
|
199
|
+
usage: extractResultUsage(m),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// thinking / user / other → nothing.
|
|
203
|
+
return {};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Session
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
export type Spawner = (
|
|
211
|
+
request: TaskEnvironmentSessionRequest,
|
|
212
|
+
) => AgentEnvironmentProcess;
|
|
213
|
+
|
|
214
|
+
export class CursorSession implements AgentSession {
|
|
215
|
+
readonly agentId: string;
|
|
216
|
+
readonly kind: AgentKind = "cursor";
|
|
217
|
+
|
|
218
|
+
private readonly environment: TaskEnvironmentAgentSessionSurface;
|
|
219
|
+
private readonly model?: string;
|
|
220
|
+
private readonly systemPreamble: string;
|
|
221
|
+
private readonly agentEnv: Record<string, string>;
|
|
222
|
+
private readonly spawner: Spawner;
|
|
223
|
+
private readonly apiKey: string;
|
|
224
|
+
private readonly readAuthFile: (signal: AbortSignal) => Promise<string | null>;
|
|
225
|
+
private readonly fetchImpl: typeof fetch;
|
|
226
|
+
private readonly probeTimeoutMs: number;
|
|
227
|
+
|
|
228
|
+
private readonly handlers = new Set<AgentEventHandler>();
|
|
229
|
+
private sessionId: string | null = null;
|
|
230
|
+
private current: AgentEnvironmentProcess | null = null;
|
|
231
|
+
private queue: Promise<void> = Promise.resolve();
|
|
232
|
+
private sentPreamble = false;
|
|
233
|
+
private closed = false;
|
|
234
|
+
|
|
235
|
+
constructor(args: {
|
|
236
|
+
taskId: string;
|
|
237
|
+
agent: RosterAgent;
|
|
238
|
+
environment: TaskEnvironmentAgentSessionSurface;
|
|
239
|
+
systemPreamble: string;
|
|
240
|
+
agentEnv?: Record<string, string>;
|
|
241
|
+
spawner?: Spawner;
|
|
242
|
+
apiKey?: string;
|
|
243
|
+
/** Reads the CLI's auth.json from the task environment. Tests inject this.
|
|
244
|
+
* The signal aborts when the probe deadline passes; the default reader
|
|
245
|
+
* kills the auth process. */
|
|
246
|
+
readAuthFile?: (signal: AbortSignal) => Promise<string | null>;
|
|
247
|
+
/** Dashboard fetch. Tests inject this. Production uses the host's fetch. */
|
|
248
|
+
fetchImpl?: typeof fetch;
|
|
249
|
+
/** Whole-probe bound, including the auth.json read. Tests shrink it. */
|
|
250
|
+
probeTimeoutMs?: number;
|
|
251
|
+
}) {
|
|
252
|
+
this.agentId = args.agent.id;
|
|
253
|
+
this.environment = args.environment;
|
|
254
|
+
this.model = args.agent.model;
|
|
255
|
+
this.systemPreamble = args.systemPreamble;
|
|
256
|
+
this.agentEnv = args.agentEnv ?? {};
|
|
257
|
+
this.spawner =
|
|
258
|
+
args.spawner ?? ((request) => spawnEnvironmentProcess(this.environment, request));
|
|
259
|
+
// An account-bound key (ADR-076/ADR-131 D3, injected through agentEnv)
|
|
260
|
+
// beats the host's own .env.local key — a workspace's Cursor account must
|
|
261
|
+
// never fall through to the operator's, and on a shared host the ambient
|
|
262
|
+
// key is usually absent anyway.
|
|
263
|
+
this.apiKey =
|
|
264
|
+
args.apiKey ?? this.agentEnv.CURSOR_API_KEY ?? process.env.CURSOR_API_KEY ?? "";
|
|
265
|
+
this.readAuthFile =
|
|
266
|
+
args.readAuthFile ?? ((signal) => readCursorAuthFromEnvironment(this.environment, signal));
|
|
267
|
+
this.fetchImpl = args.fetchImpl ?? fetch;
|
|
268
|
+
this.probeTimeoutMs = args.probeTimeoutMs ?? CURSOR_PERIOD_PROBE_TIMEOUT_MS;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* One dashboard probe for a leased account. The read runs after the CLI
|
|
273
|
+
* child has exited: the CLI rewrites ~/.config/cursor/auth.json at
|
|
274
|
+
* startup, and a probe overlapped with the turn reads the expired access
|
|
275
|
+
* token. A missing lease, a probe still inside the five-minute gate, or
|
|
276
|
+
* any failure sends nothing. The token never leaves this method except
|
|
277
|
+
* as the bearer of the two dashboard calls.
|
|
278
|
+
*
|
|
279
|
+
* The credential is marked before the first await. Two sessions that start
|
|
280
|
+
* together must not both pass the gate. The returned function is the read;
|
|
281
|
+
* call it once the child has exited, still before turn_complete.
|
|
282
|
+
*/
|
|
283
|
+
private probePeriodUsage(): () => Promise<void> {
|
|
284
|
+
const skip = (): Promise<void> => Promise.resolve();
|
|
285
|
+
const credentialId = this.agentEnv[ENGINE_ACCOUNT_ENV];
|
|
286
|
+
if (!credentialId || this.closed) return skip;
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
if (!cursorPeriodProbeDue(credentialId, now)) return skip;
|
|
289
|
+
markCursorPeriodProbed(credentialId, now);
|
|
290
|
+
return () => this.readPeriodUsage();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Auth read and both dashboard calls share one deadline. On expiry the
|
|
294
|
+
* auth reader is aborted (the default reader kills its process) and this
|
|
295
|
+
* promise settles so turn_complete is not held. */
|
|
296
|
+
private readPeriodUsage(): Promise<void> {
|
|
297
|
+
const controller = new AbortController();
|
|
298
|
+
const timer = setTimeout(() => controller.abort(), this.probeTimeoutMs);
|
|
299
|
+
let finished = false;
|
|
300
|
+
const usagePromise = this.collectPeriodUsage(controller.signal);
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
const finish = (usage: AgentAccountUsage | null): void => {
|
|
303
|
+
if (finished) return;
|
|
304
|
+
finished = true;
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
if (!this.closed) {
|
|
307
|
+
if (!usage) console.warn(CURSOR_PERIOD_USAGE_LOG);
|
|
308
|
+
else this.emit({ type: "account_usage", usage });
|
|
309
|
+
}
|
|
310
|
+
resolve();
|
|
311
|
+
};
|
|
312
|
+
controller.signal.addEventListener("abort", () => finish(null));
|
|
313
|
+
void usagePromise.then(
|
|
314
|
+
(usage) => finish(controller.signal.aborted ? null : usage),
|
|
315
|
+
() => finish(null),
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private async collectPeriodUsage(signal: AbortSignal): Promise<AgentAccountUsage | null> {
|
|
321
|
+
if (signal.aborted) return null;
|
|
322
|
+
const raw = await this.readAuthFile(signal);
|
|
323
|
+
if (signal.aborted) return null;
|
|
324
|
+
const token = raw ? cursorAccessTokenFromAuthJson(raw) : null;
|
|
325
|
+
if (!token) return null;
|
|
326
|
+
return readCursorPeriodUsage({
|
|
327
|
+
token,
|
|
328
|
+
fetchImpl: this.fetchImpl,
|
|
329
|
+
signal,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
onEvent(handler: AgentEventHandler): () => void {
|
|
334
|
+
this.handlers.add(handler);
|
|
335
|
+
return () => this.handlers.delete(handler);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private emit(event: AgentEvent): void {
|
|
339
|
+
if (this.closed && event.type !== "exit") return;
|
|
340
|
+
for (const h of this.handlers) h(event);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async send(text: string): Promise<void> {
|
|
344
|
+
if (this.closed) return;
|
|
345
|
+
const turn = this.queue.catch(() => {}).then(() => this.runTurn(text));
|
|
346
|
+
this.queue = turn;
|
|
347
|
+
return turn;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private buildRequest(prompt: string): TaskEnvironmentSessionRequest {
|
|
351
|
+
const argv = [
|
|
352
|
+
CURSOR_BIN,
|
|
353
|
+
"--disable-auto-update",
|
|
354
|
+
"-p",
|
|
355
|
+
prompt,
|
|
356
|
+
"--output-format",
|
|
357
|
+
"stream-json",
|
|
358
|
+
"--stream-partial-output",
|
|
359
|
+
"--force", // container is the sandbox — auto-run tools
|
|
360
|
+
"--trust", // skip the workspace-trust prompt in headless
|
|
361
|
+
"--approve-mcps", // load the gateway MCP servers from ~/.cursor/mcp.json (ADR-057)
|
|
362
|
+
];
|
|
363
|
+
// Long form only: the pinned CLI (2026.08.25) and its successors have no
|
|
364
|
+
// `-m` short flag and exit on it before the turn starts.
|
|
365
|
+
if (this.model && this.model !== "auto") argv.push("--model", this.model);
|
|
366
|
+
if (this.sessionId) argv.push("--resume", this.sessionId);
|
|
367
|
+
return {
|
|
368
|
+
argv: argv as [string, ...string[]],
|
|
369
|
+
projectEnvProfile: "task",
|
|
370
|
+
user: "node",
|
|
371
|
+
env: {
|
|
372
|
+
...this.agentEnv,
|
|
373
|
+
CURSOR_API_KEY: this.apiKey,
|
|
374
|
+
...(this.environment.descriptor.envMode === "loader"
|
|
375
|
+
? { __CURSOR_SANDBOX_ENV_RESTORE: CURSOR_SANDBOX_ENV_RESTORE }
|
|
376
|
+
: {}),
|
|
377
|
+
},
|
|
378
|
+
maxOutputBytes: AGENT_SESSION_OUTPUT_BUFFER_BYTES,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private async runTurn(text: string): Promise<void> {
|
|
383
|
+
if (this.closed) return;
|
|
384
|
+
requireContainerRuntimeOperational();
|
|
385
|
+
// No system-prompt flag — fold the briefing into the first turn; later
|
|
386
|
+
// turns carry it via the resumed session.
|
|
387
|
+
const prompt =
|
|
388
|
+
!this.sentPreamble && this.systemPreamble.trim().length > 0
|
|
389
|
+
? `${this.systemPreamble}\n\n---\n\n${text}`
|
|
390
|
+
: text;
|
|
391
|
+
this.sentPreamble = true;
|
|
392
|
+
|
|
393
|
+
const child = this.spawner(this.buildRequest(prompt));
|
|
394
|
+
this.current = child;
|
|
395
|
+
// Mark the gate now, before the child-exit await. The read itself waits
|
|
396
|
+
// until the CLI has rewritten auth.json.
|
|
397
|
+
const periodUsage = this.probePeriodUsage();
|
|
398
|
+
|
|
399
|
+
let acc = "";
|
|
400
|
+
let sawText = false;
|
|
401
|
+
let buf = "";
|
|
402
|
+
// Usage rides the result line but turn_complete fires on process exit;
|
|
403
|
+
// capture it here and attach it below (ADR-071 metering).
|
|
404
|
+
let turnUsage: AgentUsage | undefined;
|
|
405
|
+
// A result line that already explained the failure owns the error. The
|
|
406
|
+
// exit path must not add a second "cursor exited N" for the same turn.
|
|
407
|
+
// A usage cap is tracked on its own: a generic JSON error must not hide
|
|
408
|
+
// a cap that only shows up on stderr.
|
|
409
|
+
let sawError = false;
|
|
410
|
+
let reportedRateLimit = false;
|
|
411
|
+
let reportedCreditRefusal = false;
|
|
412
|
+
const consume = (chunk: string): void => {
|
|
413
|
+
buf += chunk;
|
|
414
|
+
let nl: number;
|
|
415
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
416
|
+
const line = buf.slice(0, nl);
|
|
417
|
+
buf = buf.slice(nl + 1);
|
|
418
|
+
const m = mapCursorLine(line);
|
|
419
|
+
if (m.sessionId) this.sessionId = m.sessionId;
|
|
420
|
+
if (typeof m.textDelta === "string") {
|
|
421
|
+
// Cursor (--stream-partial-output) streams incremental token deltas
|
|
422
|
+
// and then a REDUNDANT full-message snapshot. The timestamp_ms
|
|
423
|
+
// heuristic in mapCursorLine drops that final snapshot for models
|
|
424
|
+
// whose final line is untagged — but Cursor's multi-model support
|
|
425
|
+
// (ADR-068) means SOME models tag the final full snapshot with
|
|
426
|
+
// timestamp_ms too, so it slips through and gets appended on top of
|
|
427
|
+
// the tokens: every message doubled (live 2026-07-21). Reconcile
|
|
428
|
+
// against what we've already streamed instead of trusting the tag —
|
|
429
|
+
// robust to incremental tokens, cumulative snapshots, and the
|
|
430
|
+
// redundant final repeat alike.
|
|
431
|
+
const t = m.textDelta;
|
|
432
|
+
let delta: string | null;
|
|
433
|
+
if (t === acc) {
|
|
434
|
+
delta = null; // full-message snapshot we've already emitted
|
|
435
|
+
} else if (acc.length > 0 && t.startsWith(acc)) {
|
|
436
|
+
delta = t.slice(acc.length); // cumulative snapshot → growth only
|
|
437
|
+
acc = t;
|
|
438
|
+
} else {
|
|
439
|
+
delta = t; // incremental token / genuinely new text
|
|
440
|
+
acc += t;
|
|
441
|
+
}
|
|
442
|
+
if (delta) {
|
|
443
|
+
sawText = true;
|
|
444
|
+
this.emit({ type: "message_delta", text: delta });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (m.toolCall) {
|
|
448
|
+
this.emit({
|
|
449
|
+
type: "tool_call",
|
|
450
|
+
id: m.toolCall.id,
|
|
451
|
+
title: m.toolCall.title,
|
|
452
|
+
detail: m.toolCall.detail,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (m.end) {
|
|
456
|
+
if (m.usage) turnUsage = m.usage;
|
|
457
|
+
if (m.errorText) {
|
|
458
|
+
sawError = true;
|
|
459
|
+
if (m.creditRefusal) reportedCreditRefusal = true;
|
|
460
|
+
if (m.accountUsage) {
|
|
461
|
+
reportedRateLimit = true;
|
|
462
|
+
this.emit({ type: "account_usage", usage: m.accountUsage });
|
|
463
|
+
this.emit({
|
|
464
|
+
type: "error",
|
|
465
|
+
message: m.errorText,
|
|
466
|
+
retryable: "rate_limit",
|
|
467
|
+
...(m.accountUsage.resetsAt === undefined
|
|
468
|
+
? {}
|
|
469
|
+
: { resetsAt: m.accountUsage.resetsAt }),
|
|
470
|
+
});
|
|
471
|
+
} else {
|
|
472
|
+
this.emit({ type: "error", message: m.errorText });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const finalText = m.finalText ?? acc;
|
|
476
|
+
if (sawText || finalText) {
|
|
477
|
+
this.emit({ type: "message_complete", text: finalText });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
child.stdout?.on("data", (b: Buffer) => consume(b.toString("utf8")));
|
|
483
|
+
let stderr = "";
|
|
484
|
+
child.stderr?.on("data", (b: Buffer) => {
|
|
485
|
+
stderr += b.toString("utf8");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
await new Promise<void>((resolve) => {
|
|
489
|
+
const finish = (code: number | null, spawnErr?: string): void => {
|
|
490
|
+
this.current = null;
|
|
491
|
+
if (buf.trim().length > 0) consume("\n");
|
|
492
|
+
if (this.closed) return resolve();
|
|
493
|
+
if (spawnErr) {
|
|
494
|
+
this.emit({ type: "error", message: `cursor spawn failed: ${spawnErr}` });
|
|
495
|
+
} else if (code !== 0) {
|
|
496
|
+
// Some cursor-agent errors (e.g. an invalid key) are coloured; strip
|
|
497
|
+
// the escape codes so the cap check and the error a human sees are
|
|
498
|
+
// plain text. A generic result error must not skip this check.
|
|
499
|
+
const plain = stripVTControlCharacters(stderr);
|
|
500
|
+
const tail = plain.trim().slice(-500);
|
|
501
|
+
const creditRefusal = cursorCreditsRequiredMessage(plain);
|
|
502
|
+
const message =
|
|
503
|
+
creditRefusal ??
|
|
504
|
+
`cursor exited ${code ?? "null"}${tail ? `: ${tail}` : ""}`;
|
|
505
|
+
const accountUsage = reportedRateLimit ? undefined : rateLimitErrorUsage(plain);
|
|
506
|
+
if (accountUsage) {
|
|
507
|
+
this.emit({ type: "account_usage", usage: accountUsage });
|
|
508
|
+
}
|
|
509
|
+
if (!sawError || (creditRefusal && !reportedCreditRefusal)) {
|
|
510
|
+
if (accountUsage) {
|
|
511
|
+
this.emit({
|
|
512
|
+
type: "error",
|
|
513
|
+
message,
|
|
514
|
+
retryable: "rate_limit",
|
|
515
|
+
...(accountUsage.resetsAt === undefined
|
|
516
|
+
? {}
|
|
517
|
+
: { resetsAt: accountUsage.resetsAt }),
|
|
518
|
+
});
|
|
519
|
+
} else {
|
|
520
|
+
this.emit({ type: "error", message });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
resolve();
|
|
525
|
+
};
|
|
526
|
+
child.on("exit", (code) => finish(code));
|
|
527
|
+
child.on("error", (err) => finish(null, err.message));
|
|
528
|
+
});
|
|
529
|
+
await periodUsage();
|
|
530
|
+
if (!this.closed) this.emit({ type: "turn_complete", usage: turnUsage });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async interrupt(): Promise<void> {
|
|
534
|
+
this.current?.kill("SIGKILL");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async resolvePermission(): Promise<void> {
|
|
538
|
+
/* --force auto-approves; no permission requests are emitted */
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async close(): Promise<void> {
|
|
542
|
+
if (this.closed) return;
|
|
543
|
+
this.closed = true;
|
|
544
|
+
this.current?.kill("SIGKILL");
|
|
545
|
+
this.current = null;
|
|
546
|
+
this.emit({ type: "exit", code: 0 });
|
|
547
|
+
this.handlers.clear();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
register({
|
|
552
|
+
kind: "cursor",
|
|
553
|
+
label: "Cursor",
|
|
554
|
+
supportedModels: () => [...CURSOR_MODELS],
|
|
555
|
+
defaultModel: CURSOR_DEFAULT_MODEL,
|
|
556
|
+
supportedEfforts: () => [...CURSOR_EFFORTS],
|
|
557
|
+
// Gated on a Cursor API key in the host env (loaded from .env.local); the
|
|
558
|
+
// adapter injects it into the container. No key → engine not advertised.
|
|
559
|
+
available: () => Boolean(process.env.CURSOR_API_KEY),
|
|
560
|
+
create: async ({ taskId, agent, environment, systemPreamble, agentEnv }) =>
|
|
561
|
+
new CursorSession({ taskId, agent, environment, systemPreamble, agentEnv }),
|
|
562
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Shared typed-dispatch contract for adapters that expose ADR-083's action. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
MAX_AGENT_ID_CHARS,
|
|
5
|
+
MAX_SECRETARY_DISPATCH_INSTRUCTION_CHARS,
|
|
6
|
+
MAX_SECRETARY_DISPATCH_RECIPIENTS,
|
|
7
|
+
} from "../../src/protocol";
|
|
8
|
+
|
|
9
|
+
export const DISPATCH_TOOL_NAME = "dispatch";
|
|
10
|
+
export const MAX_DISPATCH_RECIPIENTS = MAX_SECRETARY_DISPATCH_RECIPIENTS;
|
|
11
|
+
export const MAX_DISPATCH_RECIPIENT_ID_CHARS =
|
|
12
|
+
MAX_AGENT_ID_CHARS;
|
|
13
|
+
export const MAX_DISPATCH_INSTRUCTION_CHARS =
|
|
14
|
+
MAX_SECRETARY_DISPATCH_INSTRUCTION_CHARS;
|
|
15
|
+
export const DISPATCH_RECIPIENT_ID_PATTERN = "^[A-Za-z0-9_-]+$";
|
|
16
|
+
|
|
17
|
+
export const DISPATCH_INPUT_SCHEMA = {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
recipients: {
|
|
21
|
+
type: "array",
|
|
22
|
+
minItems: 1,
|
|
23
|
+
maxItems: MAX_DISPATCH_RECIPIENTS,
|
|
24
|
+
uniqueItems: true,
|
|
25
|
+
items: {
|
|
26
|
+
type: "string",
|
|
27
|
+
minLength: 1,
|
|
28
|
+
maxLength: MAX_DISPATCH_RECIPIENT_ID_CHARS,
|
|
29
|
+
pattern: DISPATCH_RECIPIENT_ID_PATTERN,
|
|
30
|
+
},
|
|
31
|
+
description:
|
|
32
|
+
"Exact @ids of the crew agents that should receive the instruction, without the @ prefix.",
|
|
33
|
+
},
|
|
34
|
+
instruction: {
|
|
35
|
+
type: "string",
|
|
36
|
+
minLength: 1,
|
|
37
|
+
maxLength: MAX_DISPATCH_INSTRUCTION_CHARS,
|
|
38
|
+
description:
|
|
39
|
+
"Self-contained backstage instruction. Include all context the recipients need; do not include prose meant only for the human.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ["recipients", "instruction"],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
export interface DispatchInput {
|
|
47
|
+
recipients: string[];
|
|
48
|
+
instruction: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const recipientId = new RegExp(DISPATCH_RECIPIENT_ID_PATTERN);
|
|
52
|
+
|
|
53
|
+
export function parseDispatchInput(value: unknown): DispatchInput | null {
|
|
54
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const record = value as Record<string, unknown>;
|
|
58
|
+
const recipients = record.recipients;
|
|
59
|
+
const instruction = record.instruction;
|
|
60
|
+
if (
|
|
61
|
+
!Array.isArray(recipients) ||
|
|
62
|
+
recipients.length === 0 ||
|
|
63
|
+
recipients.length > MAX_DISPATCH_RECIPIENTS ||
|
|
64
|
+
!recipients.every(
|
|
65
|
+
(recipient): recipient is string =>
|
|
66
|
+
typeof recipient === "string" &&
|
|
67
|
+
recipient.length <= MAX_DISPATCH_RECIPIENT_ID_CHARS &&
|
|
68
|
+
recipientId.test(recipient),
|
|
69
|
+
) ||
|
|
70
|
+
new Set(recipients).size !== recipients.length ||
|
|
71
|
+
typeof instruction !== "string" ||
|
|
72
|
+
instruction.trim().length === 0 ||
|
|
73
|
+
instruction.length > MAX_DISPATCH_INSTRUCTION_CHARS
|
|
74
|
+
) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return { recipients, instruction };
|
|
78
|
+
}
|