amalgm 0.1.135 → 0.1.136

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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/runtime/lib/harnesses.js +61 -162
  3. package/runtime/scripts/amalgm-mcp/agent-bundles/app-entries.js +124 -0
  4. package/runtime/scripts/amalgm-mcp/agent-bundles/app-files.js +160 -0
  5. package/runtime/scripts/amalgm-mcp/agent-bundles/automation-entries.js +186 -0
  6. package/runtime/scripts/amalgm-mcp/agent-bundles/bundles.js +272 -32
  7. package/runtime/scripts/amalgm-mcp/agent-bundles/rest.js +25 -8
  8. package/runtime/scripts/amalgm-mcp/automations/rest.js +21 -11
  9. package/runtime/scripts/amalgm-mcp/automations/store.js +68 -1
  10. package/runtime/scripts/amalgm-mcp/automations/tools.js +13 -4
  11. package/runtime/scripts/amalgm-mcp/events/desktop-release-runner.js +1 -0
  12. package/runtime/scripts/amalgm-mcp/lib/email-deeplinks.js +117 -0
  13. package/runtime/scripts/amalgm-mcp/lib/email-embeds.js +42 -19
  14. package/runtime/scripts/amalgm-mcp/lib/email-md.js +24 -17
  15. package/runtime/scripts/amalgm-mcp/notify/index.js +6 -2
  16. package/runtime/scripts/amalgm-mcp/state/snapshot.js +3 -3
  17. package/runtime/scripts/amalgm-mcp/tests/agent-bundles.test.js +8 -8
  18. package/runtime/scripts/amalgm-mcp/tests/bundle-entries.test.js +288 -0
  19. package/runtime/scripts/amalgm-mcp/tests/desktop-release-automation-seed.test.js +37 -1
  20. package/runtime/scripts/amalgm-mcp/tests/desktop-release-runner.test.js +22 -0
  21. package/runtime/scripts/amalgm-mcp/tests/desktop-release-server.test.js +15 -5
  22. package/runtime/scripts/amalgm-mcp/tests/email-embeds.test.js +35 -0
  23. package/runtime/scripts/amalgm-mcp/workspace/rest.js +1 -0
  24. package/runtime/scripts/chat-core/adapters/acp-client.js +156 -0
  25. package/runtime/scripts/chat-core/adapters/cursor.js +254 -0
  26. package/runtime/scripts/chat-core/adapters/pi.js +302 -0
  27. package/runtime/scripts/chat-core/auth.js +20 -3
  28. package/runtime/scripts/chat-core/chat-payload.js +2 -0
  29. package/runtime/scripts/chat-core/contract.js +56 -1
  30. package/runtime/scripts/chat-core/normalizers/cursor.js +174 -0
  31. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +38 -0
  32. package/runtime/scripts/chat-core/normalizers/pi.js +143 -0
  33. package/runtime/scripts/chat-core/server.js +4 -0
  34. package/runtime/scripts/chat-core/tests/cursor.test.js +178 -0
  35. package/runtime/scripts/chat-core/tests/pi.test.js +182 -0
  36. package/runtime/scripts/chat-core/tooling/native-config.js +38 -0
  37. package/runtime/scripts/chat-core/usage.js +20 -0
@@ -53,6 +53,10 @@ function writeReleaseFiles(lane, version, mtimeOffsetMs = 0) {
53
53
 
54
54
  test('desktop release server resolves stable bridge download targets from metadata', () => {
55
55
  const artifacts = [
56
+ 'amalgm-thin-0.1.1-arm64-mac.zip',
57
+ 'amalgm-thin-0.1.1-arm64-mac.zip.blockmap',
58
+ 'amalgm-thin-0.1.1-arm64.dmg',
59
+ 'amalgm-thin-0.1.1-arm64.dmg.blockmap',
56
60
  'amalgm-0.1.1-arm64-mac.zip',
57
61
  'amalgm-0.1.1-arm64-mac.zip.blockmap',
58
62
  'amalgm-0.1.1-arm64.dmg',
@@ -65,10 +69,10 @@ test('desktop release server resolves stable bridge download targets from metada
65
69
  assert.deepEqual(parseDownloadPath('/main/download/zip'), { lane: 'main', kind: 'zip' });
66
70
  assert.deepEqual(parseDownloadPath('/main/latest.dmg'), { lane: 'main', kind: 'dmg' });
67
71
  assert.equal(parseDownloadPath('/main/latest-mac.yml'), null);
68
- assert.equal(latestArtifactName({ artifacts }, 'dmg'), 'amalgm-0.1.1-arm64.dmg');
72
+ assert.equal(latestArtifactName({ artifacts }, 'dmg'), 'amalgm-thin-0.1.1-arm64.dmg');
69
73
  assert.deepEqual(latestDownloadTarget('main', 'zip'), {
70
74
  metadata: { lane: 'main', artifacts },
71
- artifactName: 'amalgm-0.1.1-arm64-mac.zip',
75
+ artifactName: 'amalgm-thin-0.1.1-arm64-mac.zip',
72
76
  });
73
77
  });
74
78
 
@@ -81,6 +85,10 @@ test('desktop release server groups versioned release artifacts', () => {
81
85
  releaseKeyForArtifact('amalgm-preview-0.1.139882282-preview.1-arm64.dmg'),
82
86
  'amalgm-preview@0.1.139882282-preview.1',
83
87
  );
88
+ assert.equal(
89
+ releaseKeyForArtifact('amalgm-preview-thin-0.1.139882282-preview.1-arm64.dmg'),
90
+ 'amalgm-preview-thin@0.1.139882282-preview.1',
91
+ );
84
92
  assert.equal(releaseKeyForArtifact('latest-mac.yml'), '');
85
93
  });
86
94
 
@@ -113,6 +121,8 @@ test('desktop release server prunes stale lane artifacts after publish', () => {
113
121
 
114
122
  test('desktop release server redirects public bridge download URLs to current assets', async () => {
115
123
  writeReleaseMetadata('preview', [
124
+ 'amalgm-preview-thin-0.1.1-preview.1-arm64.zip',
125
+ 'amalgm-preview-thin-0.1.1-preview.1-arm64.dmg',
116
126
  'amalgm-preview-0.1.1-preview.1-arm64.zip',
117
127
  'amalgm-preview-0.1.1-preview.1-arm64.dmg',
118
128
  'preview-mac.yml',
@@ -127,7 +137,7 @@ test('desktop release server redirects public bridge download URLs to current as
127
137
  assert.equal(response.status, 302);
128
138
  assert.equal(
129
139
  response.headers.get('location'),
130
- `http://127.0.0.1:${port}/preview/amalgm-preview-0.1.1-preview.1-arm64.dmg`,
140
+ `http://127.0.0.1:${port}/preview/amalgm-preview-thin-0.1.1-preview.1-arm64.dmg`,
131
141
  );
132
142
 
133
143
  const zipResponse = await fetch(`http://127.0.0.1:${port}/preview/latest.zip`, {
@@ -137,7 +147,7 @@ test('desktop release server redirects public bridge download URLs to current as
137
147
  assert.equal(zipResponse.status, 302);
138
148
  assert.equal(
139
149
  zipResponse.headers.get('location'),
140
- `http://127.0.0.1:${port}/preview/amalgm-preview-0.1.1-preview.1-arm64.zip`,
150
+ `http://127.0.0.1:${port}/preview/amalgm-preview-thin-0.1.1-preview.1-arm64.zip`,
141
151
  );
142
152
 
143
153
  const forwardedResponse = await fetch(`http://127.0.0.1:${port}/preview/download`, {
@@ -150,7 +160,7 @@ test('desktop release server redirects public bridge download URLs to current as
150
160
  assert.equal(forwardedResponse.status, 302);
151
161
  assert.equal(
152
162
  forwardedResponse.headers.get('location'),
153
- 'https://amalgm-desktop-releases.fly.dev/preview/amalgm-preview-0.1.1-preview.1-arm64.dmg',
163
+ 'https://amalgm-desktop-releases.fly.dev/preview/amalgm-preview-thin-0.1.1-preview.1-arm64.dmg',
154
164
  );
155
165
  } finally {
156
166
  await new Promise((resolve) => server.close(resolve));
@@ -83,6 +83,41 @@ test('full entity embed uses chrome identity only, not centered email-only body
83
83
  assert.doesNotMatch(html, /text-transform:uppercase/);
84
84
  });
85
85
 
86
+ test('attached video cards use the notification deep link when provided', () => {
87
+ const html = renderMediaEmbed(
88
+ 'QA recording',
89
+ 'attached:qa-pass.webm',
90
+ 'cid:poster-1',
91
+ { defaultOpenUrl: 'https://amalgm.ai/session/sess-123' },
92
+ );
93
+
94
+ assert.match(html, /https:\/\/amalgm\.ai\/session\/sess-123/);
95
+ assert.doesNotMatch(html, /href="https:\/\/amalgm\.ai"(?!\/session)/);
96
+ });
97
+
98
+ test('standalone entity embeds derive canonical session routes from tile ids', () => {
99
+ const html = markdownToEmailHtml('[QA run](tile:chat:sess-123)');
100
+
101
+ assert.match(html, /https:\/\/amalgm\.ai\/session\/sess-123/);
102
+ assert.doesNotMatch(html, /href="https:\/\/amalgm\.ai"(?!\/session)/);
103
+ });
104
+
105
+ test('relative app links become absolute email links', () => {
106
+ const html = markdownToEmailHtml('Open [details](/session/sess-123)');
107
+
108
+ assert.match(html, /href="https:\/\/amalgm\.ai\/session\/sess-123"/);
109
+ });
110
+
111
+ test('standalone attached media lines keep the provided fallback deep link', () => {
112
+ const html = markdownToEmailHtml(
113
+ '![QA recording](attached:qa-pass.webm "cid:poster-1")',
114
+ { defaultOpenUrl: 'https://amalgm.ai/apps/app-03n045jhko' },
115
+ );
116
+
117
+ assert.match(html, /https:\/\/amalgm\.ai\/apps\/app-03n045jhko/);
118
+ assert.doesNotMatch(html, /href="https:\/\/amalgm\.ai"(?!\/apps)/);
119
+ });
120
+
86
121
  test('file identity prefers explicit filename over path and url', () => {
87
122
  const identity = resolveFileEmbedIdentity({
88
123
  name: 'renamed.mov',
@@ -626,6 +626,7 @@ async function handleWorktreeDelete(body, sendJson) {
626
626
  }
627
627
 
628
628
  module.exports = {
629
+ getWorkspaceRoot,
629
630
  handleProjects,
630
631
  handleProjectRegister,
631
632
  handleProjectUpdate,
@@ -0,0 +1,156 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('child_process');
4
+
5
+ /**
6
+ * Minimal Agent Client Protocol (ACP) client: JSON-RPC 2.0 over the stdio of a
7
+ * spawned agent process. This is the amalgm *client* side; the CLI (cursor-agent,
8
+ * gemini, ...) is the ACP agent. One instance per native agent process.
9
+ *
10
+ * The reader is tolerant of non-JSON stdout lines because young ACP servers are
11
+ * known to corrupt stdout with log output.
12
+ */
13
+ class AcpClient {
14
+ constructor({ binary, args = [], cwd, env, label = 'acp' }) {
15
+ this.label = label;
16
+ this.id = 1;
17
+ this.pending = new Map();
18
+ this.notificationHandlers = new Set();
19
+ this.requestHandler = null;
20
+ this.buffer = '';
21
+ this.closed = false;
22
+ this.closeError = null;
23
+ this.child = spawn(binary, args, {
24
+ cwd,
25
+ env,
26
+ stdio: ['pipe', 'pipe', 'pipe'],
27
+ detached: process.platform !== 'win32',
28
+ windowsHide: true,
29
+ });
30
+ this.child.stdout.setEncoding('utf8');
31
+ this.child.stdout.on('data', (chunk) => this.onData(chunk));
32
+ this.child.stderr.setEncoding('utf8');
33
+ this.child.stderr.on('data', (chunk) => {
34
+ const text = String(chunk || '').trim();
35
+ if (text && process.env.CHAT_CORE_DEBUG) console.warn(`[${this.label}]`, text.slice(0, 1000));
36
+ });
37
+ this.child.on('error', (err) => this.failAll(err));
38
+ this.child.on('exit', (code, signal) => {
39
+ this.failAll(new Error(`${this.label} agent exited (${code ?? signal ?? 'unknown'})`));
40
+ });
41
+ this.child.stdin.on('error', (err) => this.failAll(err));
42
+ }
43
+
44
+ failAll(err) {
45
+ this.closed = true;
46
+ this.closeError = err;
47
+ for (const pending of this.pending.values()) pending.reject(err);
48
+ this.pending.clear();
49
+ }
50
+
51
+ onData(chunk) {
52
+ this.buffer += chunk;
53
+ const lines = this.buffer.split('\n');
54
+ this.buffer = lines.pop() || '';
55
+ for (const line of lines) {
56
+ if (!line.trim()) continue;
57
+ let msg;
58
+ try { msg = JSON.parse(line); } catch { continue; }
59
+ if (process.env.CHAT_CORE_DEBUG_ACP === '1') {
60
+ console.log(`[${this.label}:recv]`, JSON.stringify(msg).slice(0, 4000));
61
+ }
62
+ this.dispatch(msg);
63
+ }
64
+ }
65
+
66
+ dispatch(msg) {
67
+ if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
68
+ const pending = this.pending.get(msg.id);
69
+ if (!pending) return;
70
+ this.pending.delete(msg.id);
71
+ if (msg.error) {
72
+ const err = new Error(msg.error.data?.message || msg.error.message || JSON.stringify(msg.error));
73
+ err.code = msg.error.code;
74
+ pending.reject(err);
75
+ } else {
76
+ pending.resolve(msg.result);
77
+ }
78
+ return;
79
+ }
80
+ if (msg.id !== undefined && msg.method) {
81
+ this.handleServerRequest(msg);
82
+ return;
83
+ }
84
+ if (msg.method) {
85
+ for (const handler of this.notificationHandlers) handler(msg);
86
+ }
87
+ }
88
+
89
+ async handleServerRequest(msg) {
90
+ if (!this.requestHandler) {
91
+ this.write({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: `Method not supported: ${msg.method}` } });
92
+ return;
93
+ }
94
+ try {
95
+ const result = await this.requestHandler(msg.method, msg.params || {});
96
+ this.write({ jsonrpc: '2.0', id: msg.id, result: result ?? {} });
97
+ } catch (err) {
98
+ this.write({ jsonrpc: '2.0', id: msg.id, error: { code: -32603, message: err.message || 'Internal error' } });
99
+ }
100
+ }
101
+
102
+ write(payload) {
103
+ if (this.closed || this.child.exitCode !== null || this.child.signalCode !== null) return;
104
+ try {
105
+ this.child.stdin.write(`${JSON.stringify(payload)}\n`, (err) => {
106
+ if (err) this.failAll(err);
107
+ });
108
+ } catch (err) {
109
+ this.failAll(err);
110
+ }
111
+ }
112
+
113
+ request(method, params = {}, timeoutMs = 180000) {
114
+ if (this.closeError) return Promise.reject(this.closeError);
115
+ if (this.closed || this.child.exitCode !== null || this.child.signalCode !== null) {
116
+ return Promise.reject(new Error(`${this.label} agent is not running`));
117
+ }
118
+ const id = this.id++;
119
+ return new Promise((resolve, reject) => {
120
+ const timer = timeoutMs > 0
121
+ ? setTimeout(() => {
122
+ this.pending.delete(id);
123
+ reject(new Error(`${method} timed out`));
124
+ }, timeoutMs)
125
+ : null;
126
+ this.pending.set(id, {
127
+ resolve: (value) => { if (timer) clearTimeout(timer); resolve(value); },
128
+ reject: (err) => { if (timer) clearTimeout(timer); reject(err); },
129
+ });
130
+ this.write({ jsonrpc: '2.0', id, method, params });
131
+ });
132
+ }
133
+
134
+ notify(method, params) {
135
+ this.write(params === undefined ? { jsonrpc: '2.0', method } : { jsonrpc: '2.0', method, params });
136
+ }
137
+
138
+ onNotification(handler) {
139
+ this.notificationHandlers.add(handler);
140
+ return () => this.notificationHandlers.delete(handler);
141
+ }
142
+
143
+ onRequest(handler) {
144
+ this.requestHandler = handler;
145
+ }
146
+
147
+ close() {
148
+ try { this.child.stdin.end(); } catch {}
149
+ try {
150
+ if (process.platform !== 'win32' && this.child.pid) process.kill(-this.child.pid, 'SIGTERM');
151
+ else this.child.kill('SIGTERM');
152
+ } catch {}
153
+ }
154
+ }
155
+
156
+ module.exports = { AcpClient };
@@ -0,0 +1,254 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const { done, errorEvent, warningEvent } = require('../events');
7
+ const { promptText } = require('../input');
8
+ const { cursorPromptUsage, cursorStopReason, normalizeCursorUpdate } = require('../normalizers/cursor');
9
+ const { recordNativeEvent } = require('../recorder');
10
+ const { relayedMcpServers } = require('../tooling/mcp-bundle');
11
+ const { executableExists, findOnPath } = require('../tooling/native-binaries');
12
+ const { prepareHarnessRuntime } = require('../tooling/runtime-home');
13
+ const { composeSystemPrompt } = require('../tooling/system-prompt');
14
+ const { AcpClient } = require('./acp-client');
15
+
16
+ const PROTOCOL_VERSION = 1;
17
+
18
+ function resolveBinary() {
19
+ const binaryName = process.platform === 'win32' ? 'cursor-agent.exe' : 'cursor-agent';
20
+ const homes = [...new Set([process.env.AMALGM_NATIVE_HOME, os.homedir()].filter(Boolean))];
21
+ const candidates = [
22
+ process.env.CURSOR_AGENT_BINARY,
23
+ ...homes.map((home) => path.join(home, '.local', 'bin', binaryName)),
24
+ '/opt/homebrew/bin/cursor-agent',
25
+ '/usr/local/bin/cursor-agent',
26
+ findOnPath('cursor-agent'),
27
+ ].filter(Boolean);
28
+ const failures = [];
29
+ for (const candidate of candidates) {
30
+ if (!executableExists(candidate)) {
31
+ failures.push(`${candidate} (missing or not executable)`);
32
+ continue;
33
+ }
34
+ const res = spawnSync(candidate, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
35
+ if (res.status === 0) return candidate;
36
+ failures.push(`${candidate} (--version failed: ${res.error?.message || res.status || res.signal || 'unknown'})`);
37
+ }
38
+ 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.`);
39
+ }
40
+
41
+ // cursor-agent's session/set_model only accepts EXACT ids from availableModels
42
+ // (base name + bracket parameters, e.g. "gpt-5.3-codex[reasoning=medium,fast=false]").
43
+ // Amalgm cliModels are bare base names, so match by base and return the exact id.
44
+ function matchAcpModel(availableModels, requested) {
45
+ const models = Array.isArray(availableModels) ? availableModels : [];
46
+ const baseOf = (id) => String(id || '').split('[')[0].trim();
47
+ let clean = String(requested || '').trim().replace(/^cursor\//, '').split('[')[0];
48
+ if (!clean) return null;
49
+ if (clean === 'auto' || clean === 'default') clean = 'default';
50
+ const findBase = (base) => models.find((m) => baseOf(m.modelId) === base)
51
+ || models.find((m) => String(m.name || '').toLowerCase() === base.toLowerCase());
52
+ let candidate = clean;
53
+ // Legacy amalgm cursor cliModels carry effort/speed suffixes ("gpt-5.3-codex-high",
54
+ // "composer-2.5-fast") that ACP model ids do not; strip them until a base matches.
55
+ for (let i = 0; i < 6; i += 1) {
56
+ const match = findBase(candidate);
57
+ if (match) return match;
58
+ const stripped = candidate.replace(/-(?:fast|thinking|low|medium|high|xhigh|max)$/, '');
59
+ if (stripped === candidate) return null;
60
+ candidate = stripped;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function toAcpMcpServers(contract) {
66
+ return relayedMcpServers(contract).map((server) => ({
67
+ type: server.type === 'sse' ? 'sse' : 'http',
68
+ name: server.name,
69
+ url: server.url,
70
+ headers: (server.headers || []).map((header) => ({ name: header.name, value: header.value })),
71
+ }));
72
+ }
73
+
74
+ function pickPermissionOption(options = []) {
75
+ const byKind = (kind) => options.find((option) => option?.kind === kind);
76
+ return byKind('allow_always') || byKind('allow_once') || options[0] || null;
77
+ }
78
+
79
+ class CursorAdapter {
80
+ async create(contract) {
81
+ const runtime = prepareHarnessRuntime(contract);
82
+ const binary = resolveBinary();
83
+ const client = new AcpClient({
84
+ binary,
85
+ args: ['acp'],
86
+ cwd: contract.cwd,
87
+ env: runtime.env,
88
+ label: 'Cursor',
89
+ });
90
+ const session = {
91
+ sessionId: contract.sessionId,
92
+ providerSessionId: contract.providerSessionId || null,
93
+ client,
94
+ needsSystemPrompt: false,
95
+ modelWarning: null,
96
+ activePrompt: null,
97
+ cancelRequested: false,
98
+ };
99
+ client.onRequest((method, params) => this.handleAgentRequest(session, method, params));
100
+ await client.request('initialize', {
101
+ protocolVersion: PROTOCOL_VERSION,
102
+ clientInfo: { name: 'amalgm-chat-core', version: '1.0.0' },
103
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
104
+ });
105
+ const sessionParams = { cwd: contract.cwd, mcpServers: toAcpMcpServers(contract) };
106
+ let acpSession = null;
107
+ if (session.providerSessionId) {
108
+ // session/load replays history as session/update notifications; nothing is
109
+ // subscribed yet, so the replay is discarded and the turn starts clean.
110
+ acpSession = await client.request('session/load', {
111
+ sessionId: session.providerSessionId,
112
+ ...sessionParams,
113
+ }, 600000).catch((err) => {
114
+ console.warn('[Cursor] Failed to load native session; starting a new one:', err.message);
115
+ return null;
116
+ });
117
+ }
118
+ if (!acpSession) {
119
+ acpSession = await client.request('session/new', sessionParams);
120
+ session.providerSessionId = acpSession.sessionId;
121
+ session.needsSystemPrompt = true;
122
+ }
123
+ await this.selectModel(session, acpSession, contract);
124
+ return session;
125
+ }
126
+
127
+ async selectModel(session, acpSession, contract) {
128
+ const requested = contract.cliModel || contract.modelId;
129
+ const models = acpSession?.models || {};
130
+ const match = matchAcpModel(models.availableModels, requested);
131
+ if (!match) {
132
+ session.modelWarning = `Cursor Agent does not offer "${requested}"; continuing with ${models.currentModelId || 'its default model'}`;
133
+ return;
134
+ }
135
+ if (match.modelId === models.currentModelId) return;
136
+ try {
137
+ await session.client.request('session/set_model', {
138
+ sessionId: session.providerSessionId,
139
+ modelId: match.modelId,
140
+ }, 30000);
141
+ } catch (err) {
142
+ session.modelWarning = `Cursor Agent rejected model "${match.modelId}": ${err.message}`;
143
+ }
144
+ }
145
+
146
+ async handleAgentRequest(session, method, params) {
147
+ recordNativeEvent('cursor.acp.request', { method, params }, {
148
+ providerSessionId: session.providerSessionId,
149
+ sessionId: session.sessionId,
150
+ });
151
+ if (method === 'session/request_permission') {
152
+ if (session.cancelRequested) return { outcome: { outcome: 'cancelled' } };
153
+ const pick = pickPermissionOption(params.options);
154
+ if (!pick) return { outcome: { outcome: 'cancelled' } };
155
+ return { outcome: { outcome: 'selected', optionId: pick.optionId } };
156
+ }
157
+ throw new Error(`Method not supported: ${method}`);
158
+ }
159
+
160
+ async resume(session, contract) {
161
+ if (session.client && !session.client.closed) return session;
162
+ return this.create({ ...contract, providerSessionId: session.providerSessionId || contract.providerSessionId });
163
+ }
164
+
165
+ async stop(session) {
166
+ session.cancelRequested = true;
167
+ if (!session.activePrompt) return;
168
+ session.client.notify('session/cancel', { sessionId: session.providerSessionId });
169
+ }
170
+
171
+ async destroy(session) {
172
+ session.client.close();
173
+ }
174
+
175
+ async *prompt(session, input, contract) {
176
+ const queue = [];
177
+ let complete = false;
178
+ let wake = null;
179
+ const wakeLoop = () => { if (wake) { wake(); wake = null; } };
180
+ const push = (e) => {
181
+ if (e.type === 'done' || e.type === 'error') complete = true;
182
+ queue.push(e);
183
+ wakeLoop();
184
+ };
185
+ const state = { providerSessionId: session.providerSessionId, tools: new Map() };
186
+ session.cancelRequested = false;
187
+ session.activePrompt = state;
188
+ if (session.modelWarning) {
189
+ push(warningEvent(session.modelWarning, { providerSessionId: session.providerSessionId }));
190
+ session.modelWarning = null;
191
+ }
192
+ const off = session.client.onNotification((msg) => {
193
+ if (msg.method !== 'session/update') return;
194
+ const params = msg.params || {};
195
+ if (params.sessionId && params.sessionId !== session.providerSessionId) return;
196
+ recordNativeEvent('cursor.acp.update', msg, {
197
+ providerSessionId: session.providerSessionId,
198
+ sessionId: contract.sessionId,
199
+ assistantMessageId: contract.assistantMessageId,
200
+ updateKind: params.update?.sessionUpdate,
201
+ });
202
+ for (const e of normalizeCursorUpdate(params.update || {}, state)) push(e);
203
+ });
204
+ let text = promptText(input, contract);
205
+ if (session.needsSystemPrompt) {
206
+ // ACP has no system-prompt slot; a fresh native session gets the composed
207
+ // instructions as a preamble on its first prompt only.
208
+ const systemPrompt = composeSystemPrompt(contract);
209
+ if (systemPrompt) text = `<system-instructions>\n${systemPrompt}\n</system-instructions>\n\n${text}`;
210
+ session.needsSystemPrompt = false;
211
+ }
212
+ session.client.request('session/prompt', {
213
+ sessionId: session.providerSessionId,
214
+ prompt: [{ type: 'text', text }],
215
+ }, 0).then((result) => {
216
+ recordNativeEvent('cursor.acp.prompt_result', result, {
217
+ providerSessionId: session.providerSessionId,
218
+ sessionId: contract.sessionId,
219
+ assistantMessageId: contract.assistantMessageId,
220
+ });
221
+ const usage = cursorPromptUsage(result?.usage, { providerSessionId: session.providerSessionId });
222
+ if (usage) push(usage);
223
+ const stopReason = session.cancelRequested ? 'cancelled' : cursorStopReason(result?.stopReason);
224
+ push(done({ providerSessionId: session.providerSessionId, stopReason }));
225
+ }).catch((err) => {
226
+ if (session.cancelRequested) {
227
+ push(done({ providerSessionId: session.providerSessionId, stopReason: 'cancelled' }));
228
+ return;
229
+ }
230
+ push(errorEvent(err.message, { providerSessionId: session.providerSessionId }));
231
+ push(done({ providerSessionId: session.providerSessionId, stopReason: 'error' }));
232
+ });
233
+ try {
234
+ while (!complete || queue.length) {
235
+ if (!queue.length) await new Promise((resolve) => { wake = resolve; });
236
+ else yield queue.shift();
237
+ }
238
+ } finally {
239
+ off();
240
+ session.activePrompt = null;
241
+ session.cancelRequested = false;
242
+ }
243
+ }
244
+ }
245
+
246
+ module.exports = {
247
+ CursorAdapter,
248
+ __private: {
249
+ matchAcpModel,
250
+ pickPermissionOption,
251
+ resolveBinary,
252
+ toAcpMcpServers,
253
+ },
254
+ };