@amalgm/chat 0.1.1 → 0.2.0
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/AGENTS.md +98 -0
- package/PURPOSE.md +143 -104
- package/README.md +82 -54
- package/dist/acp/index.d.ts +7 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +7 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/acp/projection.d.ts +30 -0
- package/dist/acp/projection.d.ts.map +1 -0
- package/dist/acp/projection.js +147 -0
- package/dist/acp/projection.js.map +1 -0
- package/dist/api/conversations.d.ts +66 -0
- package/dist/api/conversations.d.ts.map +1 -0
- package/dist/api/conversations.js +202 -0
- package/dist/api/conversations.js.map +1 -0
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -2
- package/dist/api/index.js.map +1 -1
- package/dist/conversations/index.d.ts +3 -0
- package/dist/conversations/index.d.ts.map +1 -0
- package/dist/conversations/index.js +3 -0
- package/dist/conversations/index.js.map +1 -0
- package/dist/conversations/transcript.d.ts +13 -0
- package/dist/conversations/transcript.d.ts.map +1 -0
- package/dist/conversations/transcript.js +171 -0
- package/dist/conversations/transcript.js.map +1 -0
- package/dist/conversations/types.d.ts +147 -0
- package/dist/conversations/types.d.ts.map +1 -0
- package/dist/conversations/types.js +9 -0
- package/dist/conversations/types.js.map +1 -0
- package/dist/events/constructors.d.ts +9 -15
- package/dist/events/constructors.d.ts.map +1 -1
- package/dist/events/constructors.js +6 -12
- package/dist/events/constructors.js.map +1 -1
- package/dist/events/index.d.ts +2 -3
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +2 -3
- package/dist/events/index.js.map +1 -1
- package/dist/events/schema.d.ts +2 -5
- package/dist/events/schema.d.ts.map +1 -1
- package/dist/events/schema.js +2 -5
- package/dist/events/schema.js.map +1 -1
- package/dist/execution/contract.d.ts +51 -0
- package/dist/execution/contract.d.ts.map +1 -0
- package/dist/execution/contract.js +127 -0
- package/dist/execution/contract.js.map +1 -0
- package/dist/execution/index.d.ts +6 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/index.js +4 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/revision.d.ts +5 -0
- package/dist/execution/revision.d.ts.map +1 -0
- package/dist/execution/revision.js +17 -0
- package/dist/execution/revision.js.map +1 -0
- package/dist/execution/store.d.ts +26 -0
- package/dist/execution/store.d.ts.map +1 -0
- package/dist/execution/store.js +38 -0
- package/dist/execution/store.js.map +1 -0
- package/dist/index.d.ts +8 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -23
- package/dist/index.js.map +1 -1
- package/dist/input/index.d.ts +3 -0
- package/dist/input/index.d.ts.map +1 -0
- package/dist/input/index.js +2 -0
- package/dist/input/index.js.map +1 -0
- package/dist/input/prompt.d.ts +17 -0
- package/dist/input/prompt.d.ts.map +1 -0
- package/dist/input/prompt.js +104 -0
- package/dist/input/prompt.js.map +1 -0
- package/dist/normalizers/claude.d.ts +6 -9
- package/dist/normalizers/claude.d.ts.map +1 -1
- package/dist/normalizers/claude.js +5 -8
- package/dist/normalizers/claude.js.map +1 -1
- package/dist/normalizers/codex.d.ts +5 -8
- package/dist/normalizers/codex.d.ts.map +1 -1
- package/dist/normalizers/codex.js +4 -7
- package/dist/normalizers/codex.js.map +1 -1
- package/dist/normalizers/cursor.d.ts +1 -2
- package/dist/normalizers/cursor.d.ts.map +1 -1
- package/dist/normalizers/cursor.js +2 -3
- package/dist/normalizers/cursor.js.map +1 -1
- package/dist/normalizers/index.d.ts +3 -5
- package/dist/normalizers/index.d.ts.map +1 -1
- package/dist/normalizers/index.js +3 -5
- package/dist/normalizers/index.js.map +1 -1
- package/dist/normalizers/opencode.d.ts +2 -4
- package/dist/normalizers/opencode.d.ts.map +1 -1
- package/dist/normalizers/opencode.js +2 -4
- package/dist/normalizers/opencode.js.map +1 -1
- package/dist/normalizers/pi.d.ts +3 -4
- package/dist/normalizers/pi.d.ts.map +1 -1
- package/dist/normalizers/pi.js +3 -4
- package/dist/normalizers/pi.js.map +1 -1
- package/dist/sessions/chat.d.ts +176 -0
- package/dist/sessions/chat.d.ts.map +1 -0
- package/dist/sessions/chat.js +701 -0
- package/dist/sessions/chat.js.map +1 -0
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/transcript/index.d.ts +1 -1
- package/dist/transcript/index.js +1 -1
- package/dist/transcript/message-reducer.d.ts +1 -1
- package/dist/transcript/message-reducer.js +1 -1
- package/dist/transcript/part-accumulator.d.ts +1 -1
- package/dist/transcript/part-accumulator.js +1 -1
- package/dist/transcript/tool-display.d.ts +1 -1
- package/dist/transcript/tool-display.js +1 -1
- package/dist/transcript/tool-shape.d.ts +1 -1
- package/dist/transcript/tool-shape.js +1 -1
- package/dist/usage/finalize.d.ts +7 -9
- package/dist/usage/finalize.d.ts.map +1 -1
- package/dist/usage/finalize.js +11 -13
- package/dist/usage/finalize.js.map +1 -1
- package/dist/usage/index.d.ts +2 -3
- package/dist/usage/index.d.ts.map +1 -1
- package/dist/usage/index.js +2 -3
- package/dist/usage/index.js.map +1 -1
- package/dist/usage/model-catalog.d.ts +2 -3
- package/dist/usage/model-catalog.d.ts.map +1 -1
- package/dist/usage/model-catalog.js +10 -3
- package/dist/usage/model-catalog.js.map +1 -1
- package/dist/usage/normalize.d.ts +5 -7
- package/dist/usage/normalize.d.ts.map +1 -1
- package/dist/usage/normalize.js +3 -5
- package/dist/usage/normalize.js.map +1 -1
- package/dist/usage/ports.d.ts +14 -24
- package/dist/usage/ports.d.ts.map +1 -1
- package/dist/usage/ports.js +9 -17
- package/dist/usage/ports.js.map +1 -1
- package/dist/usage/steps.d.ts +70 -0
- package/dist/usage/steps.d.ts.map +1 -0
- package/dist/usage/steps.js +152 -0
- package/dist/usage/steps.js.map +1 -0
- package/dist/usage/tokens.d.ts +4 -7
- package/dist/usage/tokens.d.ts.map +1 -1
- package/dist/usage/tokens.js +4 -7
- package/dist/usage/tokens.js.map +1 -1
- package/docs/contracts/acp-and-step-usage.md +51 -0
- package/docs/contracts/conversation-persistence.md +47 -0
- package/docs/contracts/input-and-execution.md +177 -0
- package/docs/contracts/platform-authorization.md +17 -0
- package/host/adapters/acp-capabilities.js +30 -0
- package/host/adapters/acp-client.js +156 -0
- package/host/adapters/acp.js +284 -0
- package/host/adapters/claude.js +235 -0
- package/host/adapters/codex.js +733 -0
- package/host/adapters/cursor.js +347 -0
- package/host/adapters/input-capabilities.js +89 -0
- package/host/adapters/opencode.js +486 -0
- package/host/adapters/pi.js +519 -0
- package/host/adapters/prompt.js +212 -0
- package/host/auth.js +257 -0
- package/host/credentials/store.js +209 -0
- package/host/http.d.ts +32 -0
- package/host/http.js +238 -0
- package/host/index.d.ts +84 -0
- package/host/index.js +59 -0
- package/host/lib/runtime-paths.js +44 -0
- package/host/model-resolution.js +313 -0
- package/host/native-contract.js +185 -0
- package/host/native-runtime.js +194 -0
- package/host/permissions.js +27 -0
- package/host/platform-egress.js +114 -0
- package/host/recorder.js +61 -0
- package/host/sqlite/index.d.ts +69 -0
- package/host/sqlite/index.js +704 -0
- package/host/tooling/mcp-bundle.js +214 -0
- package/host/tooling/mcp-relay.js +83 -0
- package/host/tooling/native-binaries.js +667 -0
- package/host/tooling/native-config.js +460 -0
- package/host/tooling/package-import.js +111 -0
- package/host/tooling/runtime-home.js +26 -0
- package/host/tooling/system-instructions.js +69 -0
- package/host/tooling/system-prompt.js +35 -0
- package/package.json +45 -21
- package/skills/chat/SKILL.md +97 -0
- package/skills/chat/agents/openai.yaml +4 -0
- package/skills/chat/references/contracts.md +131 -0
- package/dist/api/chat-payloads.d.ts +0 -10
- package/dist/api/chat-payloads.d.ts.map +0 -1
- package/dist/api/chat-payloads.js +0 -57
- package/dist/api/chat-payloads.js.map +0 -1
- package/dist/contract/chat-input.d.ts +0 -144
- package/dist/contract/chat-input.d.ts.map +0 -1
- package/dist/contract/chat-input.js +0 -261
- package/dist/contract/chat-input.js.map +0 -1
- package/dist/contract/create.d.ts +0 -191
- package/dist/contract/create.d.ts.map +0 -1
- package/dist/contract/create.js +0 -249
- package/dist/contract/create.js.map +0 -1
- package/dist/contract/freeze.d.ts +0 -85
- package/dist/contract/freeze.d.ts.map +0 -1
- package/dist/contract/freeze.js +0 -151
- package/dist/contract/freeze.js.map +0 -1
- package/dist/contract/harnesses.d.ts +0 -21
- package/dist/contract/harnesses.d.ts.map +0 -1
- package/dist/contract/harnesses.js +0 -39
- package/dist/contract/harnesses.js.map +0 -1
- package/dist/contract/index.d.ts +0 -17
- package/dist/contract/index.d.ts.map +0 -1
- package/dist/contract/index.js +0 -17
- package/dist/contract/index.js.map +0 -1
- package/dist/contract/input.d.ts +0 -45
- package/dist/contract/input.d.ts.map +0 -1
- package/dist/contract/input.js +0 -78
- package/dist/contract/input.js.map +0 -1
- package/dist/contract/models.d.ts +0 -57
- package/dist/contract/models.d.ts.map +0 -1
- package/dist/contract/models.js +0 -380
- package/dist/contract/models.js.map +0 -1
- package/dist/contract/payload-merge.d.ts +0 -76
- package/dist/contract/payload-merge.d.ts.map +0 -1
- package/dist/contract/payload-merge.js +0 -215
- package/dist/contract/payload-merge.js.map +0 -1
- package/dist/events/tool-display.d.ts +0 -22
- package/dist/events/tool-display.d.ts.map +0 -1
- package/dist/events/tool-display.js +0 -60
- package/dist/events/tool-display.js.map +0 -1
- package/dist/events/tool-shape.d.ts +0 -48
- package/dist/events/tool-shape.d.ts.map +0 -1
- package/dist/events/tool-shape.js +0 -225
- package/dist/events/tool-shape.js.map +0 -1
- package/dist/turns/index.d.ts +0 -11
- package/dist/turns/index.d.ts.map +0 -1
- package/dist/turns/index.js +0 -11
- package/dist/turns/index.js.map +0 -1
- package/dist/turns/queries.d.ts +0 -36
- package/dist/turns/queries.d.ts.map +0 -1
- package/dist/turns/queries.js +0 -37
- package/dist/turns/queries.js.map +0 -1
- package/dist/turns/replay.d.ts +0 -105
- package/dist/turns/replay.d.ts.map +0 -1
- package/dist/turns/replay.js +0 -163
- package/dist/turns/replay.js.map +0 -1
- package/dist/turns/status.d.ts +0 -49
- package/dist/turns/status.d.ts.map +0 -1
- package/dist/turns/status.js +0 -56
- package/dist/turns/status.js.map +0 -1
- package/dist/turns/turn-store.d.ts +0 -109
- package/dist/turns/turn-store.d.ts.map +0 -1
- package/dist/turns/turn-store.js +0 -169
- package/dist/turns/turn-store.js.map +0 -1
- package/dist/warm/client-cache.d.ts +0 -47
- package/dist/warm/client-cache.d.ts.map +0 -1
- package/dist/warm/client-cache.js +0 -48
- package/dist/warm/client-cache.js.map +0 -1
- package/dist/warm/envelope-cache.d.ts +0 -79
- package/dist/warm/envelope-cache.d.ts.map +0 -1
- package/dist/warm/envelope-cache.js +0 -85
- package/dist/warm/envelope-cache.js.map +0 -1
- package/dist/warm/index.d.ts +0 -18
- package/dist/warm/index.d.ts.map +0 -1
- package/dist/warm/index.js +0 -12
- package/dist/warm/index.js.map +0 -1
- package/dist/warm/payload.d.ts +0 -137
- package/dist/warm/payload.d.ts.map +0 -1
- package/dist/warm/payload.js +0 -112
- package/dist/warm/payload.js.map +0 -1
- package/dist/warm/revision.d.ts +0 -70
- package/dist/warm/revision.d.ts.map +0 -1
- package/dist/warm/revision.js +0 -63
- package/dist/warm/revision.js.map +0 -1
- package/dist/warm/store-policy.d.ts +0 -57
- package/dist/warm/store-policy.d.ts.map +0 -1
- package/dist/warm/store-policy.js +0 -89
- package/dist/warm/store-policy.js.map +0 -1
- package/dist/warm/store.d.ts +0 -42
- package/dist/warm/store.d.ts.map +0 -1
- package/dist/warm/store.js +0 -69
- package/dist/warm/store.js.map +0 -1
- package/dist/wire/frames.d.ts +0 -208
- package/dist/wire/frames.d.ts.map +0 -1
- package/dist/wire/frames.js +0 -245
- package/dist/wire/frames.js.map +0 -1
- package/dist/wire/index.d.ts +0 -7
- package/dist/wire/index.d.ts.map +0 -1
- package/dist/wire/index.js +0 -7
- package/dist/wire/index.js.map +0 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// Cursor Agent Client Protocol adapter. The generic ACP transport lives in
|
|
2
|
+
// ./acp-client.js; this file is the
|
|
3
|
+
// cursor parameterization (binary resolution, model translation, permission
|
|
4
|
+
// auto-grant, first-prompt system preamble).
|
|
5
|
+
//
|
|
6
|
+
// Laws come from ../../dist: event constructors via `createEventConstructors`,
|
|
7
|
+
// the normalizer via `createCursorNormalizer`, `cursorStopReason`, and
|
|
8
|
+
// Effectful tooling and the recorder are sibling host modules.
|
|
9
|
+
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { createEventConstructors } from '../../dist/events/index.js';
|
|
14
|
+
import { createCursorNormalizer, cursorStopReason } from '../../dist/normalizers/cursor.js';
|
|
15
|
+
import { recordNativeEvent } from '../recorder.js';
|
|
16
|
+
import { toAcpMcpServers } from '../tooling/mcp-bundle.js';
|
|
17
|
+
import { executableExists, findOnPath } from '../tooling/native-binaries.js';
|
|
18
|
+
import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
|
|
19
|
+
import { composeSystemPrompt } from '../tooling/system-prompt.js';
|
|
20
|
+
import { requestPermission } from '../permissions.js';
|
|
21
|
+
import { AcpClient } from './acp-client.js';
|
|
22
|
+
import { assertAcpMcpCapabilities, assertAcpPromptCapabilities } from './acp-capabilities.js';
|
|
23
|
+
import {
|
|
24
|
+
COMMON_REASONING_SETTINGS,
|
|
25
|
+
COMMON_SPEED_SETTINGS,
|
|
26
|
+
nativeInputCapabilities,
|
|
27
|
+
UnsupportedExecutionInputError,
|
|
28
|
+
} from './input-capabilities.js';
|
|
29
|
+
import { acpPromptParts } from './prompt.js';
|
|
30
|
+
|
|
31
|
+
const events = createEventConstructors({ clockMs: () => Date.now() });
|
|
32
|
+
const { done, errorEvent, warningEvent } = events;
|
|
33
|
+
const { cursorPromptUsage, normalizeCursorUpdate } = createCursorNormalizer(events);
|
|
34
|
+
|
|
35
|
+
const PROTOCOL_VERSION = 1;
|
|
36
|
+
|
|
37
|
+
function resolveBinary() {
|
|
38
|
+
const binaryName = process.platform === 'win32' ? 'cursor-agent.exe' : 'cursor-agent';
|
|
39
|
+
const homes = [...new Set([process.env.AMALGM_NATIVE_HOME, os.homedir()].filter(Boolean))];
|
|
40
|
+
const candidates = [
|
|
41
|
+
process.env.CURSOR_AGENT_BINARY,
|
|
42
|
+
...homes.map((home) => path.join(home, '.local', 'bin', binaryName)),
|
|
43
|
+
'/opt/homebrew/bin/cursor-agent',
|
|
44
|
+
'/usr/local/bin/cursor-agent',
|
|
45
|
+
findOnPath('cursor-agent'),
|
|
46
|
+
].filter(Boolean);
|
|
47
|
+
const failures = [];
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
if (!executableExists(candidate)) {
|
|
50
|
+
failures.push(`${candidate} (missing or not executable)`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const res = spawnSync(candidate, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
|
|
54
|
+
if (res.status === 0) return candidate;
|
|
55
|
+
failures.push(`${candidate} (--version failed: ${res.error?.message || res.status || res.signal || 'unknown'})`);
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Cursor Agent CLI is not available. Checked: ${failures.join(', ') || 'no candidates'}. Install it with: curl https://cursor.com/install -fsS | bash, then run cursor-agent login.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// cursor-agent's session/set_model only accepts EXACT ids from availableModels
|
|
61
|
+
// (base name + bracket parameters, e.g. "gpt-5.3-codex[reasoning=medium,fast=false]").
|
|
62
|
+
// Amalgm cliModels are bare base names, so match by base and return the exact id.
|
|
63
|
+
function modelParameters(modelId) {
|
|
64
|
+
const match = String(modelId || '').match(/\[([^\]]*)\]$/);
|
|
65
|
+
if (!match) return {};
|
|
66
|
+
return Object.fromEntries(match[1].split(',').flatMap((entry) => {
|
|
67
|
+
const [key, value] = entry.split('=').map((part) => part.trim());
|
|
68
|
+
return key ? [[key, value]] : [];
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function requestedParameters(settings = []) {
|
|
73
|
+
const wanted = {};
|
|
74
|
+
for (const setting of settings) {
|
|
75
|
+
if (COMMON_REASONING_SETTINGS.includes(setting.configId)) {
|
|
76
|
+
wanted.reasoning = String(setting.value);
|
|
77
|
+
}
|
|
78
|
+
if (COMMON_SPEED_SETTINGS.includes(setting.configId)) {
|
|
79
|
+
wanted.fast = setting.value === true || setting.value === 'fast' ? 'true' : 'false';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return wanted;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function settingsMatch(modelId, settings) {
|
|
86
|
+
const wanted = requestedParameters(settings);
|
|
87
|
+
const params = modelParameters(modelId);
|
|
88
|
+
if (wanted.reasoning && params.reasoning !== wanted.reasoning && params.effort !== wanted.reasoning) return false;
|
|
89
|
+
if (wanted.fast && params.fast !== wanted.fast) return false;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function matchAcpModel(availableModels, requested, settings = []) {
|
|
94
|
+
const models = Array.isArray(availableModels) ? availableModels : [];
|
|
95
|
+
const baseOf = (id) => String(id || '').split('[')[0].trim();
|
|
96
|
+
let clean = String(requested || '').trim().replace(/^cursor\//, '').split('[')[0];
|
|
97
|
+
if (!clean) return null;
|
|
98
|
+
if (clean === 'auto' || clean === 'default') clean = 'default';
|
|
99
|
+
const findBase = (base) => models.find((m) => (
|
|
100
|
+
baseOf(m.modelId) === base || String(m.name || '').toLowerCase() === base.toLowerCase()
|
|
101
|
+
) && settingsMatch(m.modelId, settings));
|
|
102
|
+
const acpBaseAliases = (base) => {
|
|
103
|
+
const aliases = [];
|
|
104
|
+
const claudeVersionFirst = String(base || '').match(/^claude-(4(?:\.\d+)?)-(opus|sonnet)(?:-.+)?$/);
|
|
105
|
+
if (claudeVersionFirst) {
|
|
106
|
+
const [, version, family] = claudeVersionFirst;
|
|
107
|
+
aliases.push(`claude-${family}-${version.replace('.', '-')}`);
|
|
108
|
+
}
|
|
109
|
+
return aliases;
|
|
110
|
+
};
|
|
111
|
+
let candidate = clean;
|
|
112
|
+
// Legacy amalgm cursor cliModels carry effort/speed suffixes ("gpt-5.3-codex-high",
|
|
113
|
+
// "composer-2.5-fast") that ACP model ids do not; strip them until a base matches.
|
|
114
|
+
for (let i = 0; i < 6; i += 1) {
|
|
115
|
+
for (const base of [candidate, ...acpBaseAliases(candidate)]) {
|
|
116
|
+
const match = findBase(base);
|
|
117
|
+
if (match) return match;
|
|
118
|
+
}
|
|
119
|
+
const stripped = candidate.replace(/-(?:extra-high|fast|thinking|none|low|medium|high|xhigh|max)$/, '');
|
|
120
|
+
if (stripped === candidate) return null;
|
|
121
|
+
candidate = stripped;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pickPermissionOption(options = []) {
|
|
127
|
+
const byKind = (kind) => options.find((option) => option?.kind === kind);
|
|
128
|
+
return byKind('allow_always') || byKind('allow_once') || options[0] || null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function pickRejectOption(options = []) {
|
|
132
|
+
const byKind = (kind) => options.find((option) => option?.kind === kind);
|
|
133
|
+
return byKind('reject_once') || byKind('reject_always') || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class CursorAdapter {
|
|
137
|
+
constructor(options = {}) {
|
|
138
|
+
this.requestPermission = options.requestPermission || null;
|
|
139
|
+
this.inputCapabilities = nativeInputCapabilities({
|
|
140
|
+
authModes: ['subscription', 'byok'],
|
|
141
|
+
modelSettings: [...COMMON_REASONING_SETTINGS, ...COMMON_SPEED_SETTINGS],
|
|
142
|
+
tools: true,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async create(contract) {
|
|
147
|
+
const runtime = prepareHarnessRuntime(contract);
|
|
148
|
+
const binary = resolveBinary();
|
|
149
|
+
const client = new AcpClient({
|
|
150
|
+
binary,
|
|
151
|
+
args: ['acp'],
|
|
152
|
+
cwd: contract.cwd,
|
|
153
|
+
env: runtime.env,
|
|
154
|
+
label: 'Cursor',
|
|
155
|
+
});
|
|
156
|
+
const session = {
|
|
157
|
+
sessionId: contract.sessionId,
|
|
158
|
+
providerSessionId: contract.providerSessionId || null,
|
|
159
|
+
client,
|
|
160
|
+
needsSystemPrompt: false,
|
|
161
|
+
modelWarning: null,
|
|
162
|
+
activePrompt: null,
|
|
163
|
+
cancelRequested: false,
|
|
164
|
+
permissionMode: contract.permissionMode,
|
|
165
|
+
capabilities: {},
|
|
166
|
+
};
|
|
167
|
+
client.onRequest((method, params) => this.handleAgentRequest(session, method, params));
|
|
168
|
+
const initialized = await client.request('initialize', {
|
|
169
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
170
|
+
clientInfo: { name: 'amalgm-chat', version: '1.0.0' },
|
|
171
|
+
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
172
|
+
});
|
|
173
|
+
session.capabilities = initialized?.agentCapabilities || {};
|
|
174
|
+
const mcpServers = toAcpMcpServers(contract);
|
|
175
|
+
assertAcpMcpCapabilities('cursor', session.capabilities, mcpServers);
|
|
176
|
+
const sessionParams = { cwd: contract.cwd, mcpServers };
|
|
177
|
+
let acpSession = null;
|
|
178
|
+
if (session.providerSessionId) {
|
|
179
|
+
// session/load replays history as session/update notifications; nothing is
|
|
180
|
+
// subscribed yet, so the replay is discarded and the turn starts clean.
|
|
181
|
+
acpSession = await client.request('session/load', {
|
|
182
|
+
sessionId: session.providerSessionId,
|
|
183
|
+
...sessionParams,
|
|
184
|
+
}, 600000).catch((err) => {
|
|
185
|
+
console.warn('[Cursor] Failed to load native session; starting a new one:', err.message);
|
|
186
|
+
return null;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (!acpSession) {
|
|
190
|
+
acpSession = await client.request('session/new', sessionParams);
|
|
191
|
+
session.providerSessionId = acpSession.sessionId;
|
|
192
|
+
session.needsSystemPrompt = true;
|
|
193
|
+
}
|
|
194
|
+
await this.selectModel(session, acpSession, contract);
|
|
195
|
+
return session;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async selectModel(session, acpSession, contract) {
|
|
199
|
+
const requested = contract.cliModel || contract.modelId;
|
|
200
|
+
const models = acpSession?.models || {};
|
|
201
|
+
const match = matchAcpModel(models.availableModels, requested, contract.modelSettings);
|
|
202
|
+
if (!match) {
|
|
203
|
+
throw new UnsupportedExecutionInputError(
|
|
204
|
+
'cursor',
|
|
205
|
+
'provider + model settings',
|
|
206
|
+
`Cursor Agent does not offer the exact selection ${requested}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (match.modelId === models.currentModelId) return;
|
|
210
|
+
try {
|
|
211
|
+
await session.client.request('session/set_model', {
|
|
212
|
+
sessionId: session.providerSessionId,
|
|
213
|
+
modelId: match.modelId,
|
|
214
|
+
}, 30000);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
throw new UnsupportedExecutionInputError(
|
|
217
|
+
'cursor',
|
|
218
|
+
'provider + model settings',
|
|
219
|
+
`Cursor Agent rejected ${match.modelId}: ${err.message}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async handleAgentRequest(session, method, params) {
|
|
225
|
+
recordNativeEvent('cursor.acp.request', { method, params }, {
|
|
226
|
+
providerSessionId: session.providerSessionId,
|
|
227
|
+
sessionId: session.sessionId,
|
|
228
|
+
});
|
|
229
|
+
if (method === 'session/request_permission') {
|
|
230
|
+
if (session.cancelRequested) return { outcome: { outcome: 'cancelled' } };
|
|
231
|
+
let outcome = 'allow_always';
|
|
232
|
+
if (session.permissionMode === 'ask') {
|
|
233
|
+
const decision = await requestPermission(this.requestPermission, {
|
|
234
|
+
adapterId: 'cursor',
|
|
235
|
+
providerSessionId: session.providerSessionId,
|
|
236
|
+
kind: 'tool',
|
|
237
|
+
title: params.title || 'Cursor requests permission',
|
|
238
|
+
input: params,
|
|
239
|
+
});
|
|
240
|
+
outcome = decision.outcome;
|
|
241
|
+
}
|
|
242
|
+
const pick = outcome === 'allow_always'
|
|
243
|
+
? pickPermissionOption(params.options)
|
|
244
|
+
: outcome === 'allow_once'
|
|
245
|
+
? params.options?.find((option) => option?.kind === 'allow_once') || pickPermissionOption(params.options)
|
|
246
|
+
: pickRejectOption(params.options);
|
|
247
|
+
if (!pick) return { outcome: { outcome: 'cancelled' } };
|
|
248
|
+
return { outcome: { outcome: 'selected', optionId: pick.optionId } };
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Method not supported: ${method}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async resume(session, contract) {
|
|
254
|
+
session.permissionMode = contract.permissionMode;
|
|
255
|
+
if (session.client && !session.client.closed) return session;
|
|
256
|
+
return this.create({ ...contract, providerSessionId: session.providerSessionId || contract.providerSessionId });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async stop(session) {
|
|
260
|
+
session.cancelRequested = true;
|
|
261
|
+
if (!session.activePrompt) return;
|
|
262
|
+
session.client.notify('session/cancel', { sessionId: session.providerSessionId });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async destroy(session) {
|
|
266
|
+
session.client.close();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async *prompt(session, input, contract) {
|
|
270
|
+
assertAcpPromptCapabilities('cursor', session.capabilities, input);
|
|
271
|
+
const queue = [];
|
|
272
|
+
let complete = false;
|
|
273
|
+
let wake = null;
|
|
274
|
+
const wakeLoop = () => { if (wake) { wake(); wake = null; } };
|
|
275
|
+
const push = (e) => {
|
|
276
|
+
if (e.type === 'done' || e.type === 'error') complete = true;
|
|
277
|
+
queue.push(e);
|
|
278
|
+
wakeLoop();
|
|
279
|
+
};
|
|
280
|
+
const state = { providerSessionId: session.providerSessionId, tools: new Map() };
|
|
281
|
+
session.cancelRequested = false;
|
|
282
|
+
session.activePrompt = state;
|
|
283
|
+
if (session.modelWarning) {
|
|
284
|
+
push(warningEvent(session.modelWarning, { providerSessionId: session.providerSessionId }));
|
|
285
|
+
session.modelWarning = null;
|
|
286
|
+
}
|
|
287
|
+
const off = session.client.onNotification((msg) => {
|
|
288
|
+
if (msg.method !== 'session/update') return;
|
|
289
|
+
const params = msg.params || {};
|
|
290
|
+
if (params.sessionId && params.sessionId !== session.providerSessionId) return;
|
|
291
|
+
recordNativeEvent('cursor.acp.update', msg, {
|
|
292
|
+
providerSessionId: session.providerSessionId,
|
|
293
|
+
sessionId: contract.sessionId,
|
|
294
|
+
assistantMessageId: contract.assistantMessageId,
|
|
295
|
+
updateKind: params.update?.sessionUpdate,
|
|
296
|
+
});
|
|
297
|
+
for (const e of normalizeCursorUpdate(params.update || {}, state)) push(e);
|
|
298
|
+
});
|
|
299
|
+
let preamble = '';
|
|
300
|
+
if (session.needsSystemPrompt) {
|
|
301
|
+
// ACP has no system-prompt slot; a fresh native session gets the composed
|
|
302
|
+
// instructions as a preamble on its first prompt only.
|
|
303
|
+
const systemPrompt = composeSystemPrompt(contract);
|
|
304
|
+
if (systemPrompt) preamble = `<system-instructions>\n${systemPrompt}\n</system-instructions>`;
|
|
305
|
+
session.needsSystemPrompt = false;
|
|
306
|
+
}
|
|
307
|
+
session.client.request('session/prompt', {
|
|
308
|
+
sessionId: session.providerSessionId,
|
|
309
|
+
prompt: acpPromptParts(input, preamble),
|
|
310
|
+
}, 0).then((result) => {
|
|
311
|
+
recordNativeEvent('cursor.acp.prompt_result', result, {
|
|
312
|
+
providerSessionId: session.providerSessionId,
|
|
313
|
+
sessionId: contract.sessionId,
|
|
314
|
+
assistantMessageId: contract.assistantMessageId,
|
|
315
|
+
});
|
|
316
|
+
const usage = cursorPromptUsage(result?.usage, { providerSessionId: session.providerSessionId });
|
|
317
|
+
if (usage) push(usage);
|
|
318
|
+
const stopReason = session.cancelRequested ? 'cancelled' : cursorStopReason(result?.stopReason);
|
|
319
|
+
push(done({ providerSessionId: session.providerSessionId, stopReason }));
|
|
320
|
+
}).catch((err) => {
|
|
321
|
+
if (session.cancelRequested) {
|
|
322
|
+
push(done({ providerSessionId: session.providerSessionId, stopReason: 'cancelled' }));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
push(errorEvent(err.message, { providerSessionId: session.providerSessionId }));
|
|
326
|
+
push(done({ providerSessionId: session.providerSessionId, stopReason: 'error' }));
|
|
327
|
+
});
|
|
328
|
+
try {
|
|
329
|
+
while (!complete || queue.length) {
|
|
330
|
+
if (!queue.length) await new Promise((resolve) => { wake = resolve; });
|
|
331
|
+
else yield queue.shift();
|
|
332
|
+
}
|
|
333
|
+
} finally {
|
|
334
|
+
off();
|
|
335
|
+
session.activePrompt = null;
|
|
336
|
+
session.cancelRequested = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const __private = {
|
|
342
|
+
matchAcpModel,
|
|
343
|
+
pickPermissionOption,
|
|
344
|
+
resolveBinary,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
export { CursorAdapter, __private };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter input capabilities are declarations, not guesses. The prepared
|
|
3
|
+
* contract is rejected before launch when a selected input would otherwise be
|
|
4
|
+
* ignored by the native agent.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class UnsupportedExecutionInputError extends Error {
|
|
8
|
+
constructor(adapterId, field, detail) {
|
|
9
|
+
super(`${adapterId} cannot apply ${field}: ${detail}`);
|
|
10
|
+
this.name = 'UnsupportedExecutionInputError';
|
|
11
|
+
this.code = 'UNSUPPORTED_EXECUTION_INPUT';
|
|
12
|
+
this.adapterId = adapterId;
|
|
13
|
+
this.field = field;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const COMMON_REASONING_SETTINGS = Object.freeze([
|
|
18
|
+
'thought_level',
|
|
19
|
+
'reasoning_effort',
|
|
20
|
+
'reasoning',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export const COMMON_SPEED_SETTINGS = Object.freeze(['speed', 'fast']);
|
|
24
|
+
|
|
25
|
+
export function nativeInputCapabilities(input = {}) {
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
authModes: Object.freeze([...(input.authModes || [])]),
|
|
28
|
+
modelSettings: input.modelSettings === 'negotiated'
|
|
29
|
+
? 'negotiated'
|
|
30
|
+
: Object.freeze([...(input.modelSettings || [])]),
|
|
31
|
+
tools: input.tools === true,
|
|
32
|
+
permissionModes: Object.freeze([...(input.permissionModes || ['ask', 'full_access'])]),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function assertExecutionInput(adapterId, contract, capabilities) {
|
|
37
|
+
if (!capabilities) {
|
|
38
|
+
throw new UnsupportedExecutionInputError(adapterId, 'execution', 'adapter did not declare input capabilities');
|
|
39
|
+
}
|
|
40
|
+
if (!capabilities.authModes.includes(contract.auth.mode)) {
|
|
41
|
+
throw new UnsupportedExecutionInputError(adapterId, 'auth.mode', contract.auth.mode);
|
|
42
|
+
}
|
|
43
|
+
if (!capabilities.permissionModes.includes(contract.permissionMode)) {
|
|
44
|
+
throw new UnsupportedExecutionInputError(adapterId, 'permissionMode', contract.permissionMode);
|
|
45
|
+
}
|
|
46
|
+
if (contract.tools.toolIds.length > 0 && !capabilities.tools) {
|
|
47
|
+
throw new UnsupportedExecutionInputError(adapterId, 'tools', 'selected ordinary/MCP tools are not supported');
|
|
48
|
+
}
|
|
49
|
+
if (capabilities.modelSettings !== 'negotiated') {
|
|
50
|
+
const supported = new Set(capabilities.modelSettings);
|
|
51
|
+
const unsupported = contract.model.settings.find((setting) => !supported.has(setting.configId));
|
|
52
|
+
if (unsupported) {
|
|
53
|
+
throw new UnsupportedExecutionInputError(
|
|
54
|
+
adapterId,
|
|
55
|
+
`model.settings.${unsupported.configId}`,
|
|
56
|
+
'the native adapter has no exact mapping',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const invalidReasoning = contract.model.settings.find(
|
|
60
|
+
(setting) => COMMON_REASONING_SETTINGS.includes(setting.configId)
|
|
61
|
+
&& typeof setting.value !== 'string',
|
|
62
|
+
);
|
|
63
|
+
if (invalidReasoning) {
|
|
64
|
+
throw new UnsupportedExecutionInputError(
|
|
65
|
+
adapterId,
|
|
66
|
+
`model.settings.${invalidReasoning.configId}`,
|
|
67
|
+
'the native adapter requires a named reasoning level',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const invalidSpeed = contract.model.settings.find(
|
|
71
|
+
(setting) => COMMON_SPEED_SETTINGS.includes(setting.configId)
|
|
72
|
+
&& ![
|
|
73
|
+
true,
|
|
74
|
+
false,
|
|
75
|
+
'fast',
|
|
76
|
+
'standard',
|
|
77
|
+
'normal',
|
|
78
|
+
'default',
|
|
79
|
+
].includes(setting.value),
|
|
80
|
+
);
|
|
81
|
+
if (invalidSpeed) {
|
|
82
|
+
throw new UnsupportedExecutionInputError(
|
|
83
|
+
adapterId,
|
|
84
|
+
`model.settings.${invalidSpeed.configId}`,
|
|
85
|
+
`unsupported speed value ${JSON.stringify(invalidSpeed.value)}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|