cursor-opencode-provider 0.8.0 → 0.8.3
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/CHANGELOG.md +174 -0
- package/README.md +230 -44
- package/SECURITY.md +60 -12
- package/dist/activity.d.ts +73 -1
- package/dist/activity.js +231 -5
- package/dist/agent-url.js +37 -9
- package/dist/auth-renewal.d.ts +1 -1
- package/dist/auth-renewal.js +58 -26
- package/dist/billing.d.ts +105 -0
- package/dist/billing.js +212 -0
- package/dist/classic-tool-results.d.ts +1 -0
- package/dist/classic-tool-results.js +42 -0
- package/dist/context/build.d.ts +23 -4
- package/dist/context/build.js +65 -10
- package/dist/context/dynamic-catalog.js +6 -1
- package/dist/context/env.js +27 -3
- package/dist/context/epoch.js +9 -5
- package/dist/context/frozen.js +6 -2
- package/dist/context/git.d.ts +5 -2
- package/dist/context/git.js +17 -7
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/paths.d.ts +45 -18
- package/dist/context/paths.js +72 -22
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +34 -0
- package/dist/context/skills.js +211 -0
- package/dist/debug.js +13 -2
- package/dist/early-tool-rows.d.ts +60 -0
- package/dist/early-tool-rows.js +96 -0
- package/dist/errors.d.ts +49 -7
- package/dist/errors.js +163 -8
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +117 -13
- package/dist/host-interrupt.d.ts +12 -0
- package/dist/host-interrupt.js +18 -0
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/host-steer.d.ts +71 -0
- package/dist/host-steer.js +232 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +122 -30
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +33 -21
- package/dist/index.d.ts +2 -2
- package/dist/language-model.d.ts +149 -30
- package/dist/language-model.js +3127 -673
- package/dist/lost-request.d.ts +39 -0
- package/dist/lost-request.js +111 -0
- package/dist/model-config.d.ts +12 -1
- package/dist/model-config.js +78 -30
- package/dist/models.d.ts +26 -3
- package/dist/models.js +101 -45
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/tool-results.d.ts +18 -0
- package/dist/opencode2/tool-results.js +66 -0
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +233 -65
- package/dist/plugin.js +97 -23
- package/dist/pricing-data.d.ts +40 -3
- package/dist/pricing-data.js +40 -3
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +7 -4
- package/dist/process-shared.d.ts +14 -0
- package/dist/process-shared.js +18 -0
- package/dist/protocol/apply-patch.d.ts +25 -13
- package/dist/protocol/apply-patch.js +79 -30
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.d.ts +5 -1
- package/dist/protocol/blob-store.js +10 -6
- package/dist/protocol/client-version.js +6 -4
- package/dist/protocol/conversation-cache-header.d.ts +23 -0
- package/dist/protocol/conversation-cache-header.js +149 -0
- package/dist/protocol/conversation-persistence.d.ts +27 -1
- package/dist/protocol/conversation-persistence.js +628 -126
- package/dist/protocol/conversation-state.d.ts +8 -1
- package/dist/protocol/conversation-state.js +28 -7
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +33 -23
- package/dist/protocol/host-notes.d.ts +31 -0
- package/dist/protocol/host-notes.js +85 -0
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +164 -21
- package/dist/protocol/request.d.ts +40 -19
- package/dist/protocol/request.js +55 -28
- package/dist/protocol/sent-history-images.d.ts +49 -0
- package/dist/protocol/sent-history-images.js +102 -0
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +6 -0
- package/dist/protocol/tool-call-bridge.js +24 -0
- package/dist/protocol/tools.d.ts +90 -9
- package/dist/protocol/tools.js +543 -151
- package/dist/protocol/turn-provenance.d.ts +19 -4
- package/dist/protocol/turn-provenance.js +77 -20
- package/dist/replay-safety.d.ts +22 -4
- package/dist/replay-safety.js +86 -27
- package/dist/retry-budget.d.ts +21 -0
- package/dist/retry-budget.js +94 -0
- package/dist/runtime-files.d.ts +23 -0
- package/dist/runtime-files.js +581 -0
- package/dist/session.d.ts +129 -10
- package/dist/session.js +198 -35
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/shell-timeout.d.ts +54 -10
- package/dist/shell-timeout.js +362 -92
- package/dist/stream-write.d.ts +20 -0
- package/dist/stream-write.js +87 -0
- package/dist/terminal-runner.d.ts +15 -0
- package/dist/terminal-runner.js +528 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +10 -1
- package/dist/transport/connect.js +124 -27
- package/dist/usage.d.ts +37 -17
- package/dist/usage.js +85 -34
- package/package.json +5 -3
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
- package/dist/protocol/thinking.d.ts +0 -15
- package/dist/protocol/thinking.js +0 -17
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel tool-call step boundary for OpenCode's AI SDK step model.
|
|
3
|
+
*
|
|
4
|
+
* Cursor fires execs as they are generated; CLI demuxes them concurrently and
|
|
5
|
+
* has no step boundary. OpenCode needs one `finish: tool-calls` after every
|
|
6
|
+
* call of the generation. Cursor's `tool_requests_listed` (InteractionUpdate
|
|
7
|
+
* field 27) carries that count. See tasks/plans/parallel-tool-calls-cli-parity.md.
|
|
8
|
+
*/
|
|
9
|
+
/** Idle without a progress frame while holding → close the step (ms). */
|
|
10
|
+
export const PARALLEL_STEP_IDLE_MS = 1_500;
|
|
11
|
+
/** Process-level: Cursor has sent field 27 on any Run in this process. */
|
|
12
|
+
let toolRequestsListedSeen = false;
|
|
13
|
+
export function noteToolRequestsListedSeen() {
|
|
14
|
+
toolRequestsListedSeen = true;
|
|
15
|
+
}
|
|
16
|
+
export function hasSeenToolRequestsListed() {
|
|
17
|
+
return toolRequestsListedSeen;
|
|
18
|
+
}
|
|
19
|
+
/** Test-only: reset the process capability gate. */
|
|
20
|
+
export function resetToolRequestsListedSeenForTests() {
|
|
21
|
+
toolRequestsListedSeen = false;
|
|
22
|
+
}
|
|
23
|
+
export function createParallelStep(now = Date.now()) {
|
|
24
|
+
return {
|
|
25
|
+
callIds: new Set(),
|
|
26
|
+
resolved: new Set(),
|
|
27
|
+
emitted: 0,
|
|
28
|
+
lastProgressAt: now,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function noteParallelStepProgress(step, now = Date.now()) {
|
|
32
|
+
step.lastProgressAt = now;
|
|
33
|
+
}
|
|
34
|
+
export function noteListedCallCount(step, callCount, now = Date.now()) {
|
|
35
|
+
noteToolRequestsListedSeen();
|
|
36
|
+
if (Number.isFinite(callCount) && callCount >= 0) {
|
|
37
|
+
step.listedCount = callCount;
|
|
38
|
+
}
|
|
39
|
+
noteParallelStepProgress(step, now);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Record a final disposition for `callId`. Returns true when newly resolved.
|
|
43
|
+
* Empty ids are ignored. Prior-step ids in `prior` are ignored.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveParallelStepCall(step, callId, prior, now = Date.now()) {
|
|
46
|
+
if (!callId)
|
|
47
|
+
return false;
|
|
48
|
+
if (prior?.has(callId))
|
|
49
|
+
return false;
|
|
50
|
+
step.callIds.add(callId);
|
|
51
|
+
if (step.resolved.has(callId))
|
|
52
|
+
return false;
|
|
53
|
+
step.resolved.add(callId);
|
|
54
|
+
noteParallelStepProgress(step, now);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
export function recordParallelStepEmission(step, now = Date.now()) {
|
|
58
|
+
step.emitted++;
|
|
59
|
+
noteParallelStepProgress(step, now);
|
|
60
|
+
}
|
|
61
|
+
/** True when the listed count is known and every listed call is resolved. */
|
|
62
|
+
export function parallelStepCountMet(step) {
|
|
63
|
+
return step.listedCount !== undefined && step.resolved.size >= step.listedCount;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Whether to keep pumping for more calls of this step instead of finishing.
|
|
67
|
+
*
|
|
68
|
+
* - Gate off and no count yet → do not hold (today's one-call finish).
|
|
69
|
+
* - Human-gated interaction with unknown count → do not hold (prompt must show).
|
|
70
|
+
* - Otherwise hold until the count is met (or the liveness guard fires).
|
|
71
|
+
*/
|
|
72
|
+
export function shouldHoldParallelStep(step, options = {}) {
|
|
73
|
+
if (options.humanGatedWithoutCount && step.listedCount === undefined)
|
|
74
|
+
return false;
|
|
75
|
+
const fieldSeen = options.fieldSeen ?? hasSeenToolRequestsListed();
|
|
76
|
+
if (!fieldSeen && step.listedCount === undefined)
|
|
77
|
+
return false;
|
|
78
|
+
if (parallelStepCountMet(step))
|
|
79
|
+
return false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/** True when the hold has made no progress for `idleMs`. */
|
|
83
|
+
export function shouldGuardCloseParallelStep(step, now = Date.now(), idleMs = PARALLEL_STEP_IDLE_MS) {
|
|
84
|
+
if (parallelStepCountMet(step))
|
|
85
|
+
return false;
|
|
86
|
+
return now - step.lastProgressAt >= idleMs;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Frames that reset the liveness guard while holding. Heartbeat, KV, and
|
|
90
|
+
* checkpoint are not progress — they say nothing about further tool calls.
|
|
91
|
+
*/
|
|
92
|
+
export function isParallelStepProgressFrame(kinds) {
|
|
93
|
+
return !!(kinds.toolRequestsListed
|
|
94
|
+
|| kinds.partialToolCall
|
|
95
|
+
|| kinds.toolCallDelta
|
|
96
|
+
|| kinds.toolCallStarted
|
|
97
|
+
|| kinds.toolCallCompleted
|
|
98
|
+
|| kinds.exec
|
|
99
|
+
|| kinds.interactionQuery
|
|
100
|
+
|| kinds.textDelta
|
|
101
|
+
|| kinds.thinkingDelta);
|
|
102
|
+
}
|
package/dist/plugin-opencode2.js
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { CURSOR_PROVIDER_ID, CURSOR_COMPACTION_OPTION, CURSOR_HOST_AGENT_OPTION, } from "./shared.js";
|
|
2
2
|
import { createSdk, cursorApiBaseURL, cursorGetServerConfigTelemetryEnabled, isCursorPackage } from "./plugin-core.js";
|
|
3
|
-
import { opencodeGlobalCacheDir } from "./context/paths.js";
|
|
3
|
+
import { opencode2PlanDir, opencodeGlobalCacheDir, setNativePlansDir } from "./context/paths.js";
|
|
4
4
|
import { discoverModels, isCacheFresh, readCache } from "./models.js";
|
|
5
5
|
import { resolveAgentUrl } from "./agent-url.js";
|
|
6
|
-
import { sessionActivity } from "./activity.js";
|
|
6
|
+
import { applyHostPromptEvent, applyHostToolEvent, sessionActivity } from "./activity.js";
|
|
7
|
+
import { notifyHostInterrupt } from "./host-interrupt.js";
|
|
8
|
+
import { announceHostSteer, forgetEarlySteerOfInbox, forgetEarlySteers, rememberQueuedSteer, takeQueuedSteer, } from "./host-steer.js";
|
|
7
9
|
import { fetchOpenCodeWebSearchText, parseExaWebSearchResults, } from "./web-tools.js";
|
|
8
|
-
import { captureCursorShellResult,
|
|
10
|
+
import { armCursorShellCreate, captureCursorShellResult, captureCursorShellFailure, prepareCursorShellCreate, prepareCursorShellArgs, releaseCursorShellEnv, sanitizeRegisteredCursorShellOutput, } from "./shell-timeout.js";
|
|
9
11
|
import { applyCursorProviderInventory, CURSOR_INTEGRATION_ID } from "./opencode2/catalog.js";
|
|
10
12
|
import { applyCursorIntegration, requireCursorAccessToken, resolveCursorAccessToken, } from "./opencode2/integration.js";
|
|
11
13
|
import { exposeDirectMcpTools, rememberDirectMcpNamespaces } from "./opencode2/mcp-direct.js";
|
|
12
14
|
import { registerTodoTools } from "./opencode2/todo-tools.js";
|
|
15
|
+
import { registerCursorImageSaveTool } from "./opencode2/image-save-tool.js";
|
|
13
16
|
import { OPENCODE_2_TOOL_DIALECT } from "./protocol/tools.js";
|
|
14
17
|
import { clearSessionTodos } from "./todo-store.js";
|
|
15
18
|
import { markCompactionSession } from "./compaction-marker.js";
|
|
16
19
|
import { getSessionDirectory, markSessionDirectory } from "./session-directory.js";
|
|
20
|
+
import { rememberHostSkillFiles } from "./context/skills.js";
|
|
17
21
|
import { trace } from "./debug.js";
|
|
18
|
-
import {
|
|
19
|
-
import { cancelHostAgentModeSwitch, setHostAgentModeSwitch, } from "./host-agent-mode.js";
|
|
22
|
+
import { cancelHostAgentModeSwitch, hostAgentSwitchPromptText, setHostAgentModeSwitch, } from "./host-agent-mode.js";
|
|
20
23
|
import { clearActiveCursorMode, getActiveCursorMode, normalizeSwitchModeId, setActiveCursorMode, } from "./protocol/switch-mode.js";
|
|
21
24
|
import { CursorPlugin } from "./plugin.js";
|
|
25
|
+
import { nativeToolRefusal, preserveNativeToolResults } from "./opencode2/tool-results.js";
|
|
22
26
|
/**
|
|
23
27
|
* OpenCode 2.0 plugin.
|
|
24
28
|
*
|
|
@@ -92,45 +96,90 @@ const plugin = {
|
|
|
92
96
|
setup: async (ctx) => {
|
|
93
97
|
const cacheDir = opencodeGlobalCacheDir();
|
|
94
98
|
const workspaceRoot = ctx.location?.directory || process.cwd();
|
|
99
|
+
// CreatePlan writes where OpenCode 2.0's Plan agent keeps plan files.
|
|
100
|
+
// OpenCode 2.0 sets a plugin up once per location instance and again on a
|
|
101
|
+
// plugin reload, then disposes older setups: each cleanup removes only
|
|
102
|
+
// what its own setup installed.
|
|
103
|
+
const disposeNativePlansDir = setNativePlansDir(opencode2PlanDir());
|
|
104
|
+
let disposeHostAgentModeSwitch;
|
|
105
|
+
trace(`opencode2 plugin: setup directory=${workspaceRoot}`);
|
|
95
106
|
const hasShellEnvHook = typeof ctx.shell?.hook === "function";
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
const refusals = new Map();
|
|
108
|
+
const permissionCalls = new Map();
|
|
109
|
+
const rememberRefusal = (id, refusal) => {
|
|
110
|
+
refusals.delete(id);
|
|
111
|
+
refusals.set(id, refusal);
|
|
112
|
+
while (refusals.size > 512)
|
|
113
|
+
refusals.delete(refusals.keys().next().value);
|
|
114
|
+
};
|
|
115
|
+
const releasePermissionCalls = () => {
|
|
116
|
+
for (const call of permissionCalls.values())
|
|
117
|
+
releaseCursorShellEnv(call.callID);
|
|
118
|
+
permissionCalls.clear();
|
|
119
|
+
};
|
|
120
|
+
const toolLifecycle = (event) => {
|
|
121
|
+
const payload = eventPayload(event);
|
|
122
|
+
if (event?.type === "permission.asked" && payload?.source?.type === "tool"
|
|
123
|
+
&& typeof payload.source.id === "string" && payload.source.id.startsWith("cursor_")
|
|
124
|
+
&& typeof payload.id === "string" && typeof payload.sessionID === "string") {
|
|
125
|
+
permissionCalls.set(payload.id, { sessionID: payload.sessionID, callID: payload.source.id });
|
|
126
|
+
}
|
|
127
|
+
else if (event?.type === "permission.replied") {
|
|
128
|
+
const call = permissionCalls.get(payload?.requestID);
|
|
129
|
+
if (!call || call.sessionID !== payload?.sessionID)
|
|
130
|
+
return;
|
|
131
|
+
permissionCalls.delete(payload.requestID);
|
|
132
|
+
if (payload.reply === "reject") {
|
|
133
|
+
rememberRefusal(call.callID, "rejected");
|
|
134
|
+
releaseCursorShellEnv(call.callID);
|
|
135
|
+
// Native rejection also rejects the session's other pending requests.
|
|
136
|
+
for (const [id, pending] of permissionCalls)
|
|
137
|
+
if (pending.sessionID === call.sessionID) {
|
|
138
|
+
rememberRefusal(pending.callID, "rejected");
|
|
139
|
+
releaseCursorShellEnv(pending.callID);
|
|
140
|
+
permissionCalls.delete(id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if ((event?.type === "session.tool.failed" || event?.type === "session.tool.success")
|
|
145
|
+
&& typeof payload?.id === "string") {
|
|
146
|
+
if (event.type === "session.tool.failed")
|
|
147
|
+
releaseCursorShellEnv(payload.id);
|
|
148
|
+
for (const [id, call] of permissionCalls)
|
|
149
|
+
if (call.callID === payload.id && call.sessionID === payload.sessionID) {
|
|
150
|
+
permissionCalls.delete(id);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (event?.type === "session.deleted") {
|
|
154
|
+
for (const [id, call] of permissionCalls)
|
|
155
|
+
if (call.sessionID === payload?.sessionID) {
|
|
156
|
+
releaseCursorShellEnv(call.callID);
|
|
157
|
+
permissionCalls.delete(id);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
if (typeof ctx.session.switchAgent === "function") {
|
|
100
162
|
const switchAgent = ctx.session.switchAgent;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
163
|
+
const continueTurn = typeof ctx.session.synthetic === "function"
|
|
164
|
+
? ctx.session.synthetic
|
|
165
|
+
: typeof ctx.session.prompt === "function"
|
|
166
|
+
? ctx.session.prompt
|
|
167
|
+
: undefined;
|
|
168
|
+
disposeHostAgentModeSwitch = setHostAgentModeSwitch(async ({ sessionID, targetModeID }) => {
|
|
169
|
+
const agent = normalizeSwitchModeId(targetModeID) === "plan"
|
|
170
|
+
|| normalizeSwitchModeId(targetModeID) === "spec"
|
|
171
|
+
? "plan"
|
|
172
|
+
: "build";
|
|
173
|
+
await switchAgent({ sessionID, agent });
|
|
174
|
+
// switchAgent only publishes AgentSelected and leaves the session idle.
|
|
175
|
+
// Continue under the new agent the same way OpenCode 1.x promptAsync does.
|
|
176
|
+
if (continueTurn) {
|
|
177
|
+
await continueTurn({
|
|
105
178
|
sessionID,
|
|
106
|
-
text:
|
|
179
|
+
text: hostAgentSwitchPromptText(agent),
|
|
107
180
|
});
|
|
108
181
|
}
|
|
109
|
-
|
|
110
|
-
// Switching and admitting input are separate public APIs. Restore the
|
|
111
|
-
// plan agent if admission fails so the shared retry state is honest:
|
|
112
|
-
// the plan remains active rather than silently leaving the session in
|
|
113
|
-
// build mode with no execution turn.
|
|
114
|
-
await switchAgent({ sessionID, agent: "plan" }).catch(() => { });
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
setPlanExecutionKickoff(undefined);
|
|
121
|
-
}
|
|
122
|
-
if (typeof ctx.session.switchAgent === "function") {
|
|
123
|
-
const switchAgent = ctx.session.switchAgent;
|
|
124
|
-
setHostAgentModeSwitch(async ({ sessionID, targetModeID }) => {
|
|
125
|
-
const mode = normalizeSwitchModeId(targetModeID);
|
|
126
|
-
await switchAgent({
|
|
127
|
-
sessionID,
|
|
128
|
-
agent: mode === "plan" || mode === "spec" ? "plan" : "build",
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
setHostAgentModeSwitch(undefined);
|
|
182
|
+
}, continueTurn ? { resumesTurn: true } : {});
|
|
134
183
|
}
|
|
135
184
|
const registrations = [];
|
|
136
185
|
const track = async (p) => {
|
|
@@ -200,15 +249,13 @@ const plugin = {
|
|
|
200
249
|
};
|
|
201
250
|
// ── AI SDK wiring ────────────────────────────────────────
|
|
202
251
|
await track(ctx.aisdk.hook("sdk", async (event) => {
|
|
203
|
-
if (event.sdk)
|
|
204
|
-
return;
|
|
205
252
|
if (!isCursorPackage(event.package, event.model.providerID))
|
|
206
253
|
return;
|
|
254
|
+
// The host's generic loader runs before plugin hooks and may already
|
|
255
|
+
// have created a Cursor SDK with a captured token. Own our package's
|
|
256
|
+
// SDK so current integration credentials are resolved on every Run.
|
|
207
257
|
event.sdk = createSdk({
|
|
208
258
|
name: event.model.providerID || CURSOR_PROVIDER_ID,
|
|
209
|
-
// Resolved per Run open: this SDK lives as long as the daemon, so
|
|
210
|
-
// a token captured here would outlive its expiry.
|
|
211
|
-
getAccessToken: (request) => requireCursorAccessToken(ctx.integration, request),
|
|
212
259
|
// Static fallback only. This hook fires once per model/package, not
|
|
213
260
|
// per session, and 2.0 runs one daemon across many projects — the
|
|
214
261
|
// real per-request directory comes from `x-opencode-directory`
|
|
@@ -216,6 +263,9 @@ const plugin = {
|
|
|
216
263
|
workspaceRoot,
|
|
217
264
|
cacheDir,
|
|
218
265
|
...event.options,
|
|
266
|
+
// Keep after host options: a captured resolver/token must not bypass
|
|
267
|
+
// the integration's current credential and renewal path.
|
|
268
|
+
getAccessToken: (request) => requireCursorAccessToken(ctx.integration, request),
|
|
219
269
|
// Keep after `event.options` so the OC2 plugin always selects the
|
|
220
270
|
// `path`/`shell` dialect when advertised schemas are opaque.
|
|
221
271
|
defaultDialect: OPENCODE_2_TOOL_DIALECT,
|
|
@@ -262,6 +312,7 @@ const plugin = {
|
|
|
262
312
|
// them as direct catalog tools (`codemode: false` + output schema) if
|
|
263
313
|
// the host does not already own those names. When off, register none.
|
|
264
314
|
registerTodoTools(draft);
|
|
315
|
+
registerCursorImageSaveTool(draft, ctx);
|
|
265
316
|
// MCP tools default into Code Mode. Move every server that did not
|
|
266
317
|
// explicitly opt in onto the direct catalog so this provider can
|
|
267
318
|
// advertise them to Cursor (issue #29 still routes via CallDynamicTool).
|
|
@@ -269,22 +320,35 @@ const plugin = {
|
|
|
269
320
|
exposeDirectMcpTools(draft, directMcpNamespaces);
|
|
270
321
|
}));
|
|
271
322
|
// ── Shell timeout wrapper ────────────────────────────────
|
|
272
|
-
await track(ctx.tool.hook("execute.before", (event) => {
|
|
273
|
-
if (!isShellTool(event.tool))
|
|
323
|
+
await track(ctx.tool.hook("execute.before", async (event) => {
|
|
324
|
+
if (!isShellTool(event.tool) || typeof event.input !== "object" || event.input === null)
|
|
274
325
|
return;
|
|
275
326
|
const executionID = toolExecutionID(event);
|
|
276
|
-
|
|
277
|
-
|
|
327
|
+
const input = event.input;
|
|
328
|
+
prepareCursorShellArgs(executionID, input, {
|
|
329
|
+
deferToShellHook: hasShellEnvHook,
|
|
278
330
|
preferWrapperCommand: !hasShellEnvHook,
|
|
279
331
|
});
|
|
332
|
+
if (hasShellEnvHook)
|
|
333
|
+
await armCursorShellCreate(executionID, event);
|
|
280
334
|
}));
|
|
281
335
|
await track(ctx.tool.hook("execute.after", (event) => {
|
|
336
|
+
const executionID = toolExecutionID(event);
|
|
337
|
+
if (event.status === "error") {
|
|
338
|
+
const refusal = nativeToolRefusal(event.error);
|
|
339
|
+
if (refusal)
|
|
340
|
+
rememberRefusal(executionID, refusal);
|
|
341
|
+
}
|
|
342
|
+
else
|
|
343
|
+
refusals.delete(executionID);
|
|
282
344
|
if (!isShellTool(event.tool))
|
|
283
345
|
return;
|
|
284
|
-
const executionID = toolExecutionID(event);
|
|
285
346
|
try {
|
|
286
|
-
if (event.status !== "completed")
|
|
347
|
+
if (event.status !== "completed") {
|
|
348
|
+
if (event.status === "error")
|
|
349
|
+
captureCursorShellFailure(executionID, event.error);
|
|
287
350
|
return;
|
|
351
|
+
}
|
|
288
352
|
const result = event.result;
|
|
289
353
|
// V1: `output` is the model-facing string. V2: `output` is structured
|
|
290
354
|
// per-tool output (shell: `{ output: string, ... }`) and the
|
|
@@ -331,10 +395,7 @@ const plugin = {
|
|
|
331
395
|
}));
|
|
332
396
|
if (ctx.shell) {
|
|
333
397
|
await track(ctx.shell.hook("create.before", (event) => {
|
|
334
|
-
|
|
335
|
-
if (!env)
|
|
336
|
-
return;
|
|
337
|
-
event.env = { ...event.env, ...env };
|
|
398
|
+
prepareCursorShellCreate(event);
|
|
338
399
|
}));
|
|
339
400
|
}
|
|
340
401
|
const rememberSessionDirectory = async (sessionID) => {
|
|
@@ -348,6 +409,7 @@ const plugin = {
|
|
|
348
409
|
}
|
|
349
410
|
};
|
|
350
411
|
await track(ctx.session.hook("context", async (event) => {
|
|
412
|
+
event.messages = preserveNativeToolResults(event.messages, refusals);
|
|
351
413
|
markCompactionAndOptions(event, event.agent === "compaction");
|
|
352
414
|
event.options ??= {};
|
|
353
415
|
event.options[CURSOR_HOST_AGENT_OPTION] = event.agent;
|
|
@@ -375,6 +437,20 @@ const plugin = {
|
|
|
375
437
|
// last known session directory ahead of this Location's static root.
|
|
376
438
|
// Scoped by the host to this provider: other providers' requests never
|
|
377
439
|
// reach the callback, so the header cannot leak to their endpoints.
|
|
440
|
+
// Skill files for RequestContext path-desc `agent_skills` (see context/skills.ts).
|
|
441
|
+
const rememberSkillFiles = async () => {
|
|
442
|
+
if (!ctx.skill)
|
|
443
|
+
return;
|
|
444
|
+
try {
|
|
445
|
+
const listed = await ctx.skill.list();
|
|
446
|
+
// skill.list is bound to this plugin Location. A session may have
|
|
447
|
+
// moved; never relabel this catalog with the session's new directory.
|
|
448
|
+
rememberHostSkillFiles(listed.location.directory, listed.data);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
trace(`model.request: skill.list failed: ${String(error)}`);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
378
454
|
await track(ctx.session.hook("model.request", async (event) => {
|
|
379
455
|
const current = await ctx.session
|
|
380
456
|
.get({ sessionID: event.sessionID })
|
|
@@ -387,20 +463,24 @@ const plugin = {
|
|
|
387
463
|
const directory = current ?? getSessionDirectory(event.sessionID) ?? ctx.location?.directory;
|
|
388
464
|
if (!directory)
|
|
389
465
|
return;
|
|
466
|
+
await rememberSkillFiles();
|
|
390
467
|
event.headers = {
|
|
391
468
|
...event.headers,
|
|
392
469
|
"x-opencode-directory": encodeURIComponent(directory),
|
|
393
470
|
};
|
|
394
471
|
}, { providerID: CURSOR_PROVIDER_ID }));
|
|
395
472
|
await track(ctx.session.hook("compaction", async (event) => {
|
|
473
|
+
event.messages = preserveNativeToolResults(event.messages, refusals);
|
|
396
474
|
markCompactionAndOptions(event, true);
|
|
397
475
|
await rememberSessionDirectory(event.sessionID);
|
|
398
476
|
}));
|
|
399
477
|
await track(ctx.session.hook("generate", async (event) => {
|
|
478
|
+
event.messages = preserveNativeToolResults(event.messages, refusals);
|
|
400
479
|
markCompactionAndOptions(event, false);
|
|
401
480
|
await rememberSessionDirectory(event.sessionID);
|
|
402
481
|
}));
|
|
403
482
|
await track(ctx.session.hook("title", async (event) => {
|
|
483
|
+
event.messages = preserveNativeToolResults(event.messages, refusals);
|
|
404
484
|
markCompactionAndOptions(event, false);
|
|
405
485
|
await rememberSessionDirectory(event.sessionID);
|
|
406
486
|
}));
|
|
@@ -465,34 +545,61 @@ const plugin = {
|
|
|
465
545
|
modelsLoaded = false;
|
|
466
546
|
credentialGeneration++;
|
|
467
547
|
};
|
|
468
|
-
const unsubscribe = subscribeSessionActivity(ctx, ensureModels, onCredentialSwitch);
|
|
548
|
+
const unsubscribe = subscribeSessionActivity(ctx, ensureModels, onCredentialSwitch, toolLifecycle, releasePermissionCalls);
|
|
469
549
|
return async () => {
|
|
470
550
|
clearInterval(retry);
|
|
471
551
|
unsubscribe?.();
|
|
472
|
-
|
|
473
|
-
|
|
552
|
+
releasePermissionCalls();
|
|
553
|
+
refusals.clear();
|
|
554
|
+
await saveOpenTurnsForShutdown();
|
|
555
|
+
disposeHostAgentModeSwitch?.();
|
|
556
|
+
disposeNativePlansDir();
|
|
557
|
+
trace(`opencode2 plugin: cleanup directory=${workspaceRoot}`);
|
|
474
558
|
for (const registration of registrations.reverse()) {
|
|
475
559
|
await registration.dispose().catch(() => { });
|
|
476
560
|
}
|
|
477
561
|
};
|
|
478
562
|
},
|
|
479
563
|
};
|
|
480
|
-
|
|
564
|
+
const SHUTDOWN_SAVE_TIMEOUT_MS = 2_000;
|
|
565
|
+
/** Save every open turn for the next server start, without holding shutdown up for long. */
|
|
566
|
+
async function saveOpenTurnsForShutdown() {
|
|
567
|
+
let timer;
|
|
568
|
+
const timedOut = new Promise((resolve) => {
|
|
569
|
+
timer = setTimeout(resolve, SHUTDOWN_SAVE_TIMEOUT_MS);
|
|
570
|
+
timer.unref?.();
|
|
571
|
+
});
|
|
481
572
|
try {
|
|
482
|
-
|
|
573
|
+
await Promise.race([notifyHostInterrupt(undefined, "shutdown"), timedOut]);
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
clearTimeout(timer);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function subscribeSessionActivity(ctx, onEvent, onCredentialSwitch, onToolEvent, onStreamClosed) {
|
|
580
|
+
try {
|
|
581
|
+
const controller = new AbortController();
|
|
582
|
+
const stream = ctx.event.subscribe({ signal: controller.signal });
|
|
483
583
|
if (!stream || typeof stream[Symbol.asyncIterator] !== "function")
|
|
484
584
|
return undefined;
|
|
485
585
|
let stopped = false;
|
|
486
586
|
void (async () => {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
587
|
+
try {
|
|
588
|
+
for await (const event of stream) {
|
|
589
|
+
if (stopped)
|
|
590
|
+
break;
|
|
591
|
+
applySessionActivity(event, onCredentialSwitch);
|
|
592
|
+
onToolEvent?.(event);
|
|
593
|
+
onEvent?.();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
finally {
|
|
597
|
+
onStreamClosed?.();
|
|
492
598
|
}
|
|
493
599
|
})().catch(() => { });
|
|
494
600
|
return () => {
|
|
495
601
|
stopped = true;
|
|
602
|
+
controller.abort();
|
|
496
603
|
};
|
|
497
604
|
}
|
|
498
605
|
catch {
|
|
@@ -502,6 +609,8 @@ function subscribeSessionActivity(ctx, onEvent, onCredentialSwitch) {
|
|
|
502
609
|
function applySessionActivity(event, onCredentialSwitch) {
|
|
503
610
|
const payload = eventPayload(event);
|
|
504
611
|
const info = payload?.info;
|
|
612
|
+
applyHostPromptEvent(sessionActivity, event?.type, payload);
|
|
613
|
+
applyHostToolEvent(sessionActivity, event?.type, payload);
|
|
505
614
|
switch (event?.type) {
|
|
506
615
|
case "credential.switched":
|
|
507
616
|
case "credential.updated": {
|
|
@@ -528,8 +637,8 @@ function applySessionActivity(event, onCredentialSwitch) {
|
|
|
528
637
|
if (id) {
|
|
529
638
|
sessionActivity.removeSession(id);
|
|
530
639
|
clearSessionTodos(id);
|
|
640
|
+
forgetEarlySteers(id);
|
|
531
641
|
clearActiveCursorMode(id);
|
|
532
|
-
cancelPlanExecutionKickoff(id);
|
|
533
642
|
cancelHostAgentModeSwitch(id);
|
|
534
643
|
}
|
|
535
644
|
break;
|
|
@@ -553,6 +662,42 @@ function applySessionActivity(event, onCredentialSwitch) {
|
|
|
553
662
|
sessionActivity.recordActivity(id);
|
|
554
663
|
break;
|
|
555
664
|
}
|
|
665
|
+
case "session.inbox.enqueued": {
|
|
666
|
+
const id = payload?.sessionID;
|
|
667
|
+
if (id)
|
|
668
|
+
sessionActivity.recordActivity(id);
|
|
669
|
+
const steer = inboxSteer(id, payload?.inboxID, payload?.item);
|
|
670
|
+
if (!steer)
|
|
671
|
+
break;
|
|
672
|
+
if (payload.item.delivery === "steer")
|
|
673
|
+
announceInboxSteer(steer);
|
|
674
|
+
else
|
|
675
|
+
rememberQueuedSteer(steer);
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
case "session.inbox.delivery.changed": {
|
|
679
|
+
const id = payload?.sessionID;
|
|
680
|
+
if (id)
|
|
681
|
+
sessionActivity.recordActivity(id);
|
|
682
|
+
if (payload?.delivery !== "steer" || typeof payload?.inboxID !== "string")
|
|
683
|
+
break;
|
|
684
|
+
const steer = takeQueuedSteer(payload.inboxID);
|
|
685
|
+
if (steer)
|
|
686
|
+
announceInboxSteer(steer);
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case "session.inbox.delivered":
|
|
690
|
+
case "session.inbox.cancelled": {
|
|
691
|
+
const id = payload?.sessionID;
|
|
692
|
+
if (id)
|
|
693
|
+
sessionActivity.recordActivity(id);
|
|
694
|
+
if (typeof payload?.inboxID !== "string")
|
|
695
|
+
break;
|
|
696
|
+
takeQueuedSteer(payload.inboxID);
|
|
697
|
+
if (event.type === "session.inbox.cancelled" && typeof id === "string")
|
|
698
|
+
forgetEarlySteerOfInbox(id, payload.inboxID);
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
556
701
|
default: {
|
|
557
702
|
// OpenCode 2.0 emits granular `session.*` progress events instead of
|
|
558
703
|
// the V1 `message.updated` family (`session.tool.called/success/failed`,
|
|
@@ -563,9 +708,32 @@ function applySessionActivity(event, onCredentialSwitch) {
|
|
|
563
708
|
const id = payload?.sessionID ?? info?.sessionID ?? info?.id;
|
|
564
709
|
if (id)
|
|
565
710
|
sessionActivity.recordActivity(id);
|
|
711
|
+
if (id && type === "session.execution.interrupted") {
|
|
712
|
+
void notifyHostInterrupt(id, typeof payload?.reason === "string" ? payload.reason : "user", typeof event.created === "number" ? event.created : Date.now());
|
|
713
|
+
}
|
|
566
714
|
}
|
|
567
715
|
break;
|
|
568
716
|
}
|
|
569
717
|
}
|
|
570
718
|
}
|
|
719
|
+
/** A plain-text user message; one with files or skills reaches the prompt in another shape. */
|
|
720
|
+
function inboxSteer(sessionID, inboxID, item) {
|
|
721
|
+
if (typeof sessionID !== "string" || !sessionID || typeof inboxID !== "string" || !inboxID)
|
|
722
|
+
return undefined;
|
|
723
|
+
if (item?.type !== "user" || (item.delivery !== "steer" && item.delivery !== "queue"))
|
|
724
|
+
return undefined;
|
|
725
|
+
const prompt = item.payload;
|
|
726
|
+
if (typeof prompt?.text !== "string" || !prompt.text.trim())
|
|
727
|
+
return undefined;
|
|
728
|
+
if (prompt.files?.length || prompt.skills?.length)
|
|
729
|
+
return undefined;
|
|
730
|
+
return { sessionID, inboxID, text: prompt.text };
|
|
731
|
+
}
|
|
732
|
+
function announceInboxSteer(steer) {
|
|
733
|
+
const taken = announceHostSteer(steer);
|
|
734
|
+
if (taken === undefined)
|
|
735
|
+
return;
|
|
736
|
+
trace(`steer: OpenCode enqueued a mid-step message sessionID=${steer.sessionID} inboxID=${steer.inboxID} ` +
|
|
737
|
+
`chars=${steer.text.length} ${taken ? "injected into the pumping Run" : "left for the next step"}`);
|
|
738
|
+
}
|
|
571
739
|
export default plugin;
|