@playwo/opencode-cursor-oauth 0.0.0-dev.c80ebcb27754 → 0.0.0-dev.d7836f7ad39f
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 +19 -91
- package/dist/auth.js +27 -3
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/cursor/bidi-session.d.ts +13 -0
- package/dist/cursor/bidi-session.js +149 -0
- package/dist/cursor/config.d.ts +4 -0
- package/dist/cursor/config.js +4 -0
- package/dist/cursor/connect-framing.d.ts +10 -0
- package/dist/cursor/connect-framing.js +80 -0
- package/dist/cursor/headers.d.ts +6 -0
- package/dist/cursor/headers.js +16 -0
- package/dist/cursor/index.d.ts +5 -0
- package/dist/cursor/index.js +5 -0
- package/dist/cursor/unary-rpc.d.ts +13 -0
- package/dist/cursor/unary-rpc.js +181 -0
- package/dist/index.d.ts +2 -14
- package/dist/index.js +2 -229
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +147 -0
- package/dist/models.d.ts +3 -0
- package/dist/models.js +80 -54
- package/dist/openai/index.d.ts +3 -0
- package/dist/openai/index.js +3 -0
- package/dist/openai/messages.d.ts +40 -0
- package/dist/openai/messages.js +231 -0
- package/dist/openai/tools.d.ts +7 -0
- package/dist/openai/tools.js +58 -0
- package/dist/openai/types.d.ts +41 -0
- package/dist/openai/types.js +1 -0
- package/dist/plugin/cursor-auth-plugin.d.ts +3 -0
- package/dist/plugin/cursor-auth-plugin.js +140 -0
- package/dist/proto/agent_pb.js +637 -319
- package/dist/provider/index.d.ts +2 -0
- package/dist/provider/index.js +2 -0
- package/dist/provider/model-cost.d.ts +9 -0
- package/dist/provider/model-cost.js +206 -0
- package/dist/provider/models.d.ts +8 -0
- package/dist/provider/models.js +86 -0
- package/dist/proxy/bridge-non-streaming.d.ts +3 -0
- package/dist/proxy/bridge-non-streaming.js +119 -0
- package/dist/proxy/bridge-session.d.ts +5 -0
- package/dist/proxy/bridge-session.js +13 -0
- package/dist/proxy/bridge-streaming.d.ts +5 -0
- package/dist/proxy/bridge-streaming.js +311 -0
- package/dist/proxy/bridge.d.ts +3 -0
- package/dist/proxy/bridge.js +3 -0
- package/dist/proxy/chat-completion.d.ts +2 -0
- package/dist/proxy/chat-completion.js +138 -0
- package/dist/proxy/conversation-meta.d.ts +12 -0
- package/dist/proxy/conversation-meta.js +1 -0
- package/dist/proxy/conversation-state.d.ts +35 -0
- package/dist/proxy/conversation-state.js +95 -0
- package/dist/proxy/cursor-request.d.ts +6 -0
- package/dist/proxy/cursor-request.js +104 -0
- package/dist/proxy/index.d.ts +12 -0
- package/dist/proxy/index.js +12 -0
- package/dist/proxy/server.d.ts +6 -0
- package/dist/proxy/server.js +89 -0
- package/dist/proxy/sse.d.ts +5 -0
- package/dist/proxy/sse.js +5 -0
- package/dist/proxy/state-sync.d.ts +2 -0
- package/dist/proxy/state-sync.js +17 -0
- package/dist/proxy/stream-dispatch.d.ts +42 -0
- package/dist/proxy/stream-dispatch.js +491 -0
- package/dist/proxy/stream-state.d.ts +9 -0
- package/dist/proxy/stream-state.js +1 -0
- package/dist/proxy/title.d.ts +1 -0
- package/dist/proxy/title.js +103 -0
- package/dist/proxy/types.d.ts +27 -0
- package/dist/proxy/types.js +1 -0
- package/dist/proxy.d.ts +2 -19
- package/dist/proxy.js +2 -1221
- package/package.json +1 -1
package/dist/models.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cursor model discovery via GetUsableModels.
|
|
3
|
-
* Uses the H2 bridge for transport. Falls back to a hardcoded list
|
|
4
|
-
* when discovery fails.
|
|
5
|
-
*/
|
|
6
1
|
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
7
2
|
import { z } from "zod";
|
|
8
|
-
import { callCursorUnaryRpc } from "./
|
|
3
|
+
import { callCursorUnaryRpc, decodeConnectUnaryBody } from "./cursor";
|
|
4
|
+
import { errorDetails, logPluginError, logPluginWarn } from "./logger";
|
|
9
5
|
import { GetUsableModelsRequestSchema, GetUsableModelsResponseSchema, } from "./proto/agent_pb";
|
|
10
6
|
const GET_USABLE_MODELS_PATH = "/agent.v1.AgentService/GetUsableModels";
|
|
7
|
+
const MODEL_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
11
8
|
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
12
9
|
const DEFAULT_MAX_TOKENS = 64_000;
|
|
13
10
|
const CursorModelDetailsSchema = z.object({
|
|
@@ -22,24 +19,12 @@ const CursorModelDetailsSchema = z.object({
|
|
|
22
19
|
.transform((aliases) => (aliases ?? []).filter((alias) => typeof alias === "string")),
|
|
23
20
|
thinkingDetails: z.unknown().optional(),
|
|
24
21
|
});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{ id: "claude-4.6-sonnet-medium", name: "Claude 4.6 Sonnet", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
|
|
32
|
-
{ id: "claude-4.5-sonnet", name: "Claude 4.5 Sonnet", reasoning: true, contextWindow: 200_000, maxTokens: 64_000 },
|
|
33
|
-
// GPT models
|
|
34
|
-
{ id: "gpt-5.4-medium", name: "GPT-5.4", reasoning: true, contextWindow: 272_000, maxTokens: 128_000 },
|
|
35
|
-
{ id: "gpt-5.2", name: "GPT-5.2", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 },
|
|
36
|
-
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 },
|
|
37
|
-
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex", reasoning: true, contextWindow: 400_000, maxTokens: 128_000 },
|
|
38
|
-
{ id: "gpt-5.3-codex-spark-preview", name: "GPT-5.3 Codex Spark", reasoning: true, contextWindow: 128_000, maxTokens: 128_000 },
|
|
39
|
-
// Other models
|
|
40
|
-
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", reasoning: true, contextWindow: 1_000_000, maxTokens: 64_000 },
|
|
41
|
-
{ id: "grok-code-fast-1", name: "Grok Code Fast 1", reasoning: false, contextWindow: 128_000, maxTokens: 64_000 },
|
|
42
|
-
];
|
|
22
|
+
export class CursorModelDiscoveryError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "CursorModelDiscoveryError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
43
28
|
async function fetchCursorUsableModels(apiKey) {
|
|
44
29
|
try {
|
|
45
30
|
const requestPayload = create(GetUsableModelsRequestSchema, {});
|
|
@@ -48,18 +33,51 @@ async function fetchCursorUsableModels(apiKey) {
|
|
|
48
33
|
accessToken: apiKey,
|
|
49
34
|
rpcPath: GET_USABLE_MODELS_PATH,
|
|
50
35
|
requestBody,
|
|
36
|
+
timeoutMs: MODEL_DISCOVERY_TIMEOUT_MS,
|
|
51
37
|
});
|
|
52
|
-
if (response.timedOut
|
|
53
|
-
|
|
38
|
+
if (response.timedOut) {
|
|
39
|
+
logPluginError("Cursor model discovery timed out", {
|
|
40
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
41
|
+
timeoutMs: MODEL_DISCOVERY_TIMEOUT_MS,
|
|
42
|
+
});
|
|
43
|
+
throw new CursorModelDiscoveryError(`Cursor model discovery timed out after ${MODEL_DISCOVERY_TIMEOUT_MS}ms.`);
|
|
44
|
+
}
|
|
45
|
+
if (response.exitCode !== 0) {
|
|
46
|
+
logPluginError("Cursor model discovery HTTP failure", {
|
|
47
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
48
|
+
exitCode: response.exitCode,
|
|
49
|
+
responseBody: response.body,
|
|
50
|
+
});
|
|
51
|
+
throw new CursorModelDiscoveryError(buildDiscoveryHttpError(response.exitCode, response.body));
|
|
52
|
+
}
|
|
53
|
+
if (response.body.length === 0) {
|
|
54
|
+
logPluginWarn("Cursor model discovery returned an empty response", {
|
|
55
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
56
|
+
});
|
|
57
|
+
throw new CursorModelDiscoveryError("Cursor model discovery returned an empty response.");
|
|
54
58
|
}
|
|
55
59
|
const decoded = decodeGetUsableModelsResponse(response.body);
|
|
56
|
-
if (!decoded)
|
|
57
|
-
|
|
60
|
+
if (!decoded) {
|
|
61
|
+
logPluginError("Cursor model discovery returned an unreadable response", {
|
|
62
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
63
|
+
responseBody: response.body,
|
|
64
|
+
});
|
|
65
|
+
throw new CursorModelDiscoveryError("Cursor model discovery returned an unreadable response.");
|
|
66
|
+
}
|
|
58
67
|
const models = normalizeCursorModels(decoded.models);
|
|
59
|
-
|
|
68
|
+
if (models.length === 0) {
|
|
69
|
+
throw new CursorModelDiscoveryError("Cursor model discovery returned no usable models.");
|
|
70
|
+
}
|
|
71
|
+
return models;
|
|
60
72
|
}
|
|
61
|
-
catch {
|
|
62
|
-
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof CursorModelDiscoveryError)
|
|
75
|
+
throw error;
|
|
76
|
+
logPluginError("Cursor model discovery crashed", {
|
|
77
|
+
rpcPath: GET_USABLE_MODELS_PATH,
|
|
78
|
+
...errorDetails(error),
|
|
79
|
+
});
|
|
80
|
+
throw new CursorModelDiscoveryError("Cursor model discovery failed.");
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
let cachedModels = null;
|
|
@@ -67,13 +85,43 @@ export async function getCursorModels(apiKey) {
|
|
|
67
85
|
if (cachedModels)
|
|
68
86
|
return cachedModels;
|
|
69
87
|
const discovered = await fetchCursorUsableModels(apiKey);
|
|
70
|
-
cachedModels = discovered
|
|
88
|
+
cachedModels = discovered;
|
|
71
89
|
return cachedModels;
|
|
72
90
|
}
|
|
73
91
|
/** @internal Test-only. */
|
|
74
92
|
export function clearModelCache() {
|
|
75
93
|
cachedModels = null;
|
|
76
94
|
}
|
|
95
|
+
function buildDiscoveryHttpError(exitCode, body) {
|
|
96
|
+
const detail = extractDiscoveryErrorDetail(body);
|
|
97
|
+
const protocolHint = exitCode === 464
|
|
98
|
+
? " Likely protocol mismatch: Cursor appears to expect an HTTP/2 Connect unary request."
|
|
99
|
+
: "";
|
|
100
|
+
if (!detail) {
|
|
101
|
+
return `Cursor model discovery failed with HTTP ${exitCode}.${protocolHint}`;
|
|
102
|
+
}
|
|
103
|
+
return `Cursor model discovery failed with HTTP ${exitCode}: ${detail}.${protocolHint}`;
|
|
104
|
+
}
|
|
105
|
+
function extractDiscoveryErrorDetail(body) {
|
|
106
|
+
if (body.length === 0)
|
|
107
|
+
return null;
|
|
108
|
+
const text = new TextDecoder().decode(body).trim();
|
|
109
|
+
if (!text)
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(text);
|
|
113
|
+
const code = typeof parsed.code === "string" ? parsed.code : undefined;
|
|
114
|
+
const message = typeof parsed.message === "string" ? parsed.message : undefined;
|
|
115
|
+
if (message && code)
|
|
116
|
+
return `${message} (${code})`;
|
|
117
|
+
if (message)
|
|
118
|
+
return message;
|
|
119
|
+
if (code)
|
|
120
|
+
return code;
|
|
121
|
+
}
|
|
122
|
+
catch { }
|
|
123
|
+
return text.length > 200 ? `${text.slice(0, 197)}...` : text;
|
|
124
|
+
}
|
|
77
125
|
function decodeGetUsableModelsResponse(payload) {
|
|
78
126
|
try {
|
|
79
127
|
return fromBinary(GetUsableModelsResponseSchema, payload);
|
|
@@ -90,28 +138,6 @@ function decodeGetUsableModelsResponse(payload) {
|
|
|
90
138
|
}
|
|
91
139
|
}
|
|
92
140
|
}
|
|
93
|
-
function decodeConnectUnaryBody(payload) {
|
|
94
|
-
if (payload.length < 5)
|
|
95
|
-
return null;
|
|
96
|
-
let offset = 0;
|
|
97
|
-
while (offset + 5 <= payload.length) {
|
|
98
|
-
const flags = payload[offset];
|
|
99
|
-
const view = new DataView(payload.buffer, payload.byteOffset + offset, payload.byteLength - offset);
|
|
100
|
-
const messageLength = view.getUint32(1, false);
|
|
101
|
-
const frameEnd = offset + 5 + messageLength;
|
|
102
|
-
if (frameEnd > payload.length)
|
|
103
|
-
return null;
|
|
104
|
-
// Compression flag
|
|
105
|
-
if ((flags & 0b0000_0001) !== 0)
|
|
106
|
-
return null;
|
|
107
|
-
// End-of-stream flag — skip trailer frames
|
|
108
|
-
if ((flags & 0b0000_0010) === 0) {
|
|
109
|
-
return payload.subarray(offset + 5, frameEnd);
|
|
110
|
-
}
|
|
111
|
-
offset = frameEnd;
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
141
|
function normalizeCursorModels(models) {
|
|
116
142
|
if (models.length === 0)
|
|
117
143
|
return [];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ChatCompletionRequest, OpenAIMessage, OpenAIToolCall } from "./types";
|
|
2
|
+
export interface ToolResultInfo {
|
|
3
|
+
toolCallId: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
interface ParsedMessages {
|
|
7
|
+
systemPrompt: string;
|
|
8
|
+
userText: string;
|
|
9
|
+
turns: Array<{
|
|
10
|
+
userText: string;
|
|
11
|
+
assistantText: string;
|
|
12
|
+
}>;
|
|
13
|
+
toolResults: ToolResultInfo[];
|
|
14
|
+
pendingAssistantSummary: string;
|
|
15
|
+
completedTurnsFingerprint: string;
|
|
16
|
+
assistantContinuation: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Normalize OpenAI message content to a plain string. */
|
|
19
|
+
export declare function textContent(content: OpenAIMessage["content"]): string;
|
|
20
|
+
export declare function parseMessages(messages: OpenAIMessage[]): ParsedMessages;
|
|
21
|
+
export declare function formatToolCallSummary(call: OpenAIToolCall): string;
|
|
22
|
+
export declare function formatToolResultSummary(result: ToolResultInfo): string;
|
|
23
|
+
export declare function buildCompletedTurnsFingerprint(systemPrompt: string, turns: Array<{
|
|
24
|
+
userText: string;
|
|
25
|
+
assistantText: string;
|
|
26
|
+
}>): string;
|
|
27
|
+
export declare function buildToolResumePrompt(userText: string, pendingAssistantSummary: string, toolResults: ToolResultInfo[]): string;
|
|
28
|
+
export declare function buildInitialHandoffPrompt(userText: string, turns: Array<{
|
|
29
|
+
userText: string;
|
|
30
|
+
assistantText: string;
|
|
31
|
+
}>, pendingAssistantSummary: string, toolResults: ToolResultInfo[]): string;
|
|
32
|
+
export declare function buildTitleSourceText(userText: string, turns: Array<{
|
|
33
|
+
userText: string;
|
|
34
|
+
assistantText: string;
|
|
35
|
+
}>, pendingAssistantSummary: string, toolResults: ToolResultInfo[]): string;
|
|
36
|
+
export declare function detectTitleRequest(body: ChatCompletionRequest): {
|
|
37
|
+
matched: boolean;
|
|
38
|
+
reason: string;
|
|
39
|
+
};
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { OPENCODE_TITLE_REQUEST_MARKER } from "../constants";
|
|
3
|
+
/** Normalize OpenAI message content to a plain string. */
|
|
4
|
+
export function textContent(content) {
|
|
5
|
+
if (content == null)
|
|
6
|
+
return "";
|
|
7
|
+
if (typeof content === "string")
|
|
8
|
+
return content;
|
|
9
|
+
return content
|
|
10
|
+
.filter((p) => p.type === "text" && p.text)
|
|
11
|
+
.map((p) => p.text)
|
|
12
|
+
.join("\n");
|
|
13
|
+
}
|
|
14
|
+
export function parseMessages(messages) {
|
|
15
|
+
let systemPrompt = "You are a helpful assistant.";
|
|
16
|
+
// Collect system messages
|
|
17
|
+
const systemParts = messages
|
|
18
|
+
.filter((m) => m.role === "system")
|
|
19
|
+
.map((m) => textContent(m.content));
|
|
20
|
+
if (systemParts.length > 0) {
|
|
21
|
+
systemPrompt = systemParts.join("\n");
|
|
22
|
+
}
|
|
23
|
+
const nonSystem = messages.filter((m) => m.role !== "system");
|
|
24
|
+
const parsedTurns = [];
|
|
25
|
+
let currentTurn;
|
|
26
|
+
for (const msg of nonSystem) {
|
|
27
|
+
if (msg.role === "user") {
|
|
28
|
+
if (currentTurn)
|
|
29
|
+
parsedTurns.push(currentTurn);
|
|
30
|
+
currentTurn = {
|
|
31
|
+
userText: textContent(msg.content),
|
|
32
|
+
segments: [],
|
|
33
|
+
};
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (!currentTurn) {
|
|
37
|
+
currentTurn = { userText: "", segments: [] };
|
|
38
|
+
}
|
|
39
|
+
if (msg.role === "assistant") {
|
|
40
|
+
const text = textContent(msg.content);
|
|
41
|
+
if (text) {
|
|
42
|
+
currentTurn.segments.push({ kind: "assistantText", text });
|
|
43
|
+
}
|
|
44
|
+
if (msg.tool_calls?.length) {
|
|
45
|
+
currentTurn.segments.push({
|
|
46
|
+
kind: "assistantToolCalls",
|
|
47
|
+
toolCalls: msg.tool_calls,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (msg.role === "tool") {
|
|
53
|
+
currentTurn.segments.push({
|
|
54
|
+
kind: "toolResult",
|
|
55
|
+
result: {
|
|
56
|
+
toolCallId: msg.tool_call_id ?? "",
|
|
57
|
+
content: textContent(msg.content),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (currentTurn)
|
|
63
|
+
parsedTurns.push(currentTurn);
|
|
64
|
+
let userText = "";
|
|
65
|
+
let toolResults = [];
|
|
66
|
+
let pendingAssistantSummary = "";
|
|
67
|
+
let assistantContinuation = false;
|
|
68
|
+
let completedTurnStates = parsedTurns;
|
|
69
|
+
const lastTurn = parsedTurns.at(-1);
|
|
70
|
+
if (lastTurn) {
|
|
71
|
+
const trailingSegments = splitTrailingToolResults(lastTurn.segments);
|
|
72
|
+
const hasAssistantSummary = trailingSegments.base.length > 0;
|
|
73
|
+
if (trailingSegments.trailing.length > 0 && hasAssistantSummary) {
|
|
74
|
+
completedTurnStates = parsedTurns.slice(0, -1);
|
|
75
|
+
userText = lastTurn.userText;
|
|
76
|
+
toolResults = trailingSegments.trailing.map((segment) => segment.result);
|
|
77
|
+
pendingAssistantSummary = summarizeTurnSegments(trailingSegments.base);
|
|
78
|
+
}
|
|
79
|
+
else if (lastTurn.userText && lastTurn.segments.length === 0) {
|
|
80
|
+
completedTurnStates = parsedTurns.slice(0, -1);
|
|
81
|
+
userText = lastTurn.userText;
|
|
82
|
+
}
|
|
83
|
+
else if (lastTurn.userText && hasAssistantSummary) {
|
|
84
|
+
completedTurnStates = parsedTurns.slice(0, -1);
|
|
85
|
+
userText = lastTurn.userText;
|
|
86
|
+
pendingAssistantSummary = summarizeTurnSegments(lastTurn.segments);
|
|
87
|
+
assistantContinuation = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const turns = completedTurnStates
|
|
91
|
+
.map((turn) => ({
|
|
92
|
+
userText: turn.userText,
|
|
93
|
+
assistantText: summarizeTurnSegments(turn.segments),
|
|
94
|
+
}))
|
|
95
|
+
.filter((turn) => turn.userText || turn.assistantText);
|
|
96
|
+
return {
|
|
97
|
+
systemPrompt,
|
|
98
|
+
userText,
|
|
99
|
+
turns,
|
|
100
|
+
toolResults,
|
|
101
|
+
pendingAssistantSummary,
|
|
102
|
+
completedTurnsFingerprint: buildCompletedTurnsFingerprint(systemPrompt, turns),
|
|
103
|
+
assistantContinuation,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function splitTrailingToolResults(segments) {
|
|
107
|
+
let index = segments.length;
|
|
108
|
+
while (index > 0 && segments[index - 1]?.kind === "toolResult") {
|
|
109
|
+
index -= 1;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
base: segments.slice(0, index),
|
|
113
|
+
trailing: segments
|
|
114
|
+
.slice(index)
|
|
115
|
+
.filter((segment) => segment.kind === "toolResult"),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function summarizeTurnSegments(segments) {
|
|
119
|
+
const parts = [];
|
|
120
|
+
for (const segment of segments) {
|
|
121
|
+
if (segment.kind === "assistantText") {
|
|
122
|
+
const trimmed = segment.text.trim();
|
|
123
|
+
if (trimmed)
|
|
124
|
+
parts.push(trimmed);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (segment.kind === "assistantToolCalls") {
|
|
128
|
+
const summary = segment.toolCalls.map(formatToolCallSummary).join("\n\n");
|
|
129
|
+
if (summary)
|
|
130
|
+
parts.push(summary);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
parts.push(formatToolResultSummary(segment.result));
|
|
134
|
+
}
|
|
135
|
+
return parts.join("\n\n").trim();
|
|
136
|
+
}
|
|
137
|
+
export function formatToolCallSummary(call) {
|
|
138
|
+
const args = call.function.arguments?.trim();
|
|
139
|
+
return args
|
|
140
|
+
? `[assistant requested tool ${call.function.name} id=${call.id}]\n${args}`
|
|
141
|
+
: `[assistant requested tool ${call.function.name} id=${call.id}]`;
|
|
142
|
+
}
|
|
143
|
+
export function formatToolResultSummary(result) {
|
|
144
|
+
const label = result.toolCallId
|
|
145
|
+
? `[tool result id=${result.toolCallId}]`
|
|
146
|
+
: "[tool result]";
|
|
147
|
+
const content = result.content.trim();
|
|
148
|
+
return content ? `${label}\n${content}` : label;
|
|
149
|
+
}
|
|
150
|
+
export function buildCompletedTurnsFingerprint(systemPrompt, turns) {
|
|
151
|
+
return createHash("sha256")
|
|
152
|
+
.update(JSON.stringify({ systemPrompt, turns }))
|
|
153
|
+
.digest("hex");
|
|
154
|
+
}
|
|
155
|
+
export function buildToolResumePrompt(userText, pendingAssistantSummary, toolResults) {
|
|
156
|
+
const parts = [userText.trim()];
|
|
157
|
+
if (pendingAssistantSummary.trim()) {
|
|
158
|
+
parts.push(`[previous assistant tool activity]\n${pendingAssistantSummary.trim()}`);
|
|
159
|
+
}
|
|
160
|
+
if (toolResults.length > 0) {
|
|
161
|
+
parts.push(toolResults.map(formatToolResultSummary).join("\n\n"));
|
|
162
|
+
}
|
|
163
|
+
return parts.filter(Boolean).join("\n\n");
|
|
164
|
+
}
|
|
165
|
+
export function buildInitialHandoffPrompt(userText, turns, pendingAssistantSummary, toolResults) {
|
|
166
|
+
const transcript = turns.map((turn, index) => {
|
|
167
|
+
const sections = [`Turn ${index + 1}`];
|
|
168
|
+
if (turn.userText.trim())
|
|
169
|
+
sections.push(`User: ${turn.userText.trim()}`);
|
|
170
|
+
if (turn.assistantText.trim())
|
|
171
|
+
sections.push(`Assistant: ${turn.assistantText.trim()}`);
|
|
172
|
+
return sections.join("\n");
|
|
173
|
+
});
|
|
174
|
+
const inProgress = buildToolResumePrompt("", pendingAssistantSummary, toolResults).trim();
|
|
175
|
+
const history = [
|
|
176
|
+
...transcript,
|
|
177
|
+
...(inProgress ? [`In-progress turn\n${inProgress}`] : []),
|
|
178
|
+
]
|
|
179
|
+
.join("\n\n")
|
|
180
|
+
.trim();
|
|
181
|
+
if (!history)
|
|
182
|
+
return userText;
|
|
183
|
+
return [
|
|
184
|
+
"[OpenCode session handoff]",
|
|
185
|
+
"You are continuing an existing session that previously ran on another provider/model.",
|
|
186
|
+
"Treat the transcript below as prior conversation history before answering the latest user message.",
|
|
187
|
+
"",
|
|
188
|
+
"<previous-session-transcript>",
|
|
189
|
+
history,
|
|
190
|
+
"</previous-session-transcript>",
|
|
191
|
+
"",
|
|
192
|
+
"Latest user message:",
|
|
193
|
+
userText.trim(),
|
|
194
|
+
]
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.join("\n");
|
|
197
|
+
}
|
|
198
|
+
export function buildTitleSourceText(userText, turns, pendingAssistantSummary, toolResults) {
|
|
199
|
+
const history = turns
|
|
200
|
+
.map((turn) => [
|
|
201
|
+
isTitleRequestMarker(turn.userText) ? "" : turn.userText.trim(),
|
|
202
|
+
turn.assistantText.trim(),
|
|
203
|
+
]
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.join("\n"))
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
if (pendingAssistantSummary.trim()) {
|
|
208
|
+
history.push(pendingAssistantSummary.trim());
|
|
209
|
+
}
|
|
210
|
+
if (toolResults.length > 0) {
|
|
211
|
+
history.push(toolResults.map(formatToolResultSummary).join("\n\n"));
|
|
212
|
+
}
|
|
213
|
+
if (userText.trim() && !isTitleRequestMarker(userText)) {
|
|
214
|
+
history.push(userText.trim());
|
|
215
|
+
}
|
|
216
|
+
return history.join("\n\n").trim();
|
|
217
|
+
}
|
|
218
|
+
export function detectTitleRequest(body) {
|
|
219
|
+
if ((body.tools?.length ?? 0) > 0) {
|
|
220
|
+
return { matched: false, reason: "tools-present" };
|
|
221
|
+
}
|
|
222
|
+
const firstNonSystem = body.messages.find((message) => message.role !== "system");
|
|
223
|
+
if (firstNonSystem?.role === "user" &&
|
|
224
|
+
isTitleRequestMarker(textContent(firstNonSystem.content))) {
|
|
225
|
+
return { matched: true, reason: "opencode-title-marker" };
|
|
226
|
+
}
|
|
227
|
+
return { matched: false, reason: "no-title-marker" };
|
|
228
|
+
}
|
|
229
|
+
function isTitleRequestMarker(text) {
|
|
230
|
+
return text.trim() === OPENCODE_TITLE_REQUEST_MARKER;
|
|
231
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { McpToolDefinition } from "../proto/agent_pb";
|
|
2
|
+
import type { OpenAIToolDef } from "./types";
|
|
3
|
+
export declare function selectToolsForChoice(tools: OpenAIToolDef[], toolChoice: unknown): OpenAIToolDef[];
|
|
4
|
+
/** Convert OpenAI tool definitions to Cursor's MCP tool protobuf format. */
|
|
5
|
+
export declare function buildMcpToolDefinitions(tools: OpenAIToolDef[]): McpToolDefinition[];
|
|
6
|
+
/** Decode a map of MCP arg values. */
|
|
7
|
+
export declare function decodeMcpArgsMap(args: Record<string, Uint8Array>): Record<string, unknown>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { create, fromBinary, fromJson, toBinary, toJson, } from "@bufbuild/protobuf";
|
|
2
|
+
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
3
|
+
import { McpToolDefinitionSchema } from "../proto/agent_pb";
|
|
4
|
+
export function selectToolsForChoice(tools, toolChoice) {
|
|
5
|
+
if (!tools.length)
|
|
6
|
+
return [];
|
|
7
|
+
if (toolChoice === undefined ||
|
|
8
|
+
toolChoice === null ||
|
|
9
|
+
toolChoice === "auto" ||
|
|
10
|
+
toolChoice === "required") {
|
|
11
|
+
return tools;
|
|
12
|
+
}
|
|
13
|
+
if (toolChoice === "none") {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
if (typeof toolChoice === "object") {
|
|
17
|
+
const choice = toolChoice;
|
|
18
|
+
if (choice.type === "function" &&
|
|
19
|
+
typeof choice.function?.name === "string") {
|
|
20
|
+
return tools.filter((tool) => tool.function.name === choice.function.name);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return tools;
|
|
24
|
+
}
|
|
25
|
+
/** Convert OpenAI tool definitions to Cursor's MCP tool protobuf format. */
|
|
26
|
+
export function buildMcpToolDefinitions(tools) {
|
|
27
|
+
return tools.map((t) => {
|
|
28
|
+
const fn = t.function;
|
|
29
|
+
const jsonSchema = fn.parameters && typeof fn.parameters === "object"
|
|
30
|
+
? fn.parameters
|
|
31
|
+
: { type: "object", properties: {}, required: [] };
|
|
32
|
+
const inputSchema = toBinary(ValueSchema, fromJson(ValueSchema, jsonSchema));
|
|
33
|
+
return create(McpToolDefinitionSchema, {
|
|
34
|
+
name: fn.name,
|
|
35
|
+
description: fn.description || "",
|
|
36
|
+
providerIdentifier: "opencode",
|
|
37
|
+
toolName: fn.name,
|
|
38
|
+
inputSchema,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Decode a Cursor MCP arg value (protobuf Value bytes) to a JS value. */
|
|
43
|
+
function decodeMcpArgValue(value) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = fromBinary(ValueSchema, value);
|
|
46
|
+
return toJson(ValueSchema, parsed);
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
return new TextDecoder().decode(value);
|
|
50
|
+
}
|
|
51
|
+
/** Decode a map of MCP arg values. */
|
|
52
|
+
export function decodeMcpArgsMap(args) {
|
|
53
|
+
const decoded = {};
|
|
54
|
+
for (const [key, value] of Object.entries(args)) {
|
|
55
|
+
decoded[key] = decodeMcpArgValue(value);
|
|
56
|
+
}
|
|
57
|
+
return decoded;
|
|
58
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface OpenAIToolCall {
|
|
2
|
+
id: string;
|
|
3
|
+
type: "function";
|
|
4
|
+
function: {
|
|
5
|
+
name: string;
|
|
6
|
+
arguments: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** A single element in an OpenAI multi-part content array. */
|
|
10
|
+
export interface ContentPart {
|
|
11
|
+
type: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface OpenAIMessage {
|
|
15
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
16
|
+
content: string | null | ContentPart[];
|
|
17
|
+
tool_call_id?: string;
|
|
18
|
+
tool_calls?: OpenAIToolCall[];
|
|
19
|
+
}
|
|
20
|
+
export interface OpenAIToolDef {
|
|
21
|
+
type: "function";
|
|
22
|
+
function: {
|
|
23
|
+
name: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
parameters?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface ChatCompletionRequest {
|
|
29
|
+
model: string;
|
|
30
|
+
messages: OpenAIMessage[];
|
|
31
|
+
stream?: boolean;
|
|
32
|
+
temperature?: number;
|
|
33
|
+
max_tokens?: number;
|
|
34
|
+
max_completion_tokens?: number;
|
|
35
|
+
tools?: OpenAIToolDef[];
|
|
36
|
+
tool_choice?: unknown;
|
|
37
|
+
}
|
|
38
|
+
export interface ChatRequestContext {
|
|
39
|
+
sessionId?: string;
|
|
40
|
+
agentKey?: string;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|