@openagents-org/agent-launcher 0.2.193 → 0.2.194
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/README.md +32 -2
- package/package.json +1 -1
- package/registry.json +9 -3
- package/src/adapters/antigravity-stream.js +65 -5
- package/src/adapters/base.js +277 -9
- package/src/adapters/claude.js +152 -21
- package/src/adapters/codex.js +37 -3
- package/src/adapters/cursor-node-entry.js +64 -0
- package/src/adapters/cursor.js +31 -1
- package/src/adapters/hermes.js +354 -58
- package/src/adapters/opencode.js +25 -3
- package/src/adapters/response-artifacts.js +72 -0
- package/src/adapters/thread-history.js +49 -0
- package/src/adapters/workspace-prompt.js +102 -12
- package/src/cli-models.js +244 -0
- package/src/cli.js +69 -13
- package/src/config.js +1 -0
- package/src/daemon.js +153 -15
- package/src/index.js +10 -1
- package/src/installer.js +413 -16
- package/src/mcp-server.js +138 -9
- package/src/node-config.js +6 -4
- package/src/postinstall-stub.js +69 -0
- package/src/tools-v11/brief.js +97 -0
- package/src/tools-v11/index.js +28 -0
- package/src/tools-v11/presence-handoff.js +199 -0
- package/src/workspace-client.js +230 -18
package/src/adapters/claude.js
CHANGED
|
@@ -22,7 +22,7 @@ const { spawn } = require('../wsl');
|
|
|
22
22
|
|
|
23
23
|
const BaseAdapter = require('./base');
|
|
24
24
|
const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle, redactSecrets } = require('./utils');
|
|
25
|
-
const { buildClaudeSystemPrompt, buildClaudeSkillMd, workspaceSkillName } = require('./workspace-prompt');
|
|
25
|
+
const { buildClaudeSystemPrompt, buildClaudeSkillMd, buildPinnedSections, workspaceSkillName } = require('./workspace-prompt');
|
|
26
26
|
const { pinnedFingerprint, sampleRecap } = require('./decision-log');
|
|
27
27
|
const { defaultAgentWorkdir, whichBinary, whereBinary } = require('../paths');
|
|
28
28
|
|
|
@@ -657,7 +657,12 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
657
657
|
type: 'stdio',
|
|
658
658
|
command: mcpCommand,
|
|
659
659
|
args: mcpFinalArgs,
|
|
660
|
-
env: {
|
|
660
|
+
env: {
|
|
661
|
+
OA_WORKSPACE_TOKEN: this.token,
|
|
662
|
+
// v1.1: who asked, so escalation tools can address the owner.
|
|
663
|
+
...(this.requesterEmailFor && this.requesterEmailFor(channelName)
|
|
664
|
+
? { OPENAGENTS_REQUESTER_EMAIL: this.requesterEmailFor(channelName) } : {}),
|
|
665
|
+
},
|
|
661
666
|
},
|
|
662
667
|
},
|
|
663
668
|
};
|
|
@@ -822,6 +827,11 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
822
827
|
watchdogTimer: null,
|
|
823
828
|
awaitingToolResult: false,
|
|
824
829
|
userStopped: false,
|
|
830
|
+
// Background tasks (run_in_background Bash, background sub-agents) the
|
|
831
|
+
// CLI reports as still running, from its `background_tasks_changed`
|
|
832
|
+
// events. Killing the process kills them too, and a plan nudge sent
|
|
833
|
+
// while they run makes the agent re-announce work it is waiting on.
|
|
834
|
+
backgroundTasks: 0,
|
|
825
835
|
};
|
|
826
836
|
|
|
827
837
|
if (proc.stderr) {
|
|
@@ -928,10 +938,19 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
928
938
|
try { await this.sendResponse(pp.msgChannel, trailing); } catch {}
|
|
929
939
|
}
|
|
930
940
|
pp.lastResponseText.length = 0;
|
|
941
|
+
// The plan nudge was held back while background work ran (see
|
|
942
|
+
// _queueTodoNudge); once the last task has reported, pick it up.
|
|
943
|
+
if (!event.is_error && pp.backgroundTasks === 0) {
|
|
944
|
+
await this._resumePlanAfterBackground(pp.msgChannel);
|
|
945
|
+
}
|
|
931
946
|
}
|
|
932
947
|
} else if (eventType === 'system') {
|
|
933
948
|
const subtype = event.subtype || '';
|
|
934
949
|
const message = event.message || '';
|
|
950
|
+
if (subtype === 'background_tasks_changed') {
|
|
951
|
+
pp.backgroundTasks = Array.isArray(event.tasks) ? event.tasks.length : 0;
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
935
954
|
if (subtype.includes('compact') || String(message).toLowerCase().includes('compact')) {
|
|
936
955
|
await this.sendStatus(pp.msgChannel, String(message) || 'Compacting conversation...');
|
|
937
956
|
}
|
|
@@ -1122,22 +1141,109 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1122
1141
|
/** Queue a reminder about unfinished todos (skipped when this turn IS one). */
|
|
1123
1142
|
async _queueTodoNudge(msgChannel, msg) {
|
|
1124
1143
|
if (msg._todoNudge) return;
|
|
1144
|
+
const nudge = await this._buildTodoNudge(msgChannel);
|
|
1145
|
+
if (!nudge) return;
|
|
1146
|
+
if (!this._channelQueues[msgChannel]) this._channelQueues[msgChannel] = [];
|
|
1147
|
+
if (this._channelQueues[msgChannel].some((m) => m._todoNudge)) return;
|
|
1148
|
+
this._channelQueues[msgChannel].push(nudge);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* The "you have N remaining tasks" message, or null when there is nothing
|
|
1153
|
+
* to nudge: no unfinished todos, a stop in progress, or background work
|
|
1154
|
+
* still running in this channel's process (the agent has said it is waiting
|
|
1155
|
+
* on it; nudging made it re-announce the plan — and, combined with a
|
|
1156
|
+
* restart, re-launch work that had been killed). The nudge is picked up
|
|
1157
|
+
* again once the background tasks report (_resumePlanAfterBackground).
|
|
1158
|
+
*/
|
|
1159
|
+
async _buildTodoNudge(msgChannel) {
|
|
1125
1160
|
// A stop ends the plan, it does not pause it. When the stop lands while
|
|
1126
1161
|
// this turn is finishing, nudging here is what handed the agent its own
|
|
1127
1162
|
// "please continue" right after it announced it had stopped.
|
|
1128
|
-
if (this._stoppingChannels.has(msgChannel)) return;
|
|
1163
|
+
if (this._stoppingChannels.has(msgChannel)) return null;
|
|
1164
|
+
const pp = this._persistentProcs[msgChannel];
|
|
1165
|
+
if (pp && pp.alive && pp.backgroundTasks > 0) {
|
|
1166
|
+
this._log(`Holding the plan nudge for ${msgChannel} — ${pp.backgroundTasks} background task(s) still running`);
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1129
1169
|
try {
|
|
1130
1170
|
const remaining = await this.getRemainingTodos(msgChannel);
|
|
1131
|
-
if (remaining.length
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}
|
|
1171
|
+
if (remaining.length === 0) return null;
|
|
1172
|
+
const items = remaining.map((t) => `- ${t.content}`).join('\n');
|
|
1173
|
+
return {
|
|
1174
|
+
content: `You have ${remaining.length} remaining task(s) from your plan:\n${items}\n\nPlease continue working on them.`,
|
|
1175
|
+
senderType: 'system', senderName: 'system:todos',
|
|
1176
|
+
sessionId: msgChannel, messageType: 'chat', _todoNudge: true,
|
|
1177
|
+
};
|
|
1178
|
+
} catch {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Background work finished and the CLI wrapped it up on its own. If the
|
|
1185
|
+
* plan still has open items, continue it now — queued behind a running
|
|
1186
|
+
* turn, or as its own turn when the channel is idle.
|
|
1187
|
+
*/
|
|
1188
|
+
async _resumePlanAfterBackground(msgChannel) {
|
|
1189
|
+
const nudge = await this._buildTodoNudge(msgChannel);
|
|
1190
|
+
if (!nudge) return;
|
|
1191
|
+
if (this._channelBusy.has(msgChannel)) {
|
|
1192
|
+
if (!this._channelQueues[msgChannel]) this._channelQueues[msgChannel] = [];
|
|
1193
|
+
if (!this._channelQueues[msgChannel].some((m) => m._todoNudge)) this._channelQueues[msgChannel].push(nudge);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
this._channelWorker(msgChannel, nudge);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* The pinned decision log / glossary, rendered exactly as in the system
|
|
1201
|
+
* prompt, framed as replacing the copy the running session started with.
|
|
1202
|
+
* Returns '' when nothing is pinned.
|
|
1203
|
+
*/
|
|
1204
|
+
_pinnedUpdatePreamble(msgChannel, decisionLog, glossary) {
|
|
1205
|
+
const sections = buildPinnedSections({
|
|
1206
|
+
toolMode: this.toolMode,
|
|
1207
|
+
channelName: msgChannel,
|
|
1208
|
+
mode: this._mode,
|
|
1209
|
+
decisionLog,
|
|
1210
|
+
glossary,
|
|
1211
|
+
}).join('').trim();
|
|
1212
|
+
if (!sections) return '';
|
|
1213
|
+
return '[Pinned knowledge was updated since this session started. The ' +
|
|
1214
|
+
'sections below replace the earlier versions in your instructions — ' +
|
|
1215
|
+
'treat them as current.]\n\n' + sections;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Wait for the next `result` on an already-addressed process without
|
|
1220
|
+
* writing anything to it. If the process stays completely silent for
|
|
1221
|
+
* `quietMs` there is no second turn coming; resolve with { quiet: true } so
|
|
1222
|
+
* the caller falls back to the ordinary empty-outcome handling. Once output
|
|
1223
|
+
* starts, the regular watchdog takes over.
|
|
1224
|
+
*/
|
|
1225
|
+
_awaitNextTurn(pp, quietMs = 20_000) {
|
|
1226
|
+
const startedAt = Date.now();
|
|
1227
|
+
return new Promise((resolve) => {
|
|
1228
|
+
let quietTimer = null;
|
|
1229
|
+
const finish = (r) => {
|
|
1230
|
+
if (quietTimer) { clearInterval(quietTimer); quietTimer = null; }
|
|
1231
|
+
this._stopWatchdog(pp);
|
|
1232
|
+
resolve(r);
|
|
1233
|
+
};
|
|
1234
|
+
pp.messageResolve = finish;
|
|
1235
|
+
quietTimer = setInterval(() => {
|
|
1236
|
+
if (pp.lastStdoutTime > startedAt) {
|
|
1237
|
+
clearInterval(quietTimer);
|
|
1238
|
+
quietTimer = null;
|
|
1239
|
+
this._startWatchdog(pp);
|
|
1240
|
+
} else if (Date.now() - startedAt >= quietMs) {
|
|
1241
|
+
pp.messageResolve = null;
|
|
1242
|
+
finish({ quiet: true });
|
|
1243
|
+
}
|
|
1244
|
+
}, 500);
|
|
1245
|
+
if (quietTimer.unref) quietTimer.unref();
|
|
1246
|
+
});
|
|
1141
1247
|
}
|
|
1142
1248
|
|
|
1143
1249
|
async _handleMessage(msg) {
|
|
@@ -1217,22 +1323,34 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1217
1323
|
const modeStale = existingPP.spawnMode !== this._mode;
|
|
1218
1324
|
const pinnedStale = pinnedHash !== null && existingPP.pinnedHash !== pinnedHash;
|
|
1219
1325
|
const modelStale = (existingPP.spawnModel || null) !== (this.workspaceModel || null);
|
|
1220
|
-
if (modeStale ||
|
|
1326
|
+
if (modeStale || modelStale) {
|
|
1221
1327
|
// Kill it and fall through to a fresh spawn: --resume keeps the
|
|
1222
1328
|
// conversation (it lives in the CLI transcript), while the new spawn
|
|
1223
|
-
// carries the current mode and
|
|
1329
|
+
// carries the current mode / model flags (and the current pins).
|
|
1224
1330
|
this._log(modeStale
|
|
1225
1331
|
? `Mode changed to ${this._mode} for ${msgChannel} — respawning with resume`
|
|
1226
|
-
:
|
|
1227
|
-
? `Model changed to ${this.workspaceModel || '(default)'} for ${msgChannel} — respawning with resume`
|
|
1228
|
-
: `Pinned knowledge changed for ${msgChannel} — respawning with resume to re-pin it`);
|
|
1332
|
+
: `Model changed to ${this.workspaceModel || '(default)'} for ${msgChannel} — respawning with resume`);
|
|
1229
1333
|
await this._killPersistentProc(msgChannel);
|
|
1230
1334
|
} else {
|
|
1231
|
-
|
|
1335
|
+
// A pinned-knowledge change no longer restarts the process. Agents
|
|
1336
|
+
// edit the decision log themselves on nearly every decision, and the
|
|
1337
|
+
// restart killed whatever they had running in the background (a
|
|
1338
|
+
// sub-agent mid-build) and replayed into a false "agent hit an error".
|
|
1339
|
+
// The new version rides in with this message instead and supersedes
|
|
1340
|
+
// the copy baked into the system prompt.
|
|
1341
|
+
let turnContent = content;
|
|
1342
|
+
if (pinnedStale) {
|
|
1343
|
+
const update = this._pinnedUpdatePreamble(msgChannel, decisionLogOpt, glossaryOpt);
|
|
1344
|
+
if (update) turnContent = `${update}\n\n---\n\n${content}`;
|
|
1345
|
+
existingPP.pinnedHash = pinnedHash;
|
|
1346
|
+
this._log(`Pinned knowledge changed for ${msgChannel} — passing the update to the running session`);
|
|
1347
|
+
} else {
|
|
1348
|
+
this._log(`Reusing persistent process for ${msgChannel}`);
|
|
1349
|
+
}
|
|
1232
1350
|
if (await this._bailOnStopDuringTurn(msgChannel)) return;
|
|
1233
1351
|
this._resetIdleTimer(msgChannel);
|
|
1234
1352
|
existingPP.msgChannel = msgChannel;
|
|
1235
|
-
const result = await this._sendToPersistentProc(existingPP,
|
|
1353
|
+
const result = await this._sendToPersistentProc(existingPP, turnContent);
|
|
1236
1354
|
if (existingPP.userStopped) {
|
|
1237
1355
|
await this._finishUserStop(msgChannel);
|
|
1238
1356
|
return;
|
|
@@ -1314,9 +1432,22 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
1314
1432
|
]);
|
|
1315
1433
|
pp.spawnMode = this._mode;
|
|
1316
1434
|
pp.spawnModel = this.workspaceModel || null;
|
|
1435
|
+
const resumed = attempt === 0 && !!this._channelSessions[msgChannel];
|
|
1317
1436
|
this._log(`Spawned persistent process for ${msgChannel} (attempt ${attempt + 1})`);
|
|
1318
1437
|
|
|
1319
|
-
|
|
1438
|
+
let result = await this._sendToPersistentProc(pp, effectiveContent);
|
|
1439
|
+
|
|
1440
|
+
// A resumed session can first report something left over from before
|
|
1441
|
+
// the restart — typically a background task that died with the old
|
|
1442
|
+
// process — as a turn with no text and no error. That is not this
|
|
1443
|
+
// message's answer: wait for the real turn instead of posting "the
|
|
1444
|
+
// agent hit an error" and throwing the session away.
|
|
1445
|
+
if (resumed && result.resultEvent && !pp.userStopped
|
|
1446
|
+
&& !pp.everPostedAnything && !pp.lastErrorText
|
|
1447
|
+
&& !this._composeFinalResponse(pp)) {
|
|
1448
|
+
this._log(`Empty leftover result after resume for ${msgChannel} — waiting for the real turn`);
|
|
1449
|
+
result = await this._awaitNextTurn(pp);
|
|
1450
|
+
}
|
|
1320
1451
|
|
|
1321
1452
|
if (pp.userStopped) {
|
|
1322
1453
|
await this._finishUserStop(msgChannel);
|
package/src/adapters/codex.js
CHANGED
|
@@ -79,10 +79,27 @@ class CodexAdapter extends BaseAdapter {
|
|
|
79
79
|
const isOpenAiNative = !this._directBaseUrl ||
|
|
80
80
|
this._directBaseUrl.includes('api.openai.com');
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
// A third-party endpoint that speaks the Responses API can drive the
|
|
83
|
+
// real CLI (tools, sandbox, resume) instead of the tool-less direct
|
|
84
|
+
// mode. Opt in with CODEX_WIRE_API=responses; the provider is passed to
|
|
85
|
+
// codex as `-c` overrides, so nothing is written to ~/.codex/config.toml.
|
|
86
|
+
this._customProvider = null;
|
|
87
|
+
if (this._codexBin && !isOpenAiNative && this._directApiKey &&
|
|
88
|
+
String(env.CODEX_WIRE_API || '').toLowerCase() === 'responses') {
|
|
89
|
+
this._customProvider = {
|
|
90
|
+
baseUrl: this._directBaseUrl,
|
|
91
|
+
reasoningEffort: String(env.CODEX_REASONING_EFFORT || '').toLowerCase() || null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (this._codexBin && (isOpenAiNative || !this._directApiKey || this._customProvider)) {
|
|
96
|
+
// CLI mode: OpenAI native API, subscription auth (no API key), or a
|
|
97
|
+
// Responses-compatible custom provider.
|
|
84
98
|
this._useCliMode = true;
|
|
85
|
-
|
|
99
|
+
const how = this._customProvider
|
|
100
|
+
? ` (custom Responses provider ${this._customProvider.baseUrl})`
|
|
101
|
+
: (!this._directApiKey ? ' (subscription auth)' : '');
|
|
102
|
+
this._log(`CLI mode: ${this._codexBin}${how}`);
|
|
86
103
|
} else if (this._directApiKey && this._directBaseUrl) {
|
|
87
104
|
this._directMode = true;
|
|
88
105
|
if (this._codexBin) {
|
|
@@ -319,6 +336,23 @@ class CodexAdapter extends BaseAdapter {
|
|
|
319
336
|
cmd.push('-m', effectiveModel);
|
|
320
337
|
}
|
|
321
338
|
|
|
339
|
+
// Custom Responses-API provider (see constructor). TOML values for
|
|
340
|
+
// `-c` must be quoted; the key itself travels in env (env_key), never
|
|
341
|
+
// on the command line.
|
|
342
|
+
if (this._customProvider) {
|
|
343
|
+
const q = (v) => JSON.stringify(String(v));
|
|
344
|
+
cmd.push(
|
|
345
|
+
'-c', 'model_provider=openagents',
|
|
346
|
+
'-c', `model_providers.openagents.name=${q('openagents')}`,
|
|
347
|
+
'-c', `model_providers.openagents.base_url=${q(this._customProvider.baseUrl)}`,
|
|
348
|
+
'-c', `model_providers.openagents.env_key=${q('OPENAI_API_KEY')}`,
|
|
349
|
+
'-c', `model_providers.openagents.wire_api=${q('responses')}`,
|
|
350
|
+
);
|
|
351
|
+
if (this._customProvider.reasoningEffort) {
|
|
352
|
+
cmd.push('-c', `model_reasoning_effort=${q(this._customProvider.reasoningEffort)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
322
356
|
// Working directory
|
|
323
357
|
if (this.workingDir) {
|
|
324
358
|
cmd.push('-C', this.workingDir);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where Cursor's Windows launcher keeps the node it runs. Shared by the
|
|
5
|
+
* adapter (to start a turn) and the installer (to ask `cursor-agent status`),
|
|
6
|
+
* so neither goes through cmd.exe and PowerShell.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/** A Cursor version directory: YYYY.MM.DD-commit, or YYYY.MM.DD-HH-MM-SS-commit. */
|
|
13
|
+
const CURSOR_VERSION_DIR = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(-\d{2}-\d{2}-\d{2})?-[a-f0-9]+$/;
|
|
14
|
+
|
|
15
|
+
/** The node.exe + index.js pair in one install root (beside it, or its newest version). */
|
|
16
|
+
function entryUnder(dir) {
|
|
17
|
+
const pair = (d) => {
|
|
18
|
+
const node = path.join(d, 'node.exe');
|
|
19
|
+
const entry = path.join(d, 'index.js');
|
|
20
|
+
return fs.existsSync(node) && fs.existsSync(entry) ? [node, entry] : null;
|
|
21
|
+
};
|
|
22
|
+
const beside = pair(dir);
|
|
23
|
+
if (beside) return beside;
|
|
24
|
+
|
|
25
|
+
let names;
|
|
26
|
+
try { names = fs.readdirSync(path.join(dir, 'versions')); } catch { return null; }
|
|
27
|
+
const day = (name) => {
|
|
28
|
+
const m = name.match(CURSOR_VERSION_DIR);
|
|
29
|
+
return Number(m[1] + m[2].padStart(2, '0') + m[3].padStart(2, '0'));
|
|
30
|
+
};
|
|
31
|
+
const newestFirst = names
|
|
32
|
+
.filter((name) => CURSOR_VERSION_DIR.test(name))
|
|
33
|
+
.sort((a, b) => day(b) - day(a) || (a < b ? 1 : -1));
|
|
34
|
+
for (const name of newestFirst) {
|
|
35
|
+
const found = pair(path.join(dir, 'versions', name));
|
|
36
|
+
if (found) return found;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The `node.exe` + `index.js` pair behind Cursor's Windows launcher, or null.
|
|
43
|
+
*
|
|
44
|
+
* The Windows install is not an npm shim. cursor-agent.cmd hands its arguments
|
|
45
|
+
* to PowerShell, and cursor-agent.ps1 runs the node.exe shipped in the newest
|
|
46
|
+
* `versions\<version>` directory (or beside the script) on that directory's
|
|
47
|
+
* index.js. This is the same lookup, so the caller can start node itself.
|
|
48
|
+
*
|
|
49
|
+
* The launcher found on PATH is not always inside the install (a copy or link
|
|
50
|
+
* in ~/.local/bin, say), so the installer's default root,
|
|
51
|
+
* %LOCALAPPDATA%\cursor-agent, is tried next.
|
|
52
|
+
*/
|
|
53
|
+
function resolveCursorNodeEntry(binPath, env = process.env) {
|
|
54
|
+
const dir = path.dirname(path.resolve(binPath));
|
|
55
|
+
const found = entryUnder(dir);
|
|
56
|
+
if (found) return found;
|
|
57
|
+
const localAppData = env && env.LOCALAPPDATA;
|
|
58
|
+
if (!localAppData) return null;
|
|
59
|
+
const root = path.join(localAppData, 'cursor-agent');
|
|
60
|
+
if (path.resolve(root) === dir) return null;
|
|
61
|
+
return entryUnder(root);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { resolveCursorNodeEntry, CURSOR_VERSION_DIR };
|
package/src/adapters/cursor.js
CHANGED
|
@@ -22,6 +22,7 @@ const BaseAdapter = require('./base');
|
|
|
22
22
|
const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
|
|
23
23
|
const { buildCursorSkillMd, workspaceSkillName } = require('./workspace-prompt');
|
|
24
24
|
const { defaultAgentWorkdir, whichBinary, whereBinary } = require('../paths');
|
|
25
|
+
const { resolveCursorNodeEntry } = require('./cursor-node-entry');
|
|
25
26
|
|
|
26
27
|
const IS_WINDOWS = process.platform === 'win32';
|
|
27
28
|
|
|
@@ -267,6 +268,18 @@ class CursorAdapter extends BaseAdapter {
|
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
_resolveToNodeCmd(binPath) {
|
|
271
|
+
// Cursor's own launcher first. Left to the `cmd.exe /c` fallback below, the
|
|
272
|
+
// prompt goes through cmd.exe and PowerShell before it reaches node, and
|
|
273
|
+
// cmd.exe stops reading a command line at its first newline. Every prompt
|
|
274
|
+
// carries one (the identity header ends in a blank line), so the message
|
|
275
|
+
// itself and every flag after it — --output-format, --trust, --force,
|
|
276
|
+
// --model, --workspace, --resume — were dropped. The CLI then ran the
|
|
277
|
+
// header alone, untrusted, and exited with "Workspace Trust Required",
|
|
278
|
+
// which reached the chat as "No response generated".
|
|
279
|
+
if (IS_WINDOWS) {
|
|
280
|
+
const own = resolveCursorNodeEntry(binPath);
|
|
281
|
+
if (own) return own;
|
|
282
|
+
}
|
|
270
283
|
const nodeBin = this._findNodeBin();
|
|
271
284
|
if (IS_WINDOWS && binPath.toLowerCase().endsWith('.cmd')) {
|
|
272
285
|
const cmdDir = path.dirname(path.resolve(binPath));
|
|
@@ -440,14 +453,30 @@ class CursorAdapter extends BaseAdapter {
|
|
|
440
453
|
if (this._stoppedBeforeStart(msgChannel)) return;
|
|
441
454
|
|
|
442
455
|
try {
|
|
443
|
-
const
|
|
456
|
+
const launcher = cmd[0];
|
|
457
|
+
const resolved = this._resolveToNodeCmd(launcher);
|
|
444
458
|
if (resolved) {
|
|
445
459
|
cmd = [...resolved, ...cmd.slice(1)];
|
|
446
460
|
} else if (IS_WINDOWS && cmd[0].toLowerCase().endsWith('.cmd')) {
|
|
461
|
+
if (cmd.some((a) => typeof a === 'string' && a.includes('\n'))) {
|
|
462
|
+
this._log(
|
|
463
|
+
`Could not find Cursor's node.exe for ${launcher}; starting it through cmd.exe, `
|
|
464
|
+
+ 'which stops reading at the first newline, so the prompt and the flags after it may be truncated',
|
|
465
|
+
'warn',
|
|
466
|
+
);
|
|
467
|
+
}
|
|
447
468
|
cmd = ['cmd.exe', '/c', ...cmd];
|
|
448
469
|
}
|
|
449
470
|
|
|
450
471
|
const cleanEnv = { ...(this.agentEnv || process.env) };
|
|
472
|
+
// Starting node directly skips Cursor's launcher script, so set what the
|
|
473
|
+
// script would have: its own name, and node's compile cache.
|
|
474
|
+
if (IS_WINDOWS && resolved) {
|
|
475
|
+
if (!cleanEnv.CURSOR_INVOKED_AS) cleanEnv.CURSOR_INVOKED_AS = path.basename(launcher);
|
|
476
|
+
if (!cleanEnv.NODE_COMPILE_CACHE && cleanEnv.LOCALAPPDATA) {
|
|
477
|
+
cleanEnv.NODE_COMPILE_CACHE = path.join(cleanEnv.LOCALAPPDATA, 'cursor-compile-cache');
|
|
478
|
+
}
|
|
479
|
+
}
|
|
451
480
|
|
|
452
481
|
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
453
482
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -680,3 +709,4 @@ function _extractToolDetail(tc) {
|
|
|
680
709
|
}
|
|
681
710
|
|
|
682
711
|
module.exports = CursorAdapter;
|
|
712
|
+
module.exports.resolveCursorNodeEntry = resolveCursorNodeEntry;
|