@gajae-code/ai 0.1.1
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 +2644 -0
- package/README.md +1181 -0
- package/dist/types/api-registry.d.ts +30 -0
- package/dist/types/auth-broker/client.d.ts +66 -0
- package/dist/types/auth-broker/index.d.ts +5 -0
- package/dist/types/auth-broker/refresher.d.ts +25 -0
- package/dist/types/auth-broker/remote-store.d.ts +96 -0
- package/dist/types/auth-broker/server.d.ts +32 -0
- package/dist/types/auth-broker/types.d.ts +105 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +412 -0
- package/dist/types/auth-gateway/http.d.ts +39 -0
- package/dist/types/auth-gateway/index.d.ts +3 -0
- package/dist/types/auth-gateway/server.d.ts +17 -0
- package/dist/types/auth-gateway/types.d.ts +115 -0
- package/dist/types/auth-storage.d.ts +641 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/index.d.ts +49 -0
- package/dist/types/model-cache.d.ts +17 -0
- package/dist/types/model-manager.d.ts +62 -0
- package/dist/types/model-thinking.d.ts +71 -0
- package/dist/types/models.d.ts +12 -0
- package/dist/types/provider-details.d.ts +24 -0
- package/dist/types/provider-models/bundled-references.d.ts +4 -0
- package/dist/types/provider-models/descriptors.d.ts +48 -0
- package/dist/types/provider-models/google.d.ts +20 -0
- package/dist/types/provider-models/index.d.ts +5 -0
- package/dist/types/provider-models/ollama.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +237 -0
- package/dist/types/provider-models/special.d.ts +16 -0
- package/dist/types/providers/amazon-bedrock.d.ts +36 -0
- package/dist/types/providers/anthropic-messages-server-schema.d.ts +450 -0
- package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
- package/dist/types/providers/anthropic.d.ts +188 -0
- package/dist/types/providers/aws-credentials.d.ts +43 -0
- package/dist/types/providers/aws-eventstream.d.ts +38 -0
- package/dist/types/providers/aws-sigv4.d.ts +55 -0
- package/dist/types/providers/azure-openai-responses.d.ts +15 -0
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +13022 -0
- package/dist/types/providers/cursor.d.ts +42 -0
- package/dist/types/providers/error-message.d.ts +27 -0
- package/dist/types/providers/github-copilot-headers.d.ts +40 -0
- package/dist/types/providers/gitlab-duo.d.ts +27 -0
- package/dist/types/providers/google-auth.d.ts +24 -0
- package/dist/types/providers/google-gemini-cli.d.ts +72 -0
- package/dist/types/providers/google-gemini-headers.d.ts +18 -0
- package/dist/types/providers/google-shared.d.ts +163 -0
- package/dist/types/providers/google-types.d.ts +138 -0
- package/dist/types/providers/google-vertex.d.ts +7 -0
- package/dist/types/providers/google.d.ts +4 -0
- package/dist/types/providers/grammar.d.ts +1 -0
- package/dist/types/providers/kimi.d.ts +27 -0
- package/dist/types/providers/mock.d.ts +175 -0
- package/dist/types/providers/ollama.d.ts +6 -0
- package/dist/types/providers/openai-anthropic-shim.d.ts +31 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +814 -0
- package/dist/types/providers/openai-chat-server.d.ts +16 -0
- package/dist/types/providers/openai-codex/constants.d.ts +26 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +49 -0
- package/dist/types/providers/openai-codex/response-handler.d.ts +17 -0
- package/dist/types/providers/openai-codex-responses.d.ts +67 -0
- package/dist/types/providers/openai-completions-compat.d.ts +25 -0
- package/dist/types/providers/openai-completions.d.ts +33 -0
- package/dist/types/providers/openai-responses-server-schema.d.ts +392 -0
- package/dist/types/providers/openai-responses-server.d.ts +17 -0
- package/dist/types/providers/openai-responses-shared.d.ts +89 -0
- package/dist/types/providers/openai-responses.d.ts +32 -0
- package/dist/types/providers/pi-native-client.d.ts +13 -0
- package/dist/types/providers/pi-native-server.d.ts +68 -0
- package/dist/types/providers/register-builtins.d.ts +31 -0
- package/dist/types/providers/synthetic.d.ts +26 -0
- package/dist/types/providers/transform-messages.d.ts +12 -0
- package/dist/types/providers/vision-guard.d.ts +8 -0
- package/dist/types/rate-limit-utils.d.ts +19 -0
- package/dist/types/stream.d.ts +24 -0
- package/dist/types/types.d.ts +746 -0
- package/dist/types/usage/claude.d.ts +3 -0
- package/dist/types/usage/gemini.d.ts +2 -0
- package/dist/types/usage/github-copilot.d.ts +7 -0
- package/dist/types/usage/google-antigravity.d.ts +2 -0
- package/dist/types/usage/kimi.d.ts +2 -0
- package/dist/types/usage/minimax-code.d.ts +2 -0
- package/dist/types/usage/openai-codex.d.ts +3 -0
- package/dist/types/usage/shared.d.ts +1 -0
- package/dist/types/usage/zai.d.ts +2 -0
- package/dist/types/usage.d.ts +258 -0
- package/dist/types/utils/abort.d.ts +19 -0
- package/dist/types/utils/anthropic-auth.d.ts +31 -0
- package/dist/types/utils/discovery/antigravity.d.ts +61 -0
- package/dist/types/utils/discovery/codex.d.ts +38 -0
- package/dist/types/utils/discovery/cursor.d.ts +23 -0
- package/dist/types/utils/discovery/gemini.d.ts +25 -0
- package/dist/types/utils/discovery/index.d.ts +4 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +72 -0
- package/dist/types/utils/event-stream.d.ts +28 -0
- package/dist/types/utils/fireworks-model-id.d.ts +10 -0
- package/dist/types/utils/foundry.d.ts +1 -0
- package/dist/types/utils/h2-fetch.d.ts +22 -0
- package/dist/types/utils/http-inspector.d.ts +31 -0
- package/dist/types/utils/idle-iterator.d.ts +67 -0
- package/dist/types/utils/json-parse.d.ts +10 -0
- package/dist/types/utils/oauth/alibaba-coding-plan.d.ts +18 -0
- package/dist/types/utils/oauth/anthropic.d.ts +22 -0
- package/dist/types/utils/oauth/api-key-login.d.ts +35 -0
- package/dist/types/utils/oauth/api-key-validation.d.ts +27 -0
- package/dist/types/utils/oauth/callback-server.d.ts +57 -0
- package/dist/types/utils/oauth/cerebras.d.ts +1 -0
- package/dist/types/utils/oauth/cloudflare-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/cursor.d.ts +15 -0
- package/dist/types/utils/oauth/deepseek.d.ts +10 -0
- package/dist/types/utils/oauth/firepass.d.ts +1 -0
- package/dist/types/utils/oauth/fireworks.d.ts +1 -0
- package/dist/types/utils/oauth/github-copilot.d.ts +38 -0
- package/dist/types/utils/oauth/gitlab-duo.d.ts +3 -0
- package/dist/types/utils/oauth/google-antigravity.d.ts +11 -0
- package/dist/types/utils/oauth/google-gemini-cli.d.ts +10 -0
- package/dist/types/utils/oauth/google-oauth-shared.d.ts +28 -0
- package/dist/types/utils/oauth/huggingface.d.ts +19 -0
- package/dist/types/utils/oauth/index.d.ts +38 -0
- package/dist/types/utils/oauth/kagi.d.ts +17 -0
- package/dist/types/utils/oauth/kilo.d.ts +5 -0
- package/dist/types/utils/oauth/kimi.d.ts +21 -0
- package/dist/types/utils/oauth/litellm.d.ts +18 -0
- package/dist/types/utils/oauth/lm-studio.d.ts +17 -0
- package/dist/types/utils/oauth/minimax-code.d.ts +28 -0
- package/dist/types/utils/oauth/moonshot.d.ts +1 -0
- package/dist/types/utils/oauth/nanogpt.d.ts +1 -0
- package/dist/types/utils/oauth/nvidia.d.ts +18 -0
- package/dist/types/utils/oauth/ollama-cloud.d.ts +2 -0
- package/dist/types/utils/oauth/ollama.d.ts +18 -0
- package/dist/types/utils/oauth/openai-codex.d.ts +21 -0
- package/dist/types/utils/oauth/opencode.d.ts +18 -0
- package/dist/types/utils/oauth/parallel.d.ts +17 -0
- package/dist/types/utils/oauth/perplexity.d.ts +9 -0
- package/dist/types/utils/oauth/pkce.d.ts +8 -0
- package/dist/types/utils/oauth/qianfan.d.ts +17 -0
- package/dist/types/utils/oauth/qwen-portal.d.ts +19 -0
- package/dist/types/utils/oauth/synthetic.d.ts +1 -0
- package/dist/types/utils/oauth/tavily.d.ts +17 -0
- package/dist/types/utils/oauth/together.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +44 -0
- package/dist/types/utils/oauth/venice.d.ts +18 -0
- package/dist/types/utils/oauth/vercel-ai-gateway.d.ts +18 -0
- package/dist/types/utils/oauth/vllm.d.ts +16 -0
- package/dist/types/utils/oauth/xiaomi.d.ts +19 -0
- package/dist/types/utils/oauth/zai.d.ts +18 -0
- package/dist/types/utils/oauth/zenmux.d.ts +1 -0
- package/dist/types/utils/overflow.d.ts +54 -0
- package/dist/types/utils/parse-bind.d.ts +23 -0
- package/dist/types/utils/provider-response.d.ts +3 -0
- package/dist/types/utils/retry-after.d.ts +3 -0
- package/dist/types/utils/retry.d.ts +26 -0
- package/dist/types/utils/schema/adapt.d.ts +24 -0
- package/dist/types/utils/schema/compatibility.d.ts +30 -0
- package/dist/types/utils/schema/dereference.d.ts +11 -0
- package/dist/types/utils/schema/draft.d.ts +10 -0
- package/dist/types/utils/schema/equality.d.ts +4 -0
- package/dist/types/utils/schema/fields.d.ts +49 -0
- package/dist/types/utils/schema/index.d.ts +13 -0
- package/dist/types/utils/schema/json-schema-validator.d.ts +12 -0
- package/dist/types/utils/schema/meta-validator.d.ts +2 -0
- package/dist/types/utils/schema/normalize.d.ts +93 -0
- package/dist/types/utils/schema/spill.d.ts +8 -0
- package/dist/types/utils/schema/stamps.d.ts +25 -0
- package/dist/types/utils/schema/types.d.ts +4 -0
- package/dist/types/utils/schema/wire.d.ts +54 -0
- package/dist/types/utils/schema/zod-decontaminate.d.ts +31 -0
- package/dist/types/utils/sse-debug.d.ts +10 -0
- package/dist/types/utils/tool-call-healing.d.ts +71 -0
- package/dist/types/utils/tool-choice.d.ts +50 -0
- package/dist/types/utils/validation.d.ts +17 -0
- package/dist/types/utils.d.ts +28 -0
- package/package.json +146 -0
- package/src/api-registry.ts +96 -0
- package/src/auth-broker/client.ts +358 -0
- package/src/auth-broker/index.ts +5 -0
- package/src/auth-broker/refresher.ts +127 -0
- package/src/auth-broker/remote-store.ts +623 -0
- package/src/auth-broker/server.ts +644 -0
- package/src/auth-broker/types.ts +127 -0
- package/src/auth-broker/wire-schemas.ts +200 -0
- package/src/auth-gateway/http.ts +194 -0
- package/src/auth-gateway/index.ts +3 -0
- package/src/auth-gateway/server.ts +717 -0
- package/src/auth-gateway/types.ts +134 -0
- package/src/auth-storage.ts +4104 -0
- package/src/cli.ts +262 -0
- package/src/index.ts +54 -0
- package/src/model-cache.ts +129 -0
- package/src/model-manager.ts +450 -0
- package/src/model-thinking.ts +691 -0
- package/src/models.json +73853 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +90 -0
- package/src/provider-models/bundled-references.ts +38 -0
- package/src/provider-models/descriptors.ts +308 -0
- package/src/provider-models/google.ts +91 -0
- package/src/provider-models/index.ts +5 -0
- package/src/provider-models/ollama.ts +153 -0
- package/src/provider-models/openai-compat.ts +2275 -0
- package/src/provider-models/special.ts +67 -0
- package/src/providers/amazon-bedrock.ts +849 -0
- package/src/providers/anthropic-messages-server-schema.ts +229 -0
- package/src/providers/anthropic-messages-server.ts +677 -0
- package/src/providers/anthropic.ts +2696 -0
- package/src/providers/aws-credentials.ts +501 -0
- package/src/providers/aws-eventstream.ts +185 -0
- package/src/providers/aws-sigv4.ts +218 -0
- package/src/providers/azure-openai-responses.ts +337 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2561 -0
- package/src/providers/error-message.ts +21 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +372 -0
- package/src/providers/google-auth.ts +252 -0
- package/src/providers/google-gemini-cli.ts +795 -0
- package/src/providers/google-gemini-headers.ts +41 -0
- package/src/providers/google-shared.ts +902 -0
- package/src/providers/google-types.ts +167 -0
- package/src/providers/google-vertex.ts +88 -0
- package/src/providers/google.ts +41 -0
- package/src/providers/grammar.ts +70 -0
- package/src/providers/kimi.ts +52 -0
- package/src/providers/mock.ts +500 -0
- package/src/providers/ollama.ts +544 -0
- package/src/providers/openai-anthropic-shim.ts +138 -0
- package/src/providers/openai-chat-server-schema.ts +243 -0
- package/src/providers/openai-chat-server.ts +628 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +161 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2598 -0
- package/src/providers/openai-completions-compat.ts +279 -0
- package/src/providers/openai-completions.ts +1853 -0
- package/src/providers/openai-responses-server-schema.ts +290 -0
- package/src/providers/openai-responses-server.ts +1183 -0
- package/src/providers/openai-responses-shared.ts +800 -0
- package/src/providers/openai-responses.ts +621 -0
- package/src/providers/pi-native-client.ts +228 -0
- package/src/providers/pi-native-server.ts +210 -0
- package/src/providers/register-builtins.ts +412 -0
- package/src/providers/synthetic.ts +50 -0
- package/src/providers/transform-messages.ts +309 -0
- package/src/providers/vision-guard.ts +31 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +895 -0
- package/src/types.ts +884 -0
- package/src/usage/claude.ts +431 -0
- package/src/usage/gemini.ts +250 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +201 -0
- package/src/usage/kimi.ts +271 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +503 -0
- package/src/usage/shared.ts +10 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +183 -0
- package/src/utils/abort.ts +51 -0
- package/src/utils/anthropic-auth.ts +87 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +4 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +142 -0
- package/src/utils/fireworks-model-id.ts +30 -0
- package/src/utils/foundry.ts +8 -0
- package/src/utils/h2-fetch.ts +60 -0
- package/src/utils/http-inspector.ts +176 -0
- package/src/utils/idle-iterator.ts +250 -0
- package/src/utils/json-parse.ts +148 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +200 -0
- package/src/utils/oauth/api-key-login.ts +87 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +16 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/deepseek.ts +53 -0
- package/src/utils/oauth/firepass.ts +24 -0
- package/src/utils/oauth/fireworks.ts +15 -0
- package/src/utils/oauth/github-copilot.ts +362 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +200 -0
- package/src/utils/oauth/google-gemini-cli.ts +256 -0
- package/src/utils/oauth/google-oauth-shared.ts +110 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +444 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +254 -0
- package/src/utils/oauth/litellm.ts +47 -0
- package/src/utils/oauth/lm-studio.ts +38 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +16 -0
- package/src/utils/oauth/nanogpt.ts +15 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama-cloud.ts +28 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +299 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +206 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +16 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +16 -0
- package/src/utils/oauth/types.ts +94 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +137 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +15 -0
- package/src/utils/overflow.ts +137 -0
- package/src/utils/parse-bind.ts +54 -0
- package/src/utils/provider-response.ts +30 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +54 -0
- package/src/utils/schema/CONSTRAINTS.md +164 -0
- package/src/utils/schema/adapt.ts +36 -0
- package/src/utils/schema/compatibility.ts +435 -0
- package/src/utils/schema/dereference.ts +98 -0
- package/src/utils/schema/draft.ts +341 -0
- package/src/utils/schema/equality.ts +97 -0
- package/src/utils/schema/fields.ts +190 -0
- package/src/utils/schema/index.ts +13 -0
- package/src/utils/schema/json-schema-validator.ts +577 -0
- package/src/utils/schema/meta-validator.ts +167 -0
- package/src/utils/schema/normalize.ts +1588 -0
- package/src/utils/schema/spill.ts +43 -0
- package/src/utils/schema/stamps.ts +97 -0
- package/src/utils/schema/types.ts +11 -0
- package/src/utils/schema/wire.ts +213 -0
- package/src/utils/schema/zod-decontaminate.ts +331 -0
- package/src/utils/sse-debug.ts +289 -0
- package/src/utils/tool-call-healing.ts +271 -0
- package/src/utils/tool-choice.ts +99 -0
- package/src/utils/validation.ts +1019 -0
- package/src/utils.ts +166 -0
|
@@ -0,0 +1,2561 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import http2 from "node:http2";
|
|
4
|
+
import { create, fromBinary, fromJson, type JsonValue, toBinary, toJson } from "@bufbuild/protobuf";
|
|
5
|
+
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
6
|
+
import { $env, extractHttpStatusFromError, sanitizeText } from "@gajae-code/utils";
|
|
7
|
+
import { calculateCost } from "../models";
|
|
8
|
+
import type {
|
|
9
|
+
Api,
|
|
10
|
+
AssistantMessage,
|
|
11
|
+
Context,
|
|
12
|
+
CursorExecHandlerResult,
|
|
13
|
+
CursorExecHandlers,
|
|
14
|
+
CursorMcpCall,
|
|
15
|
+
CursorShellStreamCallbacks,
|
|
16
|
+
CursorToolResultHandler,
|
|
17
|
+
ImageContent,
|
|
18
|
+
Message,
|
|
19
|
+
Model,
|
|
20
|
+
StreamFunction,
|
|
21
|
+
StreamOptions,
|
|
22
|
+
TextContent,
|
|
23
|
+
ThinkingContent,
|
|
24
|
+
Tool,
|
|
25
|
+
ToolCall,
|
|
26
|
+
ToolResultMessage,
|
|
27
|
+
} from "../types";
|
|
28
|
+
import { normalizeSystemPrompts } from "../utils";
|
|
29
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
30
|
+
import { parseStreamingJson } from "../utils/json-parse";
|
|
31
|
+
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
32
|
+
import { toolWireSchema } from "../utils/schema/wire";
|
|
33
|
+
import type { McpToolDefinition } from "./cursor/gen/agent_pb";
|
|
34
|
+
import {
|
|
35
|
+
AgentClientMessageSchema,
|
|
36
|
+
AgentConversationTurnStructureSchema,
|
|
37
|
+
AgentRunRequestSchema,
|
|
38
|
+
type AgentServerMessage,
|
|
39
|
+
AgentServerMessageSchema,
|
|
40
|
+
AssistantMessageSchema,
|
|
41
|
+
BackgroundShellSpawnResultSchema,
|
|
42
|
+
ClientHeartbeatSchema,
|
|
43
|
+
ComputerUseResultSchema,
|
|
44
|
+
ConversationActionSchema,
|
|
45
|
+
type ConversationStateStructure,
|
|
46
|
+
ConversationStateStructureSchema,
|
|
47
|
+
ConversationStepSchema,
|
|
48
|
+
ConversationTurnStructureSchema,
|
|
49
|
+
DeleteErrorSchema,
|
|
50
|
+
DeleteRejectedSchema,
|
|
51
|
+
DeleteResultSchema,
|
|
52
|
+
DeleteSuccessSchema,
|
|
53
|
+
DiagnosticsErrorSchema,
|
|
54
|
+
DiagnosticsRejectedSchema,
|
|
55
|
+
DiagnosticsResultSchema,
|
|
56
|
+
DiagnosticsSuccessSchema,
|
|
57
|
+
ExecClientControlMessageSchema,
|
|
58
|
+
type ExecClientMessage,
|
|
59
|
+
ExecClientMessageSchema,
|
|
60
|
+
ExecClientStreamCloseSchema,
|
|
61
|
+
type ExecServerMessage,
|
|
62
|
+
FetchErrorSchema,
|
|
63
|
+
FetchResultSchema,
|
|
64
|
+
GetBlobResultSchema,
|
|
65
|
+
GrepContentMatchSchema,
|
|
66
|
+
GrepContentResultSchema,
|
|
67
|
+
GrepCountResultSchema,
|
|
68
|
+
GrepErrorSchema,
|
|
69
|
+
type GrepFileCount,
|
|
70
|
+
GrepFileCountSchema,
|
|
71
|
+
GrepFileMatchSchema,
|
|
72
|
+
GrepFilesResultSchema,
|
|
73
|
+
GrepResultSchema,
|
|
74
|
+
GrepSuccessSchema,
|
|
75
|
+
type GrepUnionResult,
|
|
76
|
+
GrepUnionResultSchema,
|
|
77
|
+
KvClientMessageSchema,
|
|
78
|
+
type KvServerMessage,
|
|
79
|
+
ListMcpResourcesExecResultSchema,
|
|
80
|
+
type LsDirectoryTreeNode,
|
|
81
|
+
type LsDirectoryTreeNode_File,
|
|
82
|
+
LsDirectoryTreeNode_FileSchema,
|
|
83
|
+
LsDirectoryTreeNodeSchema,
|
|
84
|
+
LsErrorSchema,
|
|
85
|
+
LsRejectedSchema,
|
|
86
|
+
LsResultSchema,
|
|
87
|
+
LsSuccessSchema,
|
|
88
|
+
McpErrorSchema,
|
|
89
|
+
McpImageContentSchema,
|
|
90
|
+
McpResultSchema,
|
|
91
|
+
McpSuccessSchema,
|
|
92
|
+
McpTextContentSchema,
|
|
93
|
+
McpToolDefinitionSchema,
|
|
94
|
+
McpToolNotFoundSchema,
|
|
95
|
+
McpToolResultContentItemSchema,
|
|
96
|
+
ModelDetailsSchema,
|
|
97
|
+
ReadErrorSchema,
|
|
98
|
+
ReadMcpResourceExecResultSchema,
|
|
99
|
+
ReadRejectedSchema,
|
|
100
|
+
ReadResultSchema,
|
|
101
|
+
ReadSuccessSchema,
|
|
102
|
+
RecordScreenResultSchema,
|
|
103
|
+
RequestContextResultSchema,
|
|
104
|
+
RequestContextSchema,
|
|
105
|
+
RequestContextSuccessSchema,
|
|
106
|
+
ResumeActionSchema,
|
|
107
|
+
SelectedContextSchema,
|
|
108
|
+
SelectedImageSchema,
|
|
109
|
+
SetBlobResultSchema,
|
|
110
|
+
type ShellArgs,
|
|
111
|
+
ShellFailureSchema,
|
|
112
|
+
ShellRejectedSchema,
|
|
113
|
+
type ShellResult,
|
|
114
|
+
ShellResultSchema,
|
|
115
|
+
type ShellStream,
|
|
116
|
+
ShellStreamExitSchema,
|
|
117
|
+
ShellStreamSchema,
|
|
118
|
+
ShellStreamStartSchema,
|
|
119
|
+
ShellStreamStderrSchema,
|
|
120
|
+
ShellStreamStdoutSchema,
|
|
121
|
+
ShellSuccessSchema,
|
|
122
|
+
UserMessageActionSchema,
|
|
123
|
+
UserMessageSchema,
|
|
124
|
+
WriteErrorSchema,
|
|
125
|
+
WriteRejectedSchema,
|
|
126
|
+
WriteResultSchema,
|
|
127
|
+
WriteShellStdinErrorSchema,
|
|
128
|
+
WriteShellStdinResultSchema,
|
|
129
|
+
WriteSuccessSchema,
|
|
130
|
+
} from "./cursor/gen/agent_pb";
|
|
131
|
+
|
|
132
|
+
export const CURSOR_API_URL = "https://api2.cursor.sh";
|
|
133
|
+
export const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f";
|
|
134
|
+
|
|
135
|
+
const conversationStateCache = new Map<string, ConversationStateStructure>();
|
|
136
|
+
const conversationBlobStores = new Map<string, Map<string, Uint8Array>>();
|
|
137
|
+
|
|
138
|
+
export interface CursorOptions extends StreamOptions {
|
|
139
|
+
customSystemPrompt?: string;
|
|
140
|
+
conversationId?: string;
|
|
141
|
+
execHandlers?: CursorExecHandlers;
|
|
142
|
+
onToolResult?: CursorToolResultHandler;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const CONNECT_END_STREAM_FLAG = 0b00000010;
|
|
146
|
+
|
|
147
|
+
interface CursorLogEntry {
|
|
148
|
+
ts: number;
|
|
149
|
+
type: string;
|
|
150
|
+
subtype?: string;
|
|
151
|
+
data?: unknown;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function appendCursorDebugLog(entry: CursorLogEntry): Promise<void> {
|
|
155
|
+
const logPath = $env.DEBUG_CURSOR_LOG;
|
|
156
|
+
if (!logPath) return;
|
|
157
|
+
try {
|
|
158
|
+
await fs.appendFile(logPath, `${JSON.stringify(entry, debugReplacer)}\n`);
|
|
159
|
+
} catch {
|
|
160
|
+
// Ignore debug log failures
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function log(type: string, subtype?: string, data?: unknown): void {
|
|
165
|
+
if (!$env.DEBUG_CURSOR) return;
|
|
166
|
+
const normalizedData = data ? decodeLogData(data) : data;
|
|
167
|
+
const entry: CursorLogEntry = { ts: Date.now(), type, subtype, data: normalizedData };
|
|
168
|
+
const verbose = $env.DEBUG_CURSOR === "2" || $env.DEBUG_CURSOR === "verbose";
|
|
169
|
+
const dataStr = verbose && normalizedData ? ` ${JSON.stringify(normalizedData, debugReplacer)?.slice(0, 500)}` : "";
|
|
170
|
+
console.error(`[CURSOR] ${type}${subtype ? `: ${subtype}` : ""}${dataStr}`);
|
|
171
|
+
void appendCursorDebugLog(entry);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function frameConnectMessage(data: Uint8Array, flags = 0): Buffer {
|
|
175
|
+
const frame = Buffer.alloc(5 + data.length);
|
|
176
|
+
frame[0] = flags;
|
|
177
|
+
frame.writeUInt32BE(data.length, 1);
|
|
178
|
+
frame.set(data, 5);
|
|
179
|
+
return frame;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseConnectEndStream(data: Uint8Array): Error | null {
|
|
183
|
+
try {
|
|
184
|
+
const payload = JSON.parse(new TextDecoder().decode(data));
|
|
185
|
+
const error = payload?.error;
|
|
186
|
+
if (error) {
|
|
187
|
+
const code = typeof error.code === "string" ? error.code : "unknown";
|
|
188
|
+
const message = typeof error.message === "string" ? error.message : "Unknown error";
|
|
189
|
+
return new Error(`Connect error ${code}: ${message}`);
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
} catch {
|
|
193
|
+
return new Error("Failed to parse Connect end stream");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function debugBytes(bytes: Uint8Array, asHex: boolean): string {
|
|
198
|
+
if (asHex) {
|
|
199
|
+
return Buffer.from(bytes).toString("hex");
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
203
|
+
if (/^[\x20-\x7E\s]*$/.test(text)) return text;
|
|
204
|
+
} catch {}
|
|
205
|
+
return Buffer.from(bytes).toString("hex");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function debugReplacer(key: string, value: unknown): unknown {
|
|
209
|
+
if (
|
|
210
|
+
value instanceof Uint8Array ||
|
|
211
|
+
(value && typeof value === "object" && "type" in value && value.type === "Buffer")
|
|
212
|
+
) {
|
|
213
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array((value as any).data);
|
|
214
|
+
const asHex = key === "blobId" || key === "blob_id" || key.endsWith("Id") || key.endsWith("_id");
|
|
215
|
+
return debugBytes(bytes, asHex);
|
|
216
|
+
}
|
|
217
|
+
if (typeof value === "bigint") return value.toString();
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function extractLogBytes(value: unknown): Uint8Array | null {
|
|
222
|
+
if (value instanceof Uint8Array) {
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
if (value && typeof value === "object" && "type" in value && value.type === "Buffer") {
|
|
226
|
+
const data = (value as { data?: number[] }).data;
|
|
227
|
+
if (Array.isArray(data)) {
|
|
228
|
+
return new Uint8Array(data);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function decodeMcpArgsForLog(args?: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
235
|
+
if (!args) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
let mutated = false;
|
|
239
|
+
const decoded: Record<string, unknown> = {};
|
|
240
|
+
for (const [key, value] of Object.entries(args)) {
|
|
241
|
+
const bytes = extractLogBytes(value);
|
|
242
|
+
if (bytes) {
|
|
243
|
+
decoded[key] = decodeMcpArgValue(bytes);
|
|
244
|
+
mutated = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const normalizedValue = decodeLogData(value);
|
|
248
|
+
decoded[key] = normalizedValue;
|
|
249
|
+
if (normalizedValue !== value) {
|
|
250
|
+
mutated = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return mutated ? decoded : args;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function decodeLogData(value: unknown): unknown {
|
|
257
|
+
if (!value || typeof value !== "object") {
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(value)) {
|
|
261
|
+
return value.map(entry => decodeLogData(entry));
|
|
262
|
+
}
|
|
263
|
+
const record = value as Record<string, unknown>;
|
|
264
|
+
const typeName = record.$typeName;
|
|
265
|
+
const stripTypeName = typeof typeName === "string" && typeName.startsWith("agent.v1.");
|
|
266
|
+
|
|
267
|
+
if (typeName === "agent.v1.McpArgs") {
|
|
268
|
+
const decodedArgs = decodeMcpArgsForLog(record.args as Record<string, unknown> | undefined);
|
|
269
|
+
const base = stripTypeName ? omitTypeName(record) : record;
|
|
270
|
+
return decodedArgs ? { ...base, args: decodedArgs } : base;
|
|
271
|
+
}
|
|
272
|
+
if (typeName === "agent.v1.McpToolCall") {
|
|
273
|
+
const argsRecord = record.args as Record<string, unknown> | undefined;
|
|
274
|
+
const decodedArgs = decodeMcpArgsForLog(argsRecord?.args as Record<string, unknown> | undefined);
|
|
275
|
+
const base = stripTypeName ? omitTypeName(record) : record;
|
|
276
|
+
if (decodedArgs && argsRecord) {
|
|
277
|
+
return { ...base, args: { ...argsRecord, args: decodedArgs } };
|
|
278
|
+
}
|
|
279
|
+
return base;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let mutated = stripTypeName;
|
|
283
|
+
const decoded: Record<string, unknown> = {};
|
|
284
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
285
|
+
if (stripTypeName && key === "$typeName") {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const normalizedEntry = decodeLogData(entry);
|
|
289
|
+
decoded[key] = normalizedEntry;
|
|
290
|
+
if (normalizedEntry !== entry) {
|
|
291
|
+
mutated = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return mutated ? decoded : record;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function omitTypeName(record: Record<string, unknown>): Record<string, unknown> {
|
|
298
|
+
const { $typeName: _, ...rest } = record;
|
|
299
|
+
return rest;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
303
|
+
model: Model<"cursor-agent">,
|
|
304
|
+
context: Context,
|
|
305
|
+
options?: CursorOptions,
|
|
306
|
+
): AssistantMessageEventStream => {
|
|
307
|
+
const stream = new AssistantMessageEventStream();
|
|
308
|
+
|
|
309
|
+
(async () => {
|
|
310
|
+
const startTime = Date.now();
|
|
311
|
+
let firstTokenTime: number | undefined;
|
|
312
|
+
|
|
313
|
+
const output: AssistantMessage = {
|
|
314
|
+
role: "assistant",
|
|
315
|
+
content: [],
|
|
316
|
+
api: "cursor-agent" as Api,
|
|
317
|
+
provider: model.provider,
|
|
318
|
+
model: model.id,
|
|
319
|
+
usage: {
|
|
320
|
+
input: 0,
|
|
321
|
+
output: 0,
|
|
322
|
+
cacheRead: 0,
|
|
323
|
+
cacheWrite: 0,
|
|
324
|
+
totalTokens: 0,
|
|
325
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
326
|
+
},
|
|
327
|
+
stopReason: "stop",
|
|
328
|
+
timestamp: Date.now(),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
let h2Client: http2.ClientHttp2Session | null = null;
|
|
332
|
+
let h2Request: http2.ClientHttp2Stream | null = null;
|
|
333
|
+
let heartbeatTimer: NodeJS.Timeout | null = null;
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const apiKey = options?.apiKey;
|
|
337
|
+
if (!apiKey) {
|
|
338
|
+
throw new Error("Cursor API key (access token) is required");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const conversationId = options?.conversationId ?? options?.sessionId ?? crypto.randomUUID();
|
|
342
|
+
const blobStore = conversationBlobStores.get(conversationId) ?? new Map<string, Uint8Array>();
|
|
343
|
+
conversationBlobStores.set(conversationId, blobStore);
|
|
344
|
+
const cachedState = conversationStateCache.get(conversationId);
|
|
345
|
+
const { requestBytes, conversationState } = buildGrpcRequest(model, context, options, {
|
|
346
|
+
conversationId,
|
|
347
|
+
blobStore,
|
|
348
|
+
conversationState: cachedState,
|
|
349
|
+
});
|
|
350
|
+
conversationStateCache.set(conversationId, conversationState);
|
|
351
|
+
const requestContextTools = buildMcpToolDefinitions(context.tools);
|
|
352
|
+
|
|
353
|
+
const baseUrl = model.baseUrl || CURSOR_API_URL;
|
|
354
|
+
h2Client = http2.connect(baseUrl);
|
|
355
|
+
|
|
356
|
+
h2Request = h2Client.request({
|
|
357
|
+
":method": "POST",
|
|
358
|
+
":path": "/agent.v1.AgentService/Run",
|
|
359
|
+
"content-type": "application/connect+proto",
|
|
360
|
+
"connect-protocol-version": "1",
|
|
361
|
+
te: "trailers",
|
|
362
|
+
authorization: `Bearer ${apiKey}`,
|
|
363
|
+
"x-ghost-mode": "true",
|
|
364
|
+
"x-cursor-client-version": CURSOR_CLIENT_VERSION,
|
|
365
|
+
"x-cursor-client-type": "cli",
|
|
366
|
+
"x-request-id": crypto.randomUUID(),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
stream.push({ type: "start", partial: output });
|
|
370
|
+
|
|
371
|
+
let pendingBuffer = Buffer.alloc(0);
|
|
372
|
+
let endStreamError: Error | null = null;
|
|
373
|
+
let currentTextBlock: (TextContent & { index: number }) | null = null;
|
|
374
|
+
let currentThinkingBlock: (ThinkingContent & { index: number }) | null = null;
|
|
375
|
+
let currentToolCall: ToolCallState | null = null;
|
|
376
|
+
const usageState: UsageState = { sawTokenDelta: false };
|
|
377
|
+
|
|
378
|
+
const state: BlockState = {
|
|
379
|
+
get currentTextBlock() {
|
|
380
|
+
return currentTextBlock;
|
|
381
|
+
},
|
|
382
|
+
get currentThinkingBlock() {
|
|
383
|
+
return currentThinkingBlock;
|
|
384
|
+
},
|
|
385
|
+
get currentToolCall() {
|
|
386
|
+
return currentToolCall;
|
|
387
|
+
},
|
|
388
|
+
get firstTokenTime() {
|
|
389
|
+
return firstTokenTime;
|
|
390
|
+
},
|
|
391
|
+
setTextBlock: b => {
|
|
392
|
+
currentTextBlock = b;
|
|
393
|
+
},
|
|
394
|
+
setThinkingBlock: b => {
|
|
395
|
+
currentThinkingBlock = b;
|
|
396
|
+
},
|
|
397
|
+
setToolCall: t => {
|
|
398
|
+
currentToolCall = t;
|
|
399
|
+
},
|
|
400
|
+
setFirstTokenTime: () => {
|
|
401
|
+
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const onConversationCheckpoint = (checkpoint: ConversationStateStructure) => {
|
|
406
|
+
conversationStateCache.set(conversationId, checkpoint);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
let resolveH2: (() => void) | undefined;
|
|
410
|
+
|
|
411
|
+
h2Request.on("data", (chunk: Buffer) => {
|
|
412
|
+
pendingBuffer = Buffer.concat([pendingBuffer, chunk]);
|
|
413
|
+
|
|
414
|
+
while (pendingBuffer.length >= 5) {
|
|
415
|
+
const flags = pendingBuffer[0];
|
|
416
|
+
const msgLen = pendingBuffer.readUInt32BE(1);
|
|
417
|
+
if (pendingBuffer.length < 5 + msgLen) break;
|
|
418
|
+
|
|
419
|
+
const messageBytes = pendingBuffer.subarray(5, 5 + msgLen);
|
|
420
|
+
pendingBuffer = pendingBuffer.subarray(5 + msgLen);
|
|
421
|
+
|
|
422
|
+
if (flags & CONNECT_END_STREAM_FLAG) {
|
|
423
|
+
const endError = parseConnectEndStream(messageBytes);
|
|
424
|
+
if (endError) {
|
|
425
|
+
endStreamError = endError;
|
|
426
|
+
h2Request?.close();
|
|
427
|
+
}
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const serverMessage = fromBinary(AgentServerMessageSchema, messageBytes);
|
|
433
|
+
const isTurnEnded =
|
|
434
|
+
serverMessage.message.case === "interactionUpdate" &&
|
|
435
|
+
serverMessage.message.value.message?.case === "turnEnded";
|
|
436
|
+
void handleServerMessage(
|
|
437
|
+
serverMessage,
|
|
438
|
+
output,
|
|
439
|
+
stream,
|
|
440
|
+
state,
|
|
441
|
+
blobStore,
|
|
442
|
+
h2Request!,
|
|
443
|
+
options?.execHandlers,
|
|
444
|
+
options?.onToolResult,
|
|
445
|
+
usageState,
|
|
446
|
+
requestContextTools,
|
|
447
|
+
onConversationCheckpoint,
|
|
448
|
+
).catch(error => {
|
|
449
|
+
log("error", "handleServerMessage", { error: String(error) });
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Resolve only on explicit turnEnded. stopReason defaults to "stop"
|
|
453
|
+
// and is not a reliable signal for stream completion.
|
|
454
|
+
if (isTurnEnded && resolveH2) {
|
|
455
|
+
const r = resolveH2;
|
|
456
|
+
resolveH2 = undefined;
|
|
457
|
+
r();
|
|
458
|
+
}
|
|
459
|
+
} catch (e) {
|
|
460
|
+
log("error", "parseServerMessage", { error: String(e) });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
h2Request.write(frameConnectMessage(requestBytes));
|
|
466
|
+
|
|
467
|
+
const sendHeartbeat = () => {
|
|
468
|
+
if (!h2Request || h2Request.closed) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const heartbeatMessage = create(AgentClientMessageSchema, {
|
|
472
|
+
message: { case: "clientHeartbeat", value: create(ClientHeartbeatSchema, {}) },
|
|
473
|
+
});
|
|
474
|
+
const heartbeatBytes = toBinary(AgentClientMessageSchema, heartbeatMessage);
|
|
475
|
+
h2Request.write(frameConnectMessage(heartbeatBytes));
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
heartbeatTimer = setInterval(sendHeartbeat, 5000);
|
|
479
|
+
|
|
480
|
+
await new Promise<void>((resolve, reject) => {
|
|
481
|
+
resolveH2 = resolve;
|
|
482
|
+
|
|
483
|
+
h2Request!.on("trailers", trailers => {
|
|
484
|
+
const status = trailers["grpc-status"];
|
|
485
|
+
const msg = trailers["grpc-message"];
|
|
486
|
+
if (status && status !== "0") {
|
|
487
|
+
reject(new Error(`gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`));
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
h2Request!.on("end", () => {
|
|
492
|
+
resolveH2 = undefined;
|
|
493
|
+
if (endStreamError) {
|
|
494
|
+
reject(endStreamError);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
resolve();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
h2Request!.on("error", reject);
|
|
501
|
+
|
|
502
|
+
if (options?.signal) {
|
|
503
|
+
options.signal.addEventListener("abort", () => {
|
|
504
|
+
h2Request?.close();
|
|
505
|
+
reject(new Error("Request was aborted"));
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (state.currentTextBlock) {
|
|
511
|
+
const idx = output.content.indexOf(state.currentTextBlock);
|
|
512
|
+
stream.push({
|
|
513
|
+
type: "text_end",
|
|
514
|
+
contentIndex: idx,
|
|
515
|
+
content: state.currentTextBlock.text,
|
|
516
|
+
partial: output,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (state.currentThinkingBlock) {
|
|
520
|
+
const idx = output.content.indexOf(state.currentThinkingBlock);
|
|
521
|
+
stream.push({
|
|
522
|
+
type: "thinking_end",
|
|
523
|
+
contentIndex: idx,
|
|
524
|
+
content: state.currentThinkingBlock.thinking,
|
|
525
|
+
partial: output,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
if (state.currentToolCall) {
|
|
529
|
+
const idx = output.content.indexOf(state.currentToolCall);
|
|
530
|
+
state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
|
|
531
|
+
delete (state.currentToolCall as any).partialJson;
|
|
532
|
+
delete (state.currentToolCall as any).index;
|
|
533
|
+
stream.push({
|
|
534
|
+
type: "toolcall_end",
|
|
535
|
+
contentIndex: idx,
|
|
536
|
+
toolCall: state.currentToolCall,
|
|
537
|
+
partial: output,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
calculateCost(model, output.usage);
|
|
542
|
+
|
|
543
|
+
output.duration = Date.now() - startTime;
|
|
544
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
545
|
+
stream.push({
|
|
546
|
+
type: "done",
|
|
547
|
+
reason: output.stopReason as "stop" | "length" | "toolUse",
|
|
548
|
+
message: output,
|
|
549
|
+
});
|
|
550
|
+
stream.end();
|
|
551
|
+
} catch (error) {
|
|
552
|
+
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
553
|
+
output.errorStatus = extractHttpStatusFromError(error);
|
|
554
|
+
output.errorMessage = formatErrorMessageWithRetryAfter(error);
|
|
555
|
+
output.duration = Date.now() - startTime;
|
|
556
|
+
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
557
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
558
|
+
stream.end();
|
|
559
|
+
} finally {
|
|
560
|
+
if (heartbeatTimer) {
|
|
561
|
+
clearInterval(heartbeatTimer);
|
|
562
|
+
heartbeatTimer = null;
|
|
563
|
+
}
|
|
564
|
+
h2Request?.close();
|
|
565
|
+
h2Client?.close();
|
|
566
|
+
}
|
|
567
|
+
})();
|
|
568
|
+
|
|
569
|
+
return stream;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "mcp" | "todo_write" };
|
|
573
|
+
|
|
574
|
+
interface BlockState {
|
|
575
|
+
currentTextBlock: (TextContent & { index: number }) | null;
|
|
576
|
+
currentThinkingBlock: (ThinkingContent & { index: number }) | null;
|
|
577
|
+
currentToolCall: ToolCallState | null;
|
|
578
|
+
firstTokenTime: number | undefined;
|
|
579
|
+
setTextBlock: (b: (TextContent & { index: number }) | null) => void;
|
|
580
|
+
setThinkingBlock: (b: (ThinkingContent & { index: number }) | null) => void;
|
|
581
|
+
setToolCall: (t: ToolCallState | null) => void;
|
|
582
|
+
setFirstTokenTime: () => void;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
interface UsageState {
|
|
586
|
+
sawTokenDelta: boolean;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function handleServerMessage(
|
|
590
|
+
msg: AgentServerMessage,
|
|
591
|
+
output: AssistantMessage,
|
|
592
|
+
stream: AssistantMessageEventStream,
|
|
593
|
+
state: BlockState,
|
|
594
|
+
blobStore: Map<string, Uint8Array>,
|
|
595
|
+
h2Request: http2.ClientHttp2Stream,
|
|
596
|
+
execHandlers: CursorExecHandlers | undefined,
|
|
597
|
+
onToolResult: CursorToolResultHandler | undefined,
|
|
598
|
+
usageState: UsageState,
|
|
599
|
+
requestContextTools: McpToolDefinition[],
|
|
600
|
+
onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
|
|
601
|
+
): Promise<void> {
|
|
602
|
+
const msgCase = msg.message.case;
|
|
603
|
+
|
|
604
|
+
log("serverMessage", msgCase, msg.message.value);
|
|
605
|
+
|
|
606
|
+
if (msgCase === "interactionUpdate") {
|
|
607
|
+
processInteractionUpdate(msg.message.value, output, stream, state, usageState);
|
|
608
|
+
} else if (msgCase === "kvServerMessage") {
|
|
609
|
+
handleKvServerMessage(msg.message.value as KvServerMessage, blobStore, h2Request);
|
|
610
|
+
} else if (msgCase === "execServerMessage") {
|
|
611
|
+
await handleExecServerMessage(
|
|
612
|
+
msg.message.value as ExecServerMessage,
|
|
613
|
+
h2Request,
|
|
614
|
+
execHandlers,
|
|
615
|
+
onToolResult,
|
|
616
|
+
requestContextTools,
|
|
617
|
+
);
|
|
618
|
+
} else if (msgCase === "conversationCheckpointUpdate") {
|
|
619
|
+
handleConversationCheckpointUpdate(msg.message.value, output, usageState, onConversationCheckpoint);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function handleKvServerMessage(
|
|
624
|
+
kvMsg: KvServerMessage,
|
|
625
|
+
blobStore: Map<string, Uint8Array>,
|
|
626
|
+
h2Request: http2.ClientHttp2Stream,
|
|
627
|
+
): void {
|
|
628
|
+
const kvCase = kvMsg.message.case;
|
|
629
|
+
|
|
630
|
+
if (kvCase === "getBlobArgs") {
|
|
631
|
+
const blobId = kvMsg.message.value.blobId;
|
|
632
|
+
const blobIdKey = Buffer.from(blobId).toString("hex");
|
|
633
|
+
|
|
634
|
+
const blobData = blobStore.get(blobIdKey);
|
|
635
|
+
|
|
636
|
+
const response = create(KvClientMessageSchema, {
|
|
637
|
+
id: kvMsg.id,
|
|
638
|
+
message: {
|
|
639
|
+
case: "getBlobResult",
|
|
640
|
+
value: create(GetBlobResultSchema, blobData ? { blobData } : {}),
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const kvClientMessage = create(AgentClientMessageSchema, {
|
|
645
|
+
message: { case: "kvClientMessage", value: response },
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const responseBytes = toBinary(AgentClientMessageSchema, kvClientMessage);
|
|
649
|
+
h2Request.write(frameConnectMessage(responseBytes));
|
|
650
|
+
|
|
651
|
+
log("kvClient", "getBlobResult", { blobId: blobIdKey.slice(0, 40) });
|
|
652
|
+
} else if (kvCase === "setBlobArgs") {
|
|
653
|
+
const { blobId, blobData } = kvMsg.message.value;
|
|
654
|
+
const blobIdKey = Buffer.from(blobId).toString("hex");
|
|
655
|
+
blobStore.set(blobIdKey, blobData);
|
|
656
|
+
|
|
657
|
+
const response = create(KvClientMessageSchema, {
|
|
658
|
+
id: kvMsg.id,
|
|
659
|
+
message: {
|
|
660
|
+
case: "setBlobResult",
|
|
661
|
+
value: create(SetBlobResultSchema, {}),
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const kvClientMessage = create(AgentClientMessageSchema, {
|
|
666
|
+
message: { case: "kvClientMessage", value: response },
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const responseBytes = toBinary(AgentClientMessageSchema, kvClientMessage);
|
|
670
|
+
h2Request.write(frameConnectMessage(responseBytes));
|
|
671
|
+
|
|
672
|
+
log("kvClient", "setBlobResult", { blobId: blobIdKey.slice(0, 40) });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function sendShellStreamEvent(
|
|
677
|
+
h2Request: http2.ClientHttp2Stream,
|
|
678
|
+
execMsg: ExecServerMessage,
|
|
679
|
+
event: ShellStream["event"],
|
|
680
|
+
): void {
|
|
681
|
+
sendExecClientMessage(h2Request, execMsg, "shellStream", create(ShellStreamSchema, { event }));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function sanitizeShellExecResult(execResult: ShellResult): ShellResult {
|
|
685
|
+
const result = execResult.result;
|
|
686
|
+
if (!result) return execResult;
|
|
687
|
+
|
|
688
|
+
switch (result.case) {
|
|
689
|
+
case "success":
|
|
690
|
+
case "failure": {
|
|
691
|
+
const value = result.value;
|
|
692
|
+
return {
|
|
693
|
+
...execResult,
|
|
694
|
+
result: {
|
|
695
|
+
case: result.case,
|
|
696
|
+
value: {
|
|
697
|
+
...value,
|
|
698
|
+
stdout: value.stdout ? sanitizeText(value.stdout) : value.stdout,
|
|
699
|
+
stderr: value.stderr ? sanitizeText(value.stderr) : value.stderr,
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
} as ShellResult;
|
|
703
|
+
}
|
|
704
|
+
default:
|
|
705
|
+
return execResult;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function handleShellStreamArgs(
|
|
710
|
+
args: ShellArgs,
|
|
711
|
+
execMsg: ExecServerMessage,
|
|
712
|
+
h2Request: http2.ClientHttp2Stream,
|
|
713
|
+
execHandlers: CursorExecHandlers | undefined,
|
|
714
|
+
onToolResult: CursorToolResultHandler | undefined,
|
|
715
|
+
): Promise<void> {
|
|
716
|
+
const normalizedWorkingDirectory = args.workingDirectory || process.cwd();
|
|
717
|
+
const normalizedArgs: ShellArgs = { ...args, workingDirectory: normalizedWorkingDirectory };
|
|
718
|
+
const startTs = Date.now();
|
|
719
|
+
log("shellStream", "start", {
|
|
720
|
+
command: (args as any).command,
|
|
721
|
+
workingDirectory: normalizedWorkingDirectory,
|
|
722
|
+
execId: execMsg.execId,
|
|
723
|
+
hasExecHandlers: !!execHandlers,
|
|
724
|
+
hasShell: !!execHandlers?.shell,
|
|
725
|
+
hasShellStream: !!execHandlers?.shellStream,
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
sendShellStreamEvent(h2Request, execMsg, { case: "start", value: create(ShellStreamStartSchema, {}) });
|
|
729
|
+
|
|
730
|
+
// Buffer for incomplete ANSI sequences across chunks
|
|
731
|
+
let stdoutBuffer = "";
|
|
732
|
+
let stderrBuffer = "";
|
|
733
|
+
|
|
734
|
+
const incompleteEscapeRegex = /\x1b(|\[|\[\d*|\[\?|\[\?\d*|\]\d*;?)$/;
|
|
735
|
+
|
|
736
|
+
const flushStdout = () => {
|
|
737
|
+
if (stdoutBuffer) {
|
|
738
|
+
let safeEnd = stdoutBuffer.length;
|
|
739
|
+
const match = stdoutBuffer.match(incompleteEscapeRegex);
|
|
740
|
+
if (match && match[0].length > 0) {
|
|
741
|
+
safeEnd = stdoutBuffer.length - match[0].length;
|
|
742
|
+
}
|
|
743
|
+
const toSend = stdoutBuffer.slice(0, safeEnd);
|
|
744
|
+
const remaining = stdoutBuffer.slice(safeEnd);
|
|
745
|
+
if (toSend) {
|
|
746
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
747
|
+
case: "stdout",
|
|
748
|
+
value: create(ShellStreamStdoutSchema, { data: sanitizeText(toSend) }),
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
stdoutBuffer = remaining;
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const flushStderr = () => {
|
|
756
|
+
if (stderrBuffer) {
|
|
757
|
+
let safeEnd = stderrBuffer.length;
|
|
758
|
+
const match = stderrBuffer.match(incompleteEscapeRegex);
|
|
759
|
+
if (match && match[0].length > 0) {
|
|
760
|
+
safeEnd = stderrBuffer.length - match[0].length;
|
|
761
|
+
}
|
|
762
|
+
const toSend = stderrBuffer.slice(0, safeEnd);
|
|
763
|
+
const remaining = stderrBuffer.slice(safeEnd);
|
|
764
|
+
if (toSend) {
|
|
765
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
766
|
+
case: "stderr",
|
|
767
|
+
value: create(ShellStreamStderrSchema, { data: sanitizeText(toSend) }),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
stderrBuffer = remaining;
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
let stdoutFlushTimer: NodeJS.Timeout | null = null;
|
|
775
|
+
let stderrFlushTimer: NodeJS.Timeout | null = null;
|
|
776
|
+
|
|
777
|
+
const scheduleStdoutFlush = () => {
|
|
778
|
+
if (!stdoutFlushTimer) {
|
|
779
|
+
stdoutFlushTimer = setTimeout(() => {
|
|
780
|
+
stdoutFlushTimer = null;
|
|
781
|
+
flushStdout();
|
|
782
|
+
}, 100);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const scheduleStderrFlush = () => {
|
|
787
|
+
if (!stderrFlushTimer) {
|
|
788
|
+
stderrFlushTimer = setTimeout(() => {
|
|
789
|
+
stderrFlushTimer = null;
|
|
790
|
+
flushStderr();
|
|
791
|
+
}, 100);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
const streamCallbacks: CursorShellStreamCallbacks = {
|
|
796
|
+
onStdout(data: string) {
|
|
797
|
+
stdoutBuffer += data;
|
|
798
|
+
if (stdoutBuffer.includes("\n") || stdoutBuffer.length > 4096) {
|
|
799
|
+
if (stdoutFlushTimer) {
|
|
800
|
+
clearTimeout(stdoutFlushTimer);
|
|
801
|
+
stdoutFlushTimer = null;
|
|
802
|
+
}
|
|
803
|
+
flushStdout();
|
|
804
|
+
} else {
|
|
805
|
+
scheduleStdoutFlush();
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
onStderr(data: string) {
|
|
809
|
+
stderrBuffer += data;
|
|
810
|
+
if (stderrBuffer.includes("\n") || stderrBuffer.length > 4096) {
|
|
811
|
+
if (stderrFlushTimer) {
|
|
812
|
+
clearTimeout(stderrFlushTimer);
|
|
813
|
+
stderrFlushTimer = null;
|
|
814
|
+
}
|
|
815
|
+
flushStderr();
|
|
816
|
+
} else {
|
|
817
|
+
scheduleStderrFlush();
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
// Prefer the streaming handler — it forwards output chunks in real time.
|
|
823
|
+
// Falls back to the batch shell handler otherwise.
|
|
824
|
+
const streamHandler = execHandlers?.shellStream?.bind(execHandlers);
|
|
825
|
+
const batchHandler = execHandlers?.shell?.bind(execHandlers);
|
|
826
|
+
const handler = streamHandler ? (shellArgs: ShellArgs) => streamHandler(shellArgs, streamCallbacks) : batchHandler;
|
|
827
|
+
|
|
828
|
+
const { execResult } = await resolveExecHandler(
|
|
829
|
+
args as any,
|
|
830
|
+
handler as typeof batchHandler,
|
|
831
|
+
onToolResult,
|
|
832
|
+
toolResult => buildShellResultFromToolResult(normalizedArgs as any, toolResult),
|
|
833
|
+
reason =>
|
|
834
|
+
buildShellRejectedResult((normalizedArgs as any).command, (normalizedArgs as any).workingDirectory, reason),
|
|
835
|
+
error =>
|
|
836
|
+
buildShellFailureResult((normalizedArgs as any).command, (normalizedArgs as any).workingDirectory, error),
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
// When using the batch handler (no shellStream), send buffered stdout/stderr
|
|
840
|
+
// after execution completes. With shellStream these were already sent in real time.
|
|
841
|
+
const sendBufferedOutput = !streamHandler;
|
|
842
|
+
const sanitizedExecResult = sanitizeShellExecResult(execResult);
|
|
843
|
+
|
|
844
|
+
// Flush any remaining buffered output before sending results
|
|
845
|
+
if (stdoutFlushTimer) clearTimeout(stdoutFlushTimer);
|
|
846
|
+
if (stderrFlushTimer) clearTimeout(stderrFlushTimer);
|
|
847
|
+
flushStdout();
|
|
848
|
+
flushStderr();
|
|
849
|
+
|
|
850
|
+
sendShellStreamExitFromResult(h2Request, execMsg, sanitizedExecResult, sendBufferedOutput);
|
|
851
|
+
// Cursor can keep the turn pending when it receives only stream deltas.
|
|
852
|
+
// Send the final structured shellResult as completion acknowledgement.
|
|
853
|
+
sendExecClientMessage(h2Request, execMsg, "shellResult", sanitizedExecResult);
|
|
854
|
+
sendExecClientStreamClose(h2Request, execMsg);
|
|
855
|
+
|
|
856
|
+
log("shellStream", "done", { elapsed: Date.now() - startTs });
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function sendShellStreamExitFromResult(
|
|
860
|
+
h2Request: http2.ClientHttp2Stream,
|
|
861
|
+
execMsg: ExecServerMessage,
|
|
862
|
+
execResult: ShellResult,
|
|
863
|
+
sendBufferedOutput: boolean,
|
|
864
|
+
): void {
|
|
865
|
+
const result = execResult.result;
|
|
866
|
+
switch (result.case) {
|
|
867
|
+
case "success": {
|
|
868
|
+
const value = result.value;
|
|
869
|
+
if (sendBufferedOutput) {
|
|
870
|
+
if (value.stdout) {
|
|
871
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
872
|
+
case: "stdout",
|
|
873
|
+
value: create(ShellStreamStdoutSchema, { data: sanitizeText(value.stdout) }),
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
if (value.stderr) {
|
|
877
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
878
|
+
case: "stderr",
|
|
879
|
+
value: create(ShellStreamStderrSchema, { data: sanitizeText(value.stderr) }),
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
884
|
+
case: "exit",
|
|
885
|
+
value: create(ShellStreamExitSchema, {
|
|
886
|
+
code: value.exitCode,
|
|
887
|
+
cwd: value.workingDirectory,
|
|
888
|
+
aborted: false,
|
|
889
|
+
}),
|
|
890
|
+
});
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
case "failure": {
|
|
894
|
+
const value = result.value;
|
|
895
|
+
if (sendBufferedOutput) {
|
|
896
|
+
if (value.stdout) {
|
|
897
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
898
|
+
case: "stdout",
|
|
899
|
+
value: create(ShellStreamStdoutSchema, { data: sanitizeText(value.stdout) }),
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
if (value.stderr) {
|
|
903
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
904
|
+
case: "stderr",
|
|
905
|
+
value: create(ShellStreamStderrSchema, { data: sanitizeText(value.stderr) }),
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
910
|
+
case: "exit",
|
|
911
|
+
value: create(ShellStreamExitSchema, {
|
|
912
|
+
code: value.exitCode,
|
|
913
|
+
cwd: value.workingDirectory,
|
|
914
|
+
aborted: value.aborted,
|
|
915
|
+
abortReason: value.abortReason,
|
|
916
|
+
}),
|
|
917
|
+
});
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
case "rejected": {
|
|
921
|
+
sendShellStreamEvent(h2Request, execMsg, { case: "rejected", value: result.value });
|
|
922
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
923
|
+
case: "exit",
|
|
924
|
+
value: create(ShellStreamExitSchema, {
|
|
925
|
+
code: 1,
|
|
926
|
+
cwd: result.value.workingDirectory,
|
|
927
|
+
aborted: false,
|
|
928
|
+
}),
|
|
929
|
+
});
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
case "timeout": {
|
|
933
|
+
const value = result.value;
|
|
934
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
935
|
+
case: "stderr",
|
|
936
|
+
value: create(ShellStreamStderrSchema, {
|
|
937
|
+
data: `Command timed out after ${value.timeoutMs}ms`,
|
|
938
|
+
}),
|
|
939
|
+
});
|
|
940
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
941
|
+
case: "exit",
|
|
942
|
+
value: create(ShellStreamExitSchema, {
|
|
943
|
+
code: 1,
|
|
944
|
+
cwd: value.workingDirectory,
|
|
945
|
+
aborted: true,
|
|
946
|
+
}),
|
|
947
|
+
});
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
case "permissionDenied": {
|
|
951
|
+
sendShellStreamEvent(h2Request, execMsg, { case: "permissionDenied", value: result.value });
|
|
952
|
+
sendShellStreamEvent(h2Request, execMsg, {
|
|
953
|
+
case: "exit",
|
|
954
|
+
value: create(ShellStreamExitSchema, {
|
|
955
|
+
code: 1,
|
|
956
|
+
cwd: result.value.workingDirectory,
|
|
957
|
+
aborted: false,
|
|
958
|
+
}),
|
|
959
|
+
});
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
default:
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async function handleExecServerMessage(
|
|
968
|
+
execMsg: ExecServerMessage,
|
|
969
|
+
h2Request: http2.ClientHttp2Stream,
|
|
970
|
+
execHandlers: CursorExecHandlers | undefined,
|
|
971
|
+
onToolResult: CursorToolResultHandler | undefined,
|
|
972
|
+
requestContextTools: McpToolDefinition[],
|
|
973
|
+
): Promise<void> {
|
|
974
|
+
const execCase = execMsg.message.case;
|
|
975
|
+
log("exec", "dispatch", { execCase, execId: execMsg.execId, hasHandlers: !!execHandlers });
|
|
976
|
+
if (execCase === "requestContextArgs") {
|
|
977
|
+
const requestContext = create(RequestContextSchema, {
|
|
978
|
+
rules: [],
|
|
979
|
+
repositoryInfo: [],
|
|
980
|
+
tools: requestContextTools,
|
|
981
|
+
gitRepos: [],
|
|
982
|
+
projectLayouts: [],
|
|
983
|
+
mcpInstructions: [],
|
|
984
|
+
fileContents: {},
|
|
985
|
+
customSubagents: [],
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
const requestContextResult = create(RequestContextResultSchema, {
|
|
989
|
+
result: {
|
|
990
|
+
case: "success",
|
|
991
|
+
value: create(RequestContextSuccessSchema, { requestContext }),
|
|
992
|
+
},
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
sendExecClientMessage(h2Request, execMsg, "requestContextResult", requestContextResult);
|
|
996
|
+
log("execClient", "requestContextResult");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (!execCase) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
switch (execCase) {
|
|
1005
|
+
case "readArgs": {
|
|
1006
|
+
const args = execMsg.message.value;
|
|
1007
|
+
const { execResult } = await resolveExecHandler(
|
|
1008
|
+
args,
|
|
1009
|
+
execHandlers?.read?.bind(execHandlers),
|
|
1010
|
+
onToolResult,
|
|
1011
|
+
toolResult => buildReadResultFromToolResult(args.path, toolResult),
|
|
1012
|
+
reason => buildReadRejectedResult(args.path, reason),
|
|
1013
|
+
error => buildReadErrorResult(args.path, error),
|
|
1014
|
+
);
|
|
1015
|
+
sendExecClientMessage(h2Request, execMsg, "readResult", execResult);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
case "lsArgs": {
|
|
1019
|
+
const args = execMsg.message.value;
|
|
1020
|
+
const { execResult } = await resolveExecHandler(
|
|
1021
|
+
args,
|
|
1022
|
+
execHandlers?.ls?.bind(execHandlers),
|
|
1023
|
+
onToolResult,
|
|
1024
|
+
toolResult => buildLsResultFromToolResult(args.path, toolResult),
|
|
1025
|
+
reason => buildLsRejectedResult(args.path, reason),
|
|
1026
|
+
error => buildLsErrorResult(args.path, error),
|
|
1027
|
+
);
|
|
1028
|
+
sendExecClientMessage(h2Request, execMsg, "lsResult", execResult);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
case "grepArgs": {
|
|
1032
|
+
const args = execMsg.message.value;
|
|
1033
|
+
const { execResult } = await resolveExecHandler(
|
|
1034
|
+
args,
|
|
1035
|
+
execHandlers?.grep?.bind(execHandlers),
|
|
1036
|
+
onToolResult,
|
|
1037
|
+
toolResult => buildGrepResultFromToolResult(args, toolResult),
|
|
1038
|
+
reason => buildGrepErrorResult(reason),
|
|
1039
|
+
error => buildGrepErrorResult(error),
|
|
1040
|
+
);
|
|
1041
|
+
sendExecClientMessage(h2Request, execMsg, "grepResult", execResult);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
case "writeArgs": {
|
|
1045
|
+
const args = execMsg.message.value;
|
|
1046
|
+
const { execResult } = await resolveExecHandler(
|
|
1047
|
+
args,
|
|
1048
|
+
execHandlers?.write?.bind(execHandlers),
|
|
1049
|
+
onToolResult,
|
|
1050
|
+
toolResult =>
|
|
1051
|
+
buildWriteResultFromToolResult(
|
|
1052
|
+
{
|
|
1053
|
+
path: args.path,
|
|
1054
|
+
fileText: args.fileText,
|
|
1055
|
+
fileBytes: args.fileBytes,
|
|
1056
|
+
returnFileContentAfterWrite: args.returnFileContentAfterWrite,
|
|
1057
|
+
},
|
|
1058
|
+
toolResult,
|
|
1059
|
+
),
|
|
1060
|
+
reason => buildWriteRejectedResult(args.path, reason),
|
|
1061
|
+
error => buildWriteErrorResult(args.path, error),
|
|
1062
|
+
);
|
|
1063
|
+
sendExecClientMessage(h2Request, execMsg, "writeResult", execResult);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
case "deleteArgs": {
|
|
1067
|
+
const args = execMsg.message.value;
|
|
1068
|
+
const { execResult } = await resolveExecHandler(
|
|
1069
|
+
args,
|
|
1070
|
+
execHandlers?.delete?.bind(execHandlers),
|
|
1071
|
+
onToolResult,
|
|
1072
|
+
toolResult => buildDeleteResultFromToolResult(args.path, toolResult),
|
|
1073
|
+
reason => buildDeleteRejectedResult(args.path, reason),
|
|
1074
|
+
error => buildDeleteErrorResult(args.path, error),
|
|
1075
|
+
);
|
|
1076
|
+
sendExecClientMessage(h2Request, execMsg, "deleteResult", execResult);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
case "shellArgs": {
|
|
1080
|
+
const args = execMsg.message.value;
|
|
1081
|
+
const normalizedArgs: ShellArgs = { ...args, workingDirectory: args.workingDirectory || process.cwd() };
|
|
1082
|
+
const { execResult } = await resolveExecHandler(
|
|
1083
|
+
args,
|
|
1084
|
+
execHandlers?.shell?.bind(execHandlers),
|
|
1085
|
+
onToolResult,
|
|
1086
|
+
toolResult => buildShellResultFromToolResult(normalizedArgs, toolResult),
|
|
1087
|
+
reason => buildShellRejectedResult(normalizedArgs.command, normalizedArgs.workingDirectory, reason),
|
|
1088
|
+
error => buildShellFailureResult(normalizedArgs.command, normalizedArgs.workingDirectory, error),
|
|
1089
|
+
);
|
|
1090
|
+
const sanitizedExecResult = sanitizeShellExecResult(execResult);
|
|
1091
|
+
sendExecClientMessage(h2Request, execMsg, "shellResult", sanitizedExecResult);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
case "shellStreamArgs": {
|
|
1095
|
+
const args = execMsg.message.value;
|
|
1096
|
+
await handleShellStreamArgs(args, execMsg, h2Request, execHandlers, onToolResult);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
case "backgroundShellSpawnArgs": {
|
|
1100
|
+
const args = execMsg.message.value;
|
|
1101
|
+
const execResult = create(BackgroundShellSpawnResultSchema, {
|
|
1102
|
+
result: {
|
|
1103
|
+
case: "rejected",
|
|
1104
|
+
value: create(ShellRejectedSchema, {
|
|
1105
|
+
command: args.command,
|
|
1106
|
+
workingDirectory: args.workingDirectory,
|
|
1107
|
+
reason: "Not implemented",
|
|
1108
|
+
isReadonly: false,
|
|
1109
|
+
}),
|
|
1110
|
+
},
|
|
1111
|
+
});
|
|
1112
|
+
sendExecClientMessage(h2Request, execMsg, "backgroundShellSpawnResult", execResult);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
case "writeShellStdinArgs": {
|
|
1116
|
+
const execResult = create(WriteShellStdinResultSchema, {
|
|
1117
|
+
result: {
|
|
1118
|
+
case: "error",
|
|
1119
|
+
value: create(WriteShellStdinErrorSchema, {
|
|
1120
|
+
error: "Not implemented",
|
|
1121
|
+
}),
|
|
1122
|
+
},
|
|
1123
|
+
});
|
|
1124
|
+
sendExecClientMessage(h2Request, execMsg, "writeShellStdinResult", execResult);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
case "fetchArgs": {
|
|
1128
|
+
const args = execMsg.message.value;
|
|
1129
|
+
const execResult = create(FetchResultSchema, {
|
|
1130
|
+
result: {
|
|
1131
|
+
case: "error",
|
|
1132
|
+
value: create(FetchErrorSchema, {
|
|
1133
|
+
url: args.url,
|
|
1134
|
+
error: "Not implemented",
|
|
1135
|
+
}),
|
|
1136
|
+
},
|
|
1137
|
+
});
|
|
1138
|
+
sendExecClientMessage(h2Request, execMsg, "fetchResult", execResult);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
case "diagnosticsArgs": {
|
|
1142
|
+
const args = execMsg.message.value;
|
|
1143
|
+
const { execResult } = await resolveExecHandler(
|
|
1144
|
+
args,
|
|
1145
|
+
execHandlers?.diagnostics?.bind(execHandlers),
|
|
1146
|
+
onToolResult,
|
|
1147
|
+
toolResult => buildDiagnosticsResultFromToolResult(args.path, toolResult),
|
|
1148
|
+
reason => buildDiagnosticsRejectedResult(args.path, reason),
|
|
1149
|
+
error => buildDiagnosticsErrorResult(args.path, error),
|
|
1150
|
+
);
|
|
1151
|
+
sendExecClientMessage(h2Request, execMsg, "diagnosticsResult", execResult);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
case "mcpArgs": {
|
|
1155
|
+
const args = execMsg.message.value;
|
|
1156
|
+
const mcpCall = decodeMcpCall(args);
|
|
1157
|
+
const { execResult } = await resolveExecHandler(
|
|
1158
|
+
mcpCall,
|
|
1159
|
+
execHandlers?.mcp?.bind(execHandlers),
|
|
1160
|
+
onToolResult,
|
|
1161
|
+
toolResult => buildMcpResultFromToolResult(mcpCall, toolResult),
|
|
1162
|
+
_reason => buildMcpToolNotFoundResult(mcpCall),
|
|
1163
|
+
error => buildMcpErrorResult(error),
|
|
1164
|
+
);
|
|
1165
|
+
sendExecClientMessage(h2Request, execMsg, "mcpResult", execResult);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
case "listMcpResourcesExecArgs": {
|
|
1169
|
+
const execResult = create(ListMcpResourcesExecResultSchema, {});
|
|
1170
|
+
sendExecClientMessage(h2Request, execMsg, "listMcpResourcesExecResult", execResult);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
case "readMcpResourceExecArgs": {
|
|
1174
|
+
const execResult = create(ReadMcpResourceExecResultSchema, {});
|
|
1175
|
+
sendExecClientMessage(h2Request, execMsg, "readMcpResourceExecResult", execResult);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
case "recordScreenArgs": {
|
|
1179
|
+
const execResult = create(RecordScreenResultSchema, {});
|
|
1180
|
+
sendExecClientMessage(h2Request, execMsg, "recordScreenResult", execResult);
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
case "computerUseArgs": {
|
|
1184
|
+
const execResult = create(ComputerUseResultSchema, {});
|
|
1185
|
+
sendExecClientMessage(h2Request, execMsg, "computerUseResult", execResult);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
default: {
|
|
1189
|
+
log("warn", "unhandledExecMessage", { execCase });
|
|
1190
|
+
// Send a bare ExecClientMessage (id + execId only, no typed result) so the
|
|
1191
|
+
// server gets an acknowledgement and doesn't hang waiting forever.
|
|
1192
|
+
const ack = create(ExecClientMessageSchema, {
|
|
1193
|
+
id: execMsg.id,
|
|
1194
|
+
execId: execMsg.execId,
|
|
1195
|
+
});
|
|
1196
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
1197
|
+
message: { case: "execClientMessage", value: ack },
|
|
1198
|
+
});
|
|
1199
|
+
h2Request.write(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage)));
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function sendExecClientMessage<T>(
|
|
1205
|
+
h2Request: http2.ClientHttp2Stream,
|
|
1206
|
+
execMsg: ExecServerMessage,
|
|
1207
|
+
messageCase: ExecClientMessage["message"]["case"],
|
|
1208
|
+
value: T,
|
|
1209
|
+
): void {
|
|
1210
|
+
const execClientMessage = create(ExecClientMessageSchema, {
|
|
1211
|
+
id: execMsg.id,
|
|
1212
|
+
execId: execMsg.execId,
|
|
1213
|
+
message: {
|
|
1214
|
+
case: messageCase,
|
|
1215
|
+
value: value as any,
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
1220
|
+
message: { case: "execClientMessage", value: execClientMessage },
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
const responseBytes = toBinary(AgentClientMessageSchema, clientMessage);
|
|
1224
|
+
h2Request.write(frameConnectMessage(responseBytes));
|
|
1225
|
+
|
|
1226
|
+
log("execClientMessage", messageCase, value);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function sendExecClientStreamClose(h2Request: http2.ClientHttp2Stream, execMsg: ExecServerMessage): void {
|
|
1230
|
+
const closeMessage = create(ExecClientControlMessageSchema, {
|
|
1231
|
+
message: {
|
|
1232
|
+
case: "streamClose",
|
|
1233
|
+
value: create(ExecClientStreamCloseSchema, {
|
|
1234
|
+
id: execMsg.id,
|
|
1235
|
+
}),
|
|
1236
|
+
},
|
|
1237
|
+
});
|
|
1238
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
1239
|
+
message: { case: "execClientControlMessage", value: closeMessage },
|
|
1240
|
+
});
|
|
1241
|
+
const responseBytes = toBinary(AgentClientMessageSchema, clientMessage);
|
|
1242
|
+
h2Request.write(frameConnectMessage(responseBytes));
|
|
1243
|
+
log("execClientControl", "streamClose", { id: execMsg.id, execId: execMsg.execId });
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/** Exported for tests: verifies handler is invoked with correct `this` when passed as bound. */
|
|
1247
|
+
export async function resolveExecHandler<TArgs, TResult>(
|
|
1248
|
+
args: TArgs,
|
|
1249
|
+
handler: ((args: TArgs) => Promise<CursorExecHandlerResult<TResult>>) | undefined,
|
|
1250
|
+
onToolResult: CursorToolResultHandler | undefined,
|
|
1251
|
+
buildFromToolResult: (toolResult: ToolResultMessage) => TResult,
|
|
1252
|
+
buildRejected: (reason: string) => TResult,
|
|
1253
|
+
buildError: (error: string) => TResult,
|
|
1254
|
+
): Promise<{ execResult: TResult; toolResult?: ToolResultMessage }> {
|
|
1255
|
+
if (!handler) {
|
|
1256
|
+
return { execResult: buildRejected("Tool not available") };
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
try {
|
|
1260
|
+
const handlerResult = await handler(args);
|
|
1261
|
+
const { execResult, toolResult } = splitExecHandlerResult(handlerResult);
|
|
1262
|
+
const finalToolResult = await applyToolResultHandler(toolResult, onToolResult);
|
|
1263
|
+
|
|
1264
|
+
if (execResult) {
|
|
1265
|
+
return { execResult, toolResult: finalToolResult };
|
|
1266
|
+
}
|
|
1267
|
+
if (finalToolResult) {
|
|
1268
|
+
return { execResult: buildFromToolResult(finalToolResult), toolResult: finalToolResult };
|
|
1269
|
+
}
|
|
1270
|
+
return { execResult: buildRejected("Tool returned no result") };
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1273
|
+
return { execResult: buildError(message) };
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function splitExecHandlerResult<TResult>(result: CursorExecHandlerResult<TResult>): {
|
|
1278
|
+
execResult?: TResult;
|
|
1279
|
+
toolResult?: ToolResultMessage;
|
|
1280
|
+
} {
|
|
1281
|
+
if (isToolResultMessage(result)) {
|
|
1282
|
+
return { toolResult: result };
|
|
1283
|
+
}
|
|
1284
|
+
if (result && typeof result === "object") {
|
|
1285
|
+
const record = result as Record<string, unknown>;
|
|
1286
|
+
if ("execResult" in record) {
|
|
1287
|
+
const { execResult, toolResult } = record as {
|
|
1288
|
+
execResult: TResult;
|
|
1289
|
+
toolResult?: ToolResultMessage;
|
|
1290
|
+
};
|
|
1291
|
+
return { execResult, toolResult };
|
|
1292
|
+
}
|
|
1293
|
+
if ("toolResult" in record && !isToolResultMessage(record)) {
|
|
1294
|
+
const { result: execResult, toolResult } = record as {
|
|
1295
|
+
result?: TResult;
|
|
1296
|
+
toolResult?: ToolResultMessage;
|
|
1297
|
+
};
|
|
1298
|
+
return { execResult, toolResult };
|
|
1299
|
+
}
|
|
1300
|
+
if ("result" in record && !("$typeName" in record)) {
|
|
1301
|
+
const { result: execResult, toolResult } = record as {
|
|
1302
|
+
result: TResult;
|
|
1303
|
+
toolResult?: ToolResultMessage;
|
|
1304
|
+
};
|
|
1305
|
+
return { execResult, toolResult };
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return { execResult: result as TResult };
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function isToolResultMessage(value: unknown): value is ToolResultMessage {
|
|
1312
|
+
return !!value && typeof value === "object" && (value as ToolResultMessage).role === "toolResult";
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function applyToolResultHandler(
|
|
1316
|
+
toolResult: ToolResultMessage | undefined,
|
|
1317
|
+
onToolResult: CursorToolResultHandler | undefined,
|
|
1318
|
+
): Promise<ToolResultMessage | undefined> {
|
|
1319
|
+
if (!toolResult || !onToolResult) {
|
|
1320
|
+
return toolResult;
|
|
1321
|
+
}
|
|
1322
|
+
const updated = await onToolResult(toolResult);
|
|
1323
|
+
return updated ?? toolResult;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function toolResultToText(toolResult: ToolResultMessage): string {
|
|
1327
|
+
return toolResult.content.map(item => (item.type === "text" ? item.text : `[${item.mimeType} image]`)).join("\n");
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function toolResultWasTruncated(toolResult: ToolResultMessage): boolean {
|
|
1331
|
+
if (!toolResult.details || typeof toolResult.details !== "object") {
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
const truncation = (toolResult.details as { truncation?: { truncated?: boolean } }).truncation;
|
|
1335
|
+
return !!truncation?.truncated;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function toolResultDetailBoolean(toolResult: ToolResultMessage, key: string): boolean {
|
|
1339
|
+
if (!toolResult.details || typeof toolResult.details !== "object") {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
const value = (toolResult.details as Record<string, unknown>)[key];
|
|
1343
|
+
return typeof value === "boolean" ? value : false;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function buildReadResultFromToolResult(path: string, toolResult: ToolResultMessage) {
|
|
1347
|
+
const text = toolResultToText(toolResult);
|
|
1348
|
+
if (toolResult.isError) {
|
|
1349
|
+
return buildReadErrorResult(path, text || "Read failed");
|
|
1350
|
+
}
|
|
1351
|
+
const totalLines = text ? text.split("\n").length : 0;
|
|
1352
|
+
return create(ReadResultSchema, {
|
|
1353
|
+
result: {
|
|
1354
|
+
case: "success",
|
|
1355
|
+
value: create(ReadSuccessSchema, {
|
|
1356
|
+
path,
|
|
1357
|
+
totalLines,
|
|
1358
|
+
fileSize: BigInt(Buffer.byteLength(text, "utf-8")),
|
|
1359
|
+
truncated: toolResultWasTruncated(toolResult),
|
|
1360
|
+
output: { case: "content", value: text },
|
|
1361
|
+
}),
|
|
1362
|
+
},
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function buildReadErrorResult(path: string, error: string) {
|
|
1367
|
+
return create(ReadResultSchema, {
|
|
1368
|
+
result: {
|
|
1369
|
+
case: "error",
|
|
1370
|
+
value: create(ReadErrorSchema, { path, error }),
|
|
1371
|
+
},
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function buildReadRejectedResult(path: string, reason: string) {
|
|
1376
|
+
return create(ReadResultSchema, {
|
|
1377
|
+
result: {
|
|
1378
|
+
case: "rejected",
|
|
1379
|
+
value: create(ReadRejectedSchema, { path, reason }),
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function buildWriteResultFromToolResult(
|
|
1385
|
+
args: { path: string; fileText?: string; fileBytes?: Uint8Array; returnFileContentAfterWrite?: boolean },
|
|
1386
|
+
toolResult: ToolResultMessage,
|
|
1387
|
+
) {
|
|
1388
|
+
const text = toolResultToText(toolResult);
|
|
1389
|
+
if (toolResult.isError) {
|
|
1390
|
+
return buildWriteErrorResult(args.path, text || "Write failed");
|
|
1391
|
+
}
|
|
1392
|
+
const fileText = args.fileText ?? "";
|
|
1393
|
+
const fileSize = args.fileBytes?.length ?? Buffer.byteLength(fileText, "utf-8");
|
|
1394
|
+
const linesCreated = fileText ? fileText.split("\n").length : 0;
|
|
1395
|
+
return create(WriteResultSchema, {
|
|
1396
|
+
result: {
|
|
1397
|
+
case: "success",
|
|
1398
|
+
value: create(WriteSuccessSchema, {
|
|
1399
|
+
path: args.path,
|
|
1400
|
+
linesCreated,
|
|
1401
|
+
fileSize,
|
|
1402
|
+
fileContentAfterWrite: args.returnFileContentAfterWrite ? fileText : undefined,
|
|
1403
|
+
}),
|
|
1404
|
+
},
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function buildWriteErrorResult(path: string, error: string) {
|
|
1409
|
+
return create(WriteResultSchema, {
|
|
1410
|
+
result: {
|
|
1411
|
+
case: "error",
|
|
1412
|
+
value: create(WriteErrorSchema, { path, error }),
|
|
1413
|
+
},
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function buildWriteRejectedResult(path: string, reason: string) {
|
|
1418
|
+
return create(WriteResultSchema, {
|
|
1419
|
+
result: {
|
|
1420
|
+
case: "rejected",
|
|
1421
|
+
value: create(WriteRejectedSchema, { path, reason }),
|
|
1422
|
+
},
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function buildDeleteResultFromToolResult(path: string, toolResult: ToolResultMessage) {
|
|
1427
|
+
const text = toolResultToText(toolResult);
|
|
1428
|
+
if (toolResult.isError) {
|
|
1429
|
+
return buildDeleteErrorResult(path, text || "Delete failed");
|
|
1430
|
+
}
|
|
1431
|
+
return create(DeleteResultSchema, {
|
|
1432
|
+
result: {
|
|
1433
|
+
case: "success",
|
|
1434
|
+
value: create(DeleteSuccessSchema, {
|
|
1435
|
+
path,
|
|
1436
|
+
deletedFile: path,
|
|
1437
|
+
fileSize: BigInt(0),
|
|
1438
|
+
prevContent: "",
|
|
1439
|
+
}),
|
|
1440
|
+
},
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function buildDeleteErrorResult(path: string, error: string) {
|
|
1445
|
+
return create(DeleteResultSchema, {
|
|
1446
|
+
result: {
|
|
1447
|
+
case: "error",
|
|
1448
|
+
value: create(DeleteErrorSchema, { path, error }),
|
|
1449
|
+
},
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function buildDeleteRejectedResult(path: string, reason: string) {
|
|
1454
|
+
return create(DeleteResultSchema, {
|
|
1455
|
+
result: {
|
|
1456
|
+
case: "rejected",
|
|
1457
|
+
value: create(DeleteRejectedSchema, { path, reason }),
|
|
1458
|
+
},
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function buildShellResultFromToolResult(
|
|
1463
|
+
args: { command: string; workingDirectory: string },
|
|
1464
|
+
toolResult: ToolResultMessage,
|
|
1465
|
+
) {
|
|
1466
|
+
const output = toolResultToText(toolResult);
|
|
1467
|
+
if (toolResult.isError) {
|
|
1468
|
+
return buildShellFailureResult(args.command, args.workingDirectory, output || "Shell failed");
|
|
1469
|
+
}
|
|
1470
|
+
return create(ShellResultSchema, {
|
|
1471
|
+
result: {
|
|
1472
|
+
case: "success",
|
|
1473
|
+
value: create(ShellSuccessSchema, {
|
|
1474
|
+
command: args.command,
|
|
1475
|
+
workingDirectory: args.workingDirectory,
|
|
1476
|
+
exitCode: 0,
|
|
1477
|
+
signal: "",
|
|
1478
|
+
stdout: output,
|
|
1479
|
+
stderr: "",
|
|
1480
|
+
executionTime: 0,
|
|
1481
|
+
}),
|
|
1482
|
+
},
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
function buildShellFailureResult(command: string, workingDirectory: string, error: string) {
|
|
1487
|
+
return create(ShellResultSchema, {
|
|
1488
|
+
result: {
|
|
1489
|
+
case: "failure",
|
|
1490
|
+
value: create(ShellFailureSchema, {
|
|
1491
|
+
command,
|
|
1492
|
+
workingDirectory,
|
|
1493
|
+
exitCode: 1,
|
|
1494
|
+
signal: "",
|
|
1495
|
+
stdout: "",
|
|
1496
|
+
stderr: error,
|
|
1497
|
+
executionTime: 0,
|
|
1498
|
+
aborted: false,
|
|
1499
|
+
}),
|
|
1500
|
+
},
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function buildShellRejectedResult(command: string, workingDirectory: string, reason: string) {
|
|
1505
|
+
return create(ShellResultSchema, {
|
|
1506
|
+
result: {
|
|
1507
|
+
case: "rejected",
|
|
1508
|
+
value: create(ShellRejectedSchema, {
|
|
1509
|
+
command,
|
|
1510
|
+
workingDirectory,
|
|
1511
|
+
reason,
|
|
1512
|
+
isReadonly: false,
|
|
1513
|
+
}),
|
|
1514
|
+
},
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function buildLsResultFromToolResult(path: string, toolResult: ToolResultMessage) {
|
|
1519
|
+
const text = toolResultToText(toolResult);
|
|
1520
|
+
if (toolResult.isError) {
|
|
1521
|
+
return buildLsErrorResult(path, text || "Ls failed");
|
|
1522
|
+
}
|
|
1523
|
+
const rootPath = path || ".";
|
|
1524
|
+
const entries = text
|
|
1525
|
+
.split("\n")
|
|
1526
|
+
.map(line => line.trim())
|
|
1527
|
+
.filter(line => line.length > 0 && !line.startsWith("["));
|
|
1528
|
+
const childrenDirs: LsDirectoryTreeNode[] = [];
|
|
1529
|
+
const childrenFiles: LsDirectoryTreeNode_File[] = [];
|
|
1530
|
+
|
|
1531
|
+
for (const entry of entries) {
|
|
1532
|
+
const name = entry.split(" (")[0];
|
|
1533
|
+
if (name.endsWith("/")) {
|
|
1534
|
+
const dirName = name.slice(0, -1);
|
|
1535
|
+
childrenDirs.push(
|
|
1536
|
+
create(LsDirectoryTreeNodeSchema, {
|
|
1537
|
+
absPath: `${rootPath.replace(/\/$/, "")}/${dirName}`,
|
|
1538
|
+
childrenDirs: [],
|
|
1539
|
+
childrenFiles: [],
|
|
1540
|
+
childrenWereProcessed: false,
|
|
1541
|
+
fullSubtreeExtensionCounts: {},
|
|
1542
|
+
numFiles: 0,
|
|
1543
|
+
}),
|
|
1544
|
+
);
|
|
1545
|
+
} else {
|
|
1546
|
+
childrenFiles.push(create(LsDirectoryTreeNode_FileSchema, { name }));
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
const root = create(LsDirectoryTreeNodeSchema, {
|
|
1551
|
+
absPath: rootPath,
|
|
1552
|
+
childrenDirs,
|
|
1553
|
+
childrenFiles,
|
|
1554
|
+
childrenWereProcessed: true,
|
|
1555
|
+
fullSubtreeExtensionCounts: {},
|
|
1556
|
+
numFiles: childrenFiles.length,
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
return create(LsResultSchema, {
|
|
1560
|
+
result: {
|
|
1561
|
+
case: "success",
|
|
1562
|
+
value: create(LsSuccessSchema, { directoryTreeRoot: root }),
|
|
1563
|
+
},
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function buildLsErrorResult(path: string, error: string) {
|
|
1568
|
+
return create(LsResultSchema, {
|
|
1569
|
+
result: {
|
|
1570
|
+
case: "error",
|
|
1571
|
+
value: create(LsErrorSchema, { path, error }),
|
|
1572
|
+
},
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function buildLsRejectedResult(path: string, reason: string) {
|
|
1577
|
+
return create(LsResultSchema, {
|
|
1578
|
+
result: {
|
|
1579
|
+
case: "rejected",
|
|
1580
|
+
value: create(LsRejectedSchema, { path, reason }),
|
|
1581
|
+
},
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function buildGrepResultFromToolResult(
|
|
1586
|
+
args: { pattern: string; path?: string; outputMode?: string },
|
|
1587
|
+
toolResult: ToolResultMessage,
|
|
1588
|
+
) {
|
|
1589
|
+
const text = toolResultToText(toolResult);
|
|
1590
|
+
if (toolResult.isError) {
|
|
1591
|
+
return buildGrepErrorResult(text || "Grep failed");
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
const outputMode = args.outputMode || "content";
|
|
1595
|
+
const clientTruncated = toolResultDetailBoolean(toolResult, "truncated");
|
|
1596
|
+
const lines = text
|
|
1597
|
+
.split("\n")
|
|
1598
|
+
.map(line => line.trimEnd())
|
|
1599
|
+
.filter(line => line.length > 0 && !line.startsWith("[") && !line.toLowerCase().startsWith("no matches"));
|
|
1600
|
+
|
|
1601
|
+
const workspaceKey = args.path || ".";
|
|
1602
|
+
let unionResult: GrepUnionResult;
|
|
1603
|
+
|
|
1604
|
+
if (outputMode === "files_with_matches") {
|
|
1605
|
+
const files = lines;
|
|
1606
|
+
unionResult = create(GrepUnionResultSchema, {
|
|
1607
|
+
result: {
|
|
1608
|
+
case: "files",
|
|
1609
|
+
value: create(GrepFilesResultSchema, {
|
|
1610
|
+
files,
|
|
1611
|
+
totalFiles: files.length,
|
|
1612
|
+
clientTruncated,
|
|
1613
|
+
ripgrepTruncated: false,
|
|
1614
|
+
}),
|
|
1615
|
+
},
|
|
1616
|
+
});
|
|
1617
|
+
} else if (outputMode === "count") {
|
|
1618
|
+
const counts = lines
|
|
1619
|
+
.map(line => {
|
|
1620
|
+
const separatorIndex = line.lastIndexOf(":");
|
|
1621
|
+
if (separatorIndex === -1) {
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
1624
|
+
const file = line.slice(0, separatorIndex);
|
|
1625
|
+
const count = Number.parseInt(line.slice(separatorIndex + 1), 10);
|
|
1626
|
+
if (!file || Number.isNaN(count)) {
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
return create(GrepFileCountSchema, { file, count });
|
|
1630
|
+
})
|
|
1631
|
+
.filter((entry): entry is GrepFileCount => entry !== null);
|
|
1632
|
+
const totalMatches = counts.reduce((sum, entry) => sum + entry.count, 0);
|
|
1633
|
+
unionResult = create(GrepUnionResultSchema, {
|
|
1634
|
+
result: {
|
|
1635
|
+
case: "count",
|
|
1636
|
+
value: create(GrepCountResultSchema, {
|
|
1637
|
+
counts,
|
|
1638
|
+
totalFiles: counts.length,
|
|
1639
|
+
totalMatches,
|
|
1640
|
+
clientTruncated,
|
|
1641
|
+
ripgrepTruncated: false,
|
|
1642
|
+
}),
|
|
1643
|
+
},
|
|
1644
|
+
});
|
|
1645
|
+
} else {
|
|
1646
|
+
const matchMap = new Map<string, Array<{ line: number; content: string; isContextLine: boolean }>>();
|
|
1647
|
+
let totalMatchedLines = 0;
|
|
1648
|
+
|
|
1649
|
+
for (const line of lines) {
|
|
1650
|
+
const matchLine = line.match(/^(.+?):(\d+):\s?(.*)$/);
|
|
1651
|
+
const contextLine = line.match(/^(.+?)-(\d+)-\s?(.*)$/);
|
|
1652
|
+
const match = matchLine ?? contextLine;
|
|
1653
|
+
if (!match) {
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
const [, file, lineNumber, content] = match;
|
|
1657
|
+
const isContextLine = Boolean(contextLine);
|
|
1658
|
+
const list = matchMap.get(file) ?? [];
|
|
1659
|
+
list.push({ line: Number(lineNumber), content, isContextLine });
|
|
1660
|
+
matchMap.set(file, list);
|
|
1661
|
+
if (!isContextLine) {
|
|
1662
|
+
totalMatchedLines += 1;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
const matches = Array.from(matchMap.entries()).map(([file, matches]) =>
|
|
1667
|
+
create(GrepFileMatchSchema, {
|
|
1668
|
+
file,
|
|
1669
|
+
matches: matches.map(entry =>
|
|
1670
|
+
create(GrepContentMatchSchema, {
|
|
1671
|
+
lineNumber: entry.line,
|
|
1672
|
+
content: entry.content,
|
|
1673
|
+
contentTruncated: false,
|
|
1674
|
+
isContextLine: entry.isContextLine,
|
|
1675
|
+
}),
|
|
1676
|
+
),
|
|
1677
|
+
}),
|
|
1678
|
+
);
|
|
1679
|
+
const totalLines = matches.reduce((sum, entry) => sum + entry.matches.length, 0);
|
|
1680
|
+
unionResult = create(GrepUnionResultSchema, {
|
|
1681
|
+
result: {
|
|
1682
|
+
case: "content",
|
|
1683
|
+
value: create(GrepContentResultSchema, {
|
|
1684
|
+
matches,
|
|
1685
|
+
totalLines,
|
|
1686
|
+
totalMatchedLines,
|
|
1687
|
+
clientTruncated,
|
|
1688
|
+
ripgrepTruncated: false,
|
|
1689
|
+
}),
|
|
1690
|
+
},
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
return create(GrepResultSchema, {
|
|
1695
|
+
result: {
|
|
1696
|
+
case: "success",
|
|
1697
|
+
value: create(GrepSuccessSchema, {
|
|
1698
|
+
pattern: args.pattern,
|
|
1699
|
+
path: args.path || "",
|
|
1700
|
+
outputMode,
|
|
1701
|
+
workspaceResults: { [workspaceKey]: unionResult },
|
|
1702
|
+
}),
|
|
1703
|
+
},
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function buildGrepErrorResult(error: string) {
|
|
1708
|
+
return create(GrepResultSchema, {
|
|
1709
|
+
result: {
|
|
1710
|
+
case: "error",
|
|
1711
|
+
value: create(GrepErrorSchema, { error }),
|
|
1712
|
+
},
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function buildDiagnosticsResultFromToolResult(path: string, toolResult: ToolResultMessage) {
|
|
1717
|
+
const text = toolResultToText(toolResult);
|
|
1718
|
+
if (toolResult.isError) {
|
|
1719
|
+
return buildDiagnosticsErrorResult(path, text || "Diagnostics failed");
|
|
1720
|
+
}
|
|
1721
|
+
return create(DiagnosticsResultSchema, {
|
|
1722
|
+
result: {
|
|
1723
|
+
case: "success",
|
|
1724
|
+
value: create(DiagnosticsSuccessSchema, {
|
|
1725
|
+
path,
|
|
1726
|
+
diagnostics: [],
|
|
1727
|
+
totalDiagnostics: 0,
|
|
1728
|
+
}),
|
|
1729
|
+
},
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function buildDiagnosticsErrorResult(_path: string, error: string) {
|
|
1734
|
+
return create(DiagnosticsResultSchema, {
|
|
1735
|
+
result: {
|
|
1736
|
+
case: "error",
|
|
1737
|
+
value: create(DiagnosticsErrorSchema, { error }),
|
|
1738
|
+
},
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function buildDiagnosticsRejectedResult(path: string, reason: string) {
|
|
1743
|
+
return create(DiagnosticsResultSchema, {
|
|
1744
|
+
result: {
|
|
1745
|
+
case: "rejected",
|
|
1746
|
+
value: create(DiagnosticsRejectedSchema, { path, reason }),
|
|
1747
|
+
},
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function parseToolArgsJson(text: string): unknown {
|
|
1752
|
+
const trimmed = text.trim();
|
|
1753
|
+
if (!trimmed) {
|
|
1754
|
+
return text;
|
|
1755
|
+
}
|
|
1756
|
+
try {
|
|
1757
|
+
const normalized = trimmed
|
|
1758
|
+
.replace(/\bNone\b/g, "null")
|
|
1759
|
+
.replace(/\bTrue\b/g, "true")
|
|
1760
|
+
.replace(/\bFalse\b/g, "false");
|
|
1761
|
+
return Bun.JSON5.parse(normalized);
|
|
1762
|
+
} catch {}
|
|
1763
|
+
return text;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function decodeMcpArgValue(value: Uint8Array): unknown {
|
|
1767
|
+
try {
|
|
1768
|
+
const parsedValue = fromBinary(ValueSchema, value);
|
|
1769
|
+
const jsonValue = toJson(ValueSchema, parsedValue) as JsonValue;
|
|
1770
|
+
if (typeof jsonValue === "string") {
|
|
1771
|
+
return parseToolArgsJson(jsonValue);
|
|
1772
|
+
}
|
|
1773
|
+
return jsonValue;
|
|
1774
|
+
} catch {}
|
|
1775
|
+
const text = new TextDecoder().decode(value);
|
|
1776
|
+
return parseToolArgsJson(text);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function decodeMcpArgsMap(args?: Record<string, Uint8Array>): Record<string, unknown> | undefined {
|
|
1780
|
+
if (!args) {
|
|
1781
|
+
return undefined;
|
|
1782
|
+
}
|
|
1783
|
+
const decoded: Record<string, unknown> = {};
|
|
1784
|
+
for (const [key, value] of Object.entries(args)) {
|
|
1785
|
+
decoded[key] = decodeMcpArgValue(value);
|
|
1786
|
+
}
|
|
1787
|
+
return decoded;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function decodeMcpCall(args: {
|
|
1791
|
+
name: string;
|
|
1792
|
+
args: Record<string, Uint8Array>;
|
|
1793
|
+
toolCallId: string;
|
|
1794
|
+
providerIdentifier: string;
|
|
1795
|
+
toolName: string;
|
|
1796
|
+
}): CursorMcpCall {
|
|
1797
|
+
const decodedArgs: Record<string, unknown> = {};
|
|
1798
|
+
for (const [key, value] of Object.entries(args.args ?? {})) {
|
|
1799
|
+
decodedArgs[key] = decodeMcpArgValue(value);
|
|
1800
|
+
}
|
|
1801
|
+
return {
|
|
1802
|
+
name: args.name,
|
|
1803
|
+
providerIdentifier: args.providerIdentifier,
|
|
1804
|
+
toolName: args.toolName || args.name,
|
|
1805
|
+
toolCallId: args.toolCallId,
|
|
1806
|
+
args: decodedArgs,
|
|
1807
|
+
rawArgs: args.args ?? {},
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function mapTodoStatusValue(status?: number): "pending" | "in_progress" | "completed" {
|
|
1812
|
+
switch (status) {
|
|
1813
|
+
case 2:
|
|
1814
|
+
return "in_progress";
|
|
1815
|
+
case 3:
|
|
1816
|
+
return "completed";
|
|
1817
|
+
default:
|
|
1818
|
+
return "pending";
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
interface CursorTodoItem {
|
|
1823
|
+
id?: string;
|
|
1824
|
+
content?: string;
|
|
1825
|
+
status?: number;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
interface CursorUpdateTodosToolCall {
|
|
1829
|
+
updateTodosToolCall?: { args?: { todos?: CursorTodoItem[] } };
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function buildTodoWriteArgs(toolCall: CursorUpdateTodosToolCall): {
|
|
1833
|
+
todos: Array<{ id?: string; content: string; activeForm: string; status: "pending" | "in_progress" | "completed" }>;
|
|
1834
|
+
} | null {
|
|
1835
|
+
const todos = toolCall.updateTodosToolCall?.args?.todos;
|
|
1836
|
+
if (!todos) return null;
|
|
1837
|
+
return {
|
|
1838
|
+
todos: todos.map(todo => ({
|
|
1839
|
+
id: typeof todo.id === "string" && todo.id.length > 0 ? todo.id : undefined,
|
|
1840
|
+
content: typeof todo.content === "string" ? todo.content : "",
|
|
1841
|
+
activeForm: typeof todo.content === "string" ? todo.content : "",
|
|
1842
|
+
status: mapTodoStatusValue(typeof todo.status === "number" ? todo.status : undefined),
|
|
1843
|
+
})),
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function buildMcpResultFromToolResult(_mcpCall: CursorMcpCall, toolResult: ToolResultMessage) {
|
|
1848
|
+
if (toolResult.isError) {
|
|
1849
|
+
return buildMcpErrorResult(toolResultToText(toolResult) || "MCP tool failed");
|
|
1850
|
+
}
|
|
1851
|
+
const content = toolResult.content.map(item => {
|
|
1852
|
+
if (item.type === "image") {
|
|
1853
|
+
return create(McpToolResultContentItemSchema, {
|
|
1854
|
+
content: {
|
|
1855
|
+
case: "image",
|
|
1856
|
+
value: create(McpImageContentSchema, {
|
|
1857
|
+
data: Uint8Array.from(Buffer.from(item.data, "base64")),
|
|
1858
|
+
mimeType: item.mimeType,
|
|
1859
|
+
}),
|
|
1860
|
+
},
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
return create(McpToolResultContentItemSchema, {
|
|
1864
|
+
content: {
|
|
1865
|
+
case: "text",
|
|
1866
|
+
value: create(McpTextContentSchema, { text: item.text }),
|
|
1867
|
+
},
|
|
1868
|
+
});
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
return create(McpResultSchema, {
|
|
1872
|
+
result: {
|
|
1873
|
+
case: "success",
|
|
1874
|
+
value: create(McpSuccessSchema, {
|
|
1875
|
+
content,
|
|
1876
|
+
isError: false,
|
|
1877
|
+
}),
|
|
1878
|
+
},
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function buildMcpToolNotFoundResult(mcpCall: CursorMcpCall) {
|
|
1883
|
+
return create(McpResultSchema, {
|
|
1884
|
+
result: {
|
|
1885
|
+
case: "toolNotFound",
|
|
1886
|
+
value: create(McpToolNotFoundSchema, { name: mcpCall.toolName, availableTools: [] }),
|
|
1887
|
+
},
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function buildMcpErrorResult(error: string) {
|
|
1892
|
+
return create(McpResultSchema, {
|
|
1893
|
+
result: {
|
|
1894
|
+
case: "error",
|
|
1895
|
+
value: create(McpErrorSchema, { error }),
|
|
1896
|
+
},
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
function processInteractionUpdate(
|
|
1901
|
+
update: any,
|
|
1902
|
+
output: AssistantMessage,
|
|
1903
|
+
stream: AssistantMessageEventStream,
|
|
1904
|
+
state: BlockState,
|
|
1905
|
+
usageState: UsageState,
|
|
1906
|
+
): void {
|
|
1907
|
+
const updateCase = update.message?.case;
|
|
1908
|
+
|
|
1909
|
+
log("interactionUpdate", updateCase, update.message?.value);
|
|
1910
|
+
|
|
1911
|
+
if (updateCase === "textDelta") {
|
|
1912
|
+
state.setFirstTokenTime();
|
|
1913
|
+
const delta = update.message.value.text || "";
|
|
1914
|
+
if (!state.currentTextBlock) {
|
|
1915
|
+
const block: TextContent & { index: number } = {
|
|
1916
|
+
type: "text",
|
|
1917
|
+
text: "",
|
|
1918
|
+
index: output.content.length,
|
|
1919
|
+
};
|
|
1920
|
+
output.content.push(block);
|
|
1921
|
+
state.setTextBlock(block);
|
|
1922
|
+
stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
|
|
1923
|
+
}
|
|
1924
|
+
state.currentTextBlock!.text += delta;
|
|
1925
|
+
const idx = output.content.indexOf(state.currentTextBlock!);
|
|
1926
|
+
stream.push({ type: "text_delta", contentIndex: idx, delta, partial: output });
|
|
1927
|
+
} else if (updateCase === "thinkingDelta") {
|
|
1928
|
+
state.setFirstTokenTime();
|
|
1929
|
+
const delta = update.message.value.text || "";
|
|
1930
|
+
if (!state.currentThinkingBlock) {
|
|
1931
|
+
const block: ThinkingContent & { index: number } = {
|
|
1932
|
+
type: "thinking",
|
|
1933
|
+
thinking: "",
|
|
1934
|
+
index: output.content.length,
|
|
1935
|
+
};
|
|
1936
|
+
output.content.push(block);
|
|
1937
|
+
state.setThinkingBlock(block);
|
|
1938
|
+
stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
|
|
1939
|
+
}
|
|
1940
|
+
state.currentThinkingBlock!.thinking += delta;
|
|
1941
|
+
const idx = output.content.indexOf(state.currentThinkingBlock!);
|
|
1942
|
+
stream.push({ type: "thinking_delta", contentIndex: idx, delta, partial: output });
|
|
1943
|
+
} else if (updateCase === "thinkingCompleted") {
|
|
1944
|
+
if (state.currentThinkingBlock) {
|
|
1945
|
+
const idx = output.content.indexOf(state.currentThinkingBlock);
|
|
1946
|
+
delete (state.currentThinkingBlock as any).index;
|
|
1947
|
+
stream.push({
|
|
1948
|
+
type: "thinking_end",
|
|
1949
|
+
contentIndex: idx,
|
|
1950
|
+
content: state.currentThinkingBlock.thinking,
|
|
1951
|
+
partial: output,
|
|
1952
|
+
});
|
|
1953
|
+
state.setThinkingBlock(null);
|
|
1954
|
+
}
|
|
1955
|
+
} else if (updateCase === "toolCallStarted") {
|
|
1956
|
+
const toolCall = update.message.value.toolCall;
|
|
1957
|
+
if (toolCall) {
|
|
1958
|
+
const mcpCall = toolCall.mcpToolCall;
|
|
1959
|
+
if (mcpCall) {
|
|
1960
|
+
const args = mcpCall.args || {};
|
|
1961
|
+
const block: ToolCallState = {
|
|
1962
|
+
type: "toolCall",
|
|
1963
|
+
id: args.toolCallId || crypto.randomUUID(),
|
|
1964
|
+
name: args.name || args.toolName || "",
|
|
1965
|
+
arguments: {},
|
|
1966
|
+
index: output.content.length,
|
|
1967
|
+
partialJson: "",
|
|
1968
|
+
kind: "mcp",
|
|
1969
|
+
};
|
|
1970
|
+
output.content.push(block);
|
|
1971
|
+
state.setToolCall(block);
|
|
1972
|
+
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
const todoArgs = buildTodoWriteArgs(toolCall);
|
|
1977
|
+
if (todoArgs) {
|
|
1978
|
+
const callId = update.message.value.callId || crypto.randomUUID();
|
|
1979
|
+
const block: ToolCallState = {
|
|
1980
|
+
type: "toolCall",
|
|
1981
|
+
id: callId,
|
|
1982
|
+
name: "todo_write",
|
|
1983
|
+
arguments: todoArgs,
|
|
1984
|
+
index: output.content.length,
|
|
1985
|
+
kind: "todo_write",
|
|
1986
|
+
};
|
|
1987
|
+
output.content.push(block);
|
|
1988
|
+
state.setToolCall(block);
|
|
1989
|
+
stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output });
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
} else if (updateCase === "toolCallDelta" || updateCase === "partialToolCall") {
|
|
1993
|
+
if (state.currentToolCall?.kind === "mcp") {
|
|
1994
|
+
const delta = update.message.value.argsTextDelta || "";
|
|
1995
|
+
state.currentToolCall.partialJson = `${state.currentToolCall.partialJson ?? ""}${delta}`;
|
|
1996
|
+
state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson ?? "");
|
|
1997
|
+
const idx = output.content.indexOf(state.currentToolCall);
|
|
1998
|
+
stream.push({ type: "toolcall_delta", contentIndex: idx, delta, partial: output });
|
|
1999
|
+
}
|
|
2000
|
+
} else if (updateCase === "toolCallCompleted") {
|
|
2001
|
+
if (state.currentToolCall) {
|
|
2002
|
+
const toolCall = update.message.value.toolCall;
|
|
2003
|
+
if (state.currentToolCall.kind === "mcp") {
|
|
2004
|
+
const decodedArgs = decodeMcpArgsMap(toolCall?.mcpToolCall?.args?.args);
|
|
2005
|
+
if (decodedArgs) {
|
|
2006
|
+
state.currentToolCall.arguments = decodedArgs;
|
|
2007
|
+
}
|
|
2008
|
+
} else if (state.currentToolCall.kind === "todo_write" && toolCall) {
|
|
2009
|
+
const todoArgs = buildTodoWriteArgs(toolCall);
|
|
2010
|
+
if (todoArgs) {
|
|
2011
|
+
state.currentToolCall.arguments = todoArgs;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
const idx = output.content.indexOf(state.currentToolCall);
|
|
2015
|
+
delete (state.currentToolCall as any).partialJson;
|
|
2016
|
+
delete (state.currentToolCall as any).index;
|
|
2017
|
+
delete (state.currentToolCall as any).kind;
|
|
2018
|
+
stream.push({ type: "toolcall_end", contentIndex: idx, toolCall: state.currentToolCall, partial: output });
|
|
2019
|
+
state.setToolCall(null);
|
|
2020
|
+
}
|
|
2021
|
+
} else if (updateCase === "turnEnded") {
|
|
2022
|
+
output.stopReason = "stop";
|
|
2023
|
+
} else if (updateCase === "tokenDelta") {
|
|
2024
|
+
const tokenDelta = update.message.value;
|
|
2025
|
+
usageState.sawTokenDelta = true;
|
|
2026
|
+
output.usage.output += tokenDelta.tokens || 0;
|
|
2027
|
+
output.usage.totalTokens = output.usage.input + output.usage.output;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function handleConversationCheckpointUpdate(
|
|
2032
|
+
checkpoint: ConversationStateStructure,
|
|
2033
|
+
output: AssistantMessage,
|
|
2034
|
+
usageState: UsageState,
|
|
2035
|
+
onConversationCheckpoint?: (checkpoint: ConversationStateStructure) => void,
|
|
2036
|
+
): void {
|
|
2037
|
+
onConversationCheckpoint?.(checkpoint);
|
|
2038
|
+
if (usageState.sawTokenDelta) {
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
const usedTokens = checkpoint.tokenDetails?.usedTokens ?? 0;
|
|
2042
|
+
if (usedTokens <= 0) {
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
if (output.usage.output !== usedTokens) {
|
|
2046
|
+
output.usage.output = usedTokens;
|
|
2047
|
+
output.usage.totalTokens = output.usage.input + output.usage.output;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
function createBlobId(data: Uint8Array): Uint8Array {
|
|
2052
|
+
return new Uint8Array(createHash("sha256").update(data).digest());
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function storeCursorBlob(blobStore: Map<string, Uint8Array>, data: Uint8Array): Uint8Array {
|
|
2056
|
+
const blobId = createBlobId(data);
|
|
2057
|
+
blobStore.set(Buffer.from(blobId).toString("hex"), data);
|
|
2058
|
+
return blobId;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function readCursorBlob(blobStore: Map<string, Uint8Array>, blobId: Uint8Array): Uint8Array {
|
|
2062
|
+
const data = blobStore.get(Buffer.from(blobId).toString("hex"));
|
|
2063
|
+
if (!data) {
|
|
2064
|
+
throw new Error("Cursor blob not found");
|
|
2065
|
+
}
|
|
2066
|
+
return data;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
const CURSOR_NATIVE_TOOL_NAMES = new Set(["bash", "read", "write", "delete", "ls", "grep", "lsp", "todo_write"]);
|
|
2070
|
+
|
|
2071
|
+
function buildMcpToolDefinitions(tools: Tool[] | undefined): McpToolDefinition[] {
|
|
2072
|
+
if (!tools || tools.length === 0) {
|
|
2073
|
+
return [];
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const advertisedTools = tools.filter(tool => !CURSOR_NATIVE_TOOL_NAMES.has(tool.name));
|
|
2077
|
+
if (advertisedTools.length === 0) {
|
|
2078
|
+
return [];
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
return advertisedTools.map(tool => {
|
|
2082
|
+
const jsonSchema = toolWireSchema(tool);
|
|
2083
|
+
const schemaValue: JsonValue =
|
|
2084
|
+
jsonSchema && typeof jsonSchema === "object"
|
|
2085
|
+
? (jsonSchema as JsonValue)
|
|
2086
|
+
: { type: "object", properties: {}, required: [] };
|
|
2087
|
+
const inputSchema = toBinary(ValueSchema, fromJson(ValueSchema, schemaValue));
|
|
2088
|
+
return create(McpToolDefinitionSchema, {
|
|
2089
|
+
name: tool.name,
|
|
2090
|
+
description: tool.description || "",
|
|
2091
|
+
providerIdentifier: "pi-agent",
|
|
2092
|
+
toolName: tool.name,
|
|
2093
|
+
inputSchema,
|
|
2094
|
+
});
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
/**
|
|
2099
|
+
* Extract text content from a user or developer message.
|
|
2100
|
+
*/
|
|
2101
|
+
function extractUserMessageText(msg: Message): string {
|
|
2102
|
+
if (msg.role !== "user" && msg.role !== "developer") return "";
|
|
2103
|
+
const content = msg.content;
|
|
2104
|
+
if (typeof content === "string") return content.trim();
|
|
2105
|
+
const text = content
|
|
2106
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
2107
|
+
.map(c => c.text)
|
|
2108
|
+
.join("\n");
|
|
2109
|
+
return text.trim();
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
function hasUserMessageImages(msg: Message): boolean {
|
|
2113
|
+
return (
|
|
2114
|
+
(msg.role === "user" || msg.role === "developer") &&
|
|
2115
|
+
Array.isArray(msg.content) &&
|
|
2116
|
+
msg.content.some(item => item.type === "image")
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
type CursorRootPromptContentPart = { type: "text"; text: string } | { type: "image"; image: string; mediaType: string };
|
|
2121
|
+
|
|
2122
|
+
function buildCursorRootPromptContent(content: string | (TextContent | ImageContent)[]): CursorRootPromptContentPart[] {
|
|
2123
|
+
if (typeof content === "string") {
|
|
2124
|
+
const text = content.trim();
|
|
2125
|
+
return text ? [{ type: "text", text }] : [];
|
|
2126
|
+
}
|
|
2127
|
+
const parts: CursorRootPromptContentPart[] = [];
|
|
2128
|
+
for (const item of content) {
|
|
2129
|
+
if (item.type === "text") {
|
|
2130
|
+
const text = item.text.trim();
|
|
2131
|
+
if (text) {
|
|
2132
|
+
parts.push({ type: "text", text });
|
|
2133
|
+
}
|
|
2134
|
+
} else {
|
|
2135
|
+
parts.push({ type: "image", image: item.data, mediaType: item.mimeType });
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
return parts;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function cursorUserContentKey(content: string | (TextContent | ImageContent)[]): string {
|
|
2142
|
+
if (typeof content === "string") {
|
|
2143
|
+
return content.trim();
|
|
2144
|
+
}
|
|
2145
|
+
const hash = createHash("sha256");
|
|
2146
|
+
for (const item of content) {
|
|
2147
|
+
hash.update(item.type);
|
|
2148
|
+
if (item.type === "text") {
|
|
2149
|
+
hash.update(item.text);
|
|
2150
|
+
} else {
|
|
2151
|
+
hash.update(item.mimeType);
|
|
2152
|
+
hash.update(item.data);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return hash.digest("hex");
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/**
|
|
2159
|
+
* Extract text content from an assistant message.
|
|
2160
|
+
*/
|
|
2161
|
+
function extractAssistantMessageText(msg: Message): string {
|
|
2162
|
+
if (msg.role !== "assistant") return "";
|
|
2163
|
+
if (!Array.isArray(msg.content)) return "";
|
|
2164
|
+
return msg.content
|
|
2165
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
2166
|
+
.map(c => c.text)
|
|
2167
|
+
.join("\n");
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/**
|
|
2171
|
+
* Derive a stable, UUID-formatted `message_id` from a content key.
|
|
2172
|
+
* Ensures identical historical messages hash to the same blob IDs across
|
|
2173
|
+
* requests, so `conversationBlobStores` does not grow unboundedly and
|
|
2174
|
+
* unchanged history reuses existing blob IDs.
|
|
2175
|
+
*/
|
|
2176
|
+
type CursorMessageId = `${string}-${string}-${string}-${string}-${string}`;
|
|
2177
|
+
|
|
2178
|
+
function deterministicMessageId(key: string): CursorMessageId {
|
|
2179
|
+
const hex = createHash("sha256").update(key).digest("hex");
|
|
2180
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/**
|
|
2184
|
+
* Index of the last user/developer message in `messages`, or -1 if none.
|
|
2185
|
+
* Used to exclude the current user turn from history builders — it goes in
|
|
2186
|
+
* `ConversationActionSchema.userMessageAction`, not in history structures.
|
|
2187
|
+
*/
|
|
2188
|
+
function findLastUserMessageIndex(messages: Message[]): number {
|
|
2189
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2190
|
+
const role = messages[i].role;
|
|
2191
|
+
if (role === "user" || role === "developer") {
|
|
2192
|
+
return i;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
return -1;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* Build `ConversationStateStructure.rootPromptMessagesJson` blob IDs for the
|
|
2200
|
+
* system prompt plus prior conversation history, as JSON blobs matching
|
|
2201
|
+
* Cursor's internal Vercel-AI-SDK-shaped message format.
|
|
2202
|
+
*
|
|
2203
|
+
* Cursor's server uses `rootPromptMessagesJson` (not `turns[]`) to build the
|
|
2204
|
+
* actual model prompt. `turns[]` is UI/display metadata. Without populating
|
|
2205
|
+
* this field, multi-turn conversations lose prior context — the model sees
|
|
2206
|
+
* only an empty placeholder where historical user turns should be.
|
|
2207
|
+
* The last user message is excluded because it is sent in the action.
|
|
2208
|
+
*/
|
|
2209
|
+
/**
|
|
2210
|
+
* Build one Cursor system-message JSON blob per ordered system prompt. Emitting separate blobs
|
|
2211
|
+
* (rather than a single `\n\n`-joined string) lets Cursor's blob cache hit independently per
|
|
2212
|
+
* entry: changing only the last prompt does not invalidate earlier blob ids, so the prefix
|
|
2213
|
+
* up to the changed prompt remains cached on the server side.
|
|
2214
|
+
*
|
|
2215
|
+
* When no system prompts are provided, returns a single default greeting so we never emit
|
|
2216
|
+
* an empty `rootPromptMessagesJson` head.
|
|
2217
|
+
*/
|
|
2218
|
+
export function buildCursorSystemPromptJsons(systemPrompt: readonly string[] | undefined): string[] {
|
|
2219
|
+
const systemPrompts = normalizeSystemPrompts(systemPrompt);
|
|
2220
|
+
if (systemPrompts.length === 0) {
|
|
2221
|
+
return [JSON.stringify({ role: "system", content: "You are a helpful assistant." })];
|
|
2222
|
+
}
|
|
2223
|
+
return systemPrompts.map(content => JSON.stringify({ role: "system", content }));
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function buildRootPromptMessagesJson(
|
|
2227
|
+
messages: Message[],
|
|
2228
|
+
systemPromptIds: Uint8Array[],
|
|
2229
|
+
blobStore: Map<string, Uint8Array>,
|
|
2230
|
+
): Uint8Array[] {
|
|
2231
|
+
const entries: Uint8Array[] = [...systemPromptIds];
|
|
2232
|
+
const lastUserIdx = findLastUserMessageIndex(messages);
|
|
2233
|
+
|
|
2234
|
+
const pushJson = (obj: unknown) => {
|
|
2235
|
+
const bytes = new TextEncoder().encode(JSON.stringify(obj));
|
|
2236
|
+
entries.push(storeCursorBlob(blobStore, bytes));
|
|
2237
|
+
};
|
|
2238
|
+
|
|
2239
|
+
for (let i = 0; i < messages.length; i++) {
|
|
2240
|
+
if (i === lastUserIdx) break;
|
|
2241
|
+
const msg = messages[i];
|
|
2242
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
2243
|
+
const content = buildCursorRootPromptContent(msg.content);
|
|
2244
|
+
if (content.length === 0) continue;
|
|
2245
|
+
pushJson({ role: "user", content });
|
|
2246
|
+
} else if (msg.role === "assistant") {
|
|
2247
|
+
const text = extractAssistantMessageText(msg);
|
|
2248
|
+
if (!text) continue;
|
|
2249
|
+
pushJson({ role: "assistant", content: [{ type: "text", text }] });
|
|
2250
|
+
} else if (msg.role === "toolResult") {
|
|
2251
|
+
const text = toolResultToText(msg);
|
|
2252
|
+
if (!text) continue;
|
|
2253
|
+
pushJson({
|
|
2254
|
+
role: "user",
|
|
2255
|
+
content: [{ type: "text", text: `[Tool Result]\n${text}` }],
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
return entries;
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
/**
|
|
2264
|
+
* Convert context.messages to Cursor's ConversationTurnStructure blob IDs.
|
|
2265
|
+
* Groups messages into turns: each turn is a user message followed by the assistant's response.
|
|
2266
|
+
* Excludes the last user message (which goes in the action).
|
|
2267
|
+
*
|
|
2268
|
+
* Each `AgentConversationTurnStructure.user_message`, `steps[]`, and the outer
|
|
2269
|
+
* `ConversationStateStructure.turns[]` entry is a blob ID into `blobStore`.
|
|
2270
|
+
*/
|
|
2271
|
+
function buildConversationTurns(messages: Message[], blobStore: Map<string, Uint8Array>): Uint8Array[] {
|
|
2272
|
+
const turns: Uint8Array[] = [];
|
|
2273
|
+
|
|
2274
|
+
// Find turn boundaries - each turn starts with a user message
|
|
2275
|
+
let i = 0;
|
|
2276
|
+
while (i < messages.length) {
|
|
2277
|
+
const msg = messages[i];
|
|
2278
|
+
|
|
2279
|
+
// Skip non-user messages at the start
|
|
2280
|
+
if (msg.role !== "user" && msg.role !== "developer") {
|
|
2281
|
+
i++;
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// Check if this is the last user message (which goes in the action, not turns)
|
|
2286
|
+
let isLastUserMessage = true;
|
|
2287
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
2288
|
+
if (messages[j].role === "user" || messages[j].role === "developer") {
|
|
2289
|
+
isLastUserMessage = false;
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
if (isLastUserMessage) {
|
|
2294
|
+
break;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// Create and serialize user message
|
|
2298
|
+
const userText = extractUserMessageText(msg);
|
|
2299
|
+
if (userText.length === 0 && !hasUserMessageImages(msg)) {
|
|
2300
|
+
i++;
|
|
2301
|
+
continue;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
const userMessage = createCursorUserMessage(
|
|
2305
|
+
msg.content,
|
|
2306
|
+
userText,
|
|
2307
|
+
deterministicMessageId(`u:${turns.length}:${cursorUserContentKey(msg.content)}`),
|
|
2308
|
+
);
|
|
2309
|
+
const userMessageBytes = toBinary(UserMessageSchema, userMessage);
|
|
2310
|
+
const userMessageBlobId = storeCursorBlob(blobStore, userMessageBytes);
|
|
2311
|
+
|
|
2312
|
+
// Collect and serialize steps until next user message
|
|
2313
|
+
const stepBlobIds: Uint8Array[] = [];
|
|
2314
|
+
i++;
|
|
2315
|
+
|
|
2316
|
+
while (i < messages.length && messages[i].role !== "user" && messages[i].role !== "developer") {
|
|
2317
|
+
const stepMsg = messages[i];
|
|
2318
|
+
|
|
2319
|
+
if (stepMsg.role === "assistant") {
|
|
2320
|
+
const text = extractAssistantMessageText(stepMsg);
|
|
2321
|
+
if (text) {
|
|
2322
|
+
const step = create(ConversationStepSchema, {
|
|
2323
|
+
message: {
|
|
2324
|
+
case: "assistantMessage",
|
|
2325
|
+
value: create(AssistantMessageSchema, { text }),
|
|
2326
|
+
},
|
|
2327
|
+
});
|
|
2328
|
+
stepBlobIds.push(storeCursorBlob(blobStore, toBinary(ConversationStepSchema, step)));
|
|
2329
|
+
}
|
|
2330
|
+
} else if (stepMsg.role === "toolResult") {
|
|
2331
|
+
// Include tool results as assistant text for context
|
|
2332
|
+
const text = toolResultToText(stepMsg);
|
|
2333
|
+
if (text) {
|
|
2334
|
+
const step = create(ConversationStepSchema, {
|
|
2335
|
+
message: {
|
|
2336
|
+
case: "assistantMessage",
|
|
2337
|
+
value: create(AssistantMessageSchema, { text: `[Tool Result]\n${text}` }),
|
|
2338
|
+
},
|
|
2339
|
+
});
|
|
2340
|
+
stepBlobIds.push(storeCursorBlob(blobStore, toBinary(ConversationStepSchema, step)));
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
i++;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// Create the serialized turn using Structure types. The bytes fields
|
|
2348
|
+
// (user_message, steps) are blob IDs resolved through the KV store.
|
|
2349
|
+
const agentTurn = create(AgentConversationTurnStructureSchema, {
|
|
2350
|
+
userMessage: userMessageBlobId,
|
|
2351
|
+
steps: stepBlobIds,
|
|
2352
|
+
});
|
|
2353
|
+
const turn = create(ConversationTurnStructureSchema, {
|
|
2354
|
+
turn: {
|
|
2355
|
+
case: "agentConversationTurn",
|
|
2356
|
+
value: agentTurn,
|
|
2357
|
+
},
|
|
2358
|
+
});
|
|
2359
|
+
turns.push(storeCursorBlob(blobStore, toBinary(ConversationTurnStructureSchema, turn)));
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
return turns;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/** Exported for tests: decodes Cursor history blobs built from conversation messages. */
|
|
2366
|
+
export function buildCursorHistoryForTest(messages: Message[]): {
|
|
2367
|
+
rootPromptMessagesJson: unknown[];
|
|
2368
|
+
turnUserMessagesJson: JsonValue[];
|
|
2369
|
+
} {
|
|
2370
|
+
const blobStore = new Map<string, Uint8Array>();
|
|
2371
|
+
const rootPromptMessagesJson = buildRootPromptMessagesJson(messages, [], blobStore).map(blobId =>
|
|
2372
|
+
JSON.parse(new TextDecoder().decode(readCursorBlob(blobStore, blobId))),
|
|
2373
|
+
);
|
|
2374
|
+
const turnUserMessagesJson: JsonValue[] = [];
|
|
2375
|
+
for (const turnBlobId of buildConversationTurns(messages, blobStore)) {
|
|
2376
|
+
const turn = fromBinary(ConversationTurnStructureSchema, readCursorBlob(blobStore, turnBlobId));
|
|
2377
|
+
if (turn.turn.case !== "agentConversationTurn") {
|
|
2378
|
+
continue;
|
|
2379
|
+
}
|
|
2380
|
+
const userMessage = fromBinary(UserMessageSchema, readCursorBlob(blobStore, turn.turn.value.userMessage));
|
|
2381
|
+
turnUserMessagesJson.push(toJson(UserMessageSchema, userMessage));
|
|
2382
|
+
}
|
|
2383
|
+
return { rootPromptMessagesJson, turnUserMessagesJson };
|
|
2384
|
+
}
|
|
2385
|
+
function createCursorUserMessage(
|
|
2386
|
+
content: string | (TextContent | ImageContent)[],
|
|
2387
|
+
text: string,
|
|
2388
|
+
messageId = crypto.randomUUID(),
|
|
2389
|
+
) {
|
|
2390
|
+
const images = typeof content === "string" ? [] : extractImages(content);
|
|
2391
|
+
return create(UserMessageSchema, {
|
|
2392
|
+
text,
|
|
2393
|
+
messageId,
|
|
2394
|
+
...(images.length > 0
|
|
2395
|
+
? {
|
|
2396
|
+
selectedContext: create(SelectedContextSchema, {
|
|
2397
|
+
selectedImages: images,
|
|
2398
|
+
}),
|
|
2399
|
+
}
|
|
2400
|
+
: {}),
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function extractImages(content: (TextContent | ImageContent)[]) {
|
|
2405
|
+
return content
|
|
2406
|
+
.filter((item): item is ImageContent => item.type === "image")
|
|
2407
|
+
.map(image =>
|
|
2408
|
+
create(SelectedImageSchema, {
|
|
2409
|
+
uuid: crypto.randomUUID(),
|
|
2410
|
+
mimeType: image.mimeType,
|
|
2411
|
+
dataOrBlobId: {
|
|
2412
|
+
case: "data",
|
|
2413
|
+
value: Uint8Array.from(Buffer.from(image.data, "base64")),
|
|
2414
|
+
},
|
|
2415
|
+
}),
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function buildGrpcRequest(
|
|
2420
|
+
model: Model<"cursor-agent">,
|
|
2421
|
+
context: Context,
|
|
2422
|
+
options: CursorOptions | undefined,
|
|
2423
|
+
state: {
|
|
2424
|
+
conversationId: string;
|
|
2425
|
+
blobStore: Map<string, Uint8Array>;
|
|
2426
|
+
conversationState?: ConversationStateStructure;
|
|
2427
|
+
},
|
|
2428
|
+
): {
|
|
2429
|
+
requestBytes: Uint8Array;
|
|
2430
|
+
blobStore: Map<string, Uint8Array>;
|
|
2431
|
+
conversationState: ConversationStateStructure;
|
|
2432
|
+
} {
|
|
2433
|
+
const blobStore = state.blobStore;
|
|
2434
|
+
|
|
2435
|
+
const systemPromptIds = buildCursorSystemPromptJsons(context.systemPrompt).map(json =>
|
|
2436
|
+
storeCursorBlob(blobStore, new TextEncoder().encode(json)),
|
|
2437
|
+
);
|
|
2438
|
+
|
|
2439
|
+
const lastMessage = context.messages[context.messages.length - 1];
|
|
2440
|
+
let userContent: string | (TextContent | ImageContent)[] | undefined;
|
|
2441
|
+
let userText = "";
|
|
2442
|
+
let hasUserImages = false;
|
|
2443
|
+
if (lastMessage?.role === "user" || lastMessage?.role === "developer") {
|
|
2444
|
+
userContent = lastMessage.content;
|
|
2445
|
+
if (typeof userContent === "string") {
|
|
2446
|
+
userText = userContent.trim();
|
|
2447
|
+
} else {
|
|
2448
|
+
userText = extractText(userContent);
|
|
2449
|
+
hasUserImages = hasImages(userContent);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
const action = create(ConversationActionSchema, {
|
|
2454
|
+
action:
|
|
2455
|
+
userContent && (userText.trim().length > 0 || hasUserImages)
|
|
2456
|
+
? {
|
|
2457
|
+
case: "userMessageAction",
|
|
2458
|
+
value: create(UserMessageActionSchema, {
|
|
2459
|
+
userMessage: createCursorUserMessage(userContent, userText),
|
|
2460
|
+
}),
|
|
2461
|
+
}
|
|
2462
|
+
: {
|
|
2463
|
+
case: "resumeAction",
|
|
2464
|
+
value: create(ResumeActionSchema, {}),
|
|
2465
|
+
},
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
// Build conversation turns from prior messages (excluding the last user message).
|
|
2469
|
+
// This populates the UI-side history view (`turns[]`).
|
|
2470
|
+
const turns = buildConversationTurns(context.messages, blobStore);
|
|
2471
|
+
|
|
2472
|
+
// Build `rootPromptMessagesJson` from prior messages. Cursor's server uses this
|
|
2473
|
+
// field (not `turns[]`) to construct the actual model prompt; if we only send the
|
|
2474
|
+
// system prompt here, multi-turn conversations lose prior context and the model
|
|
2475
|
+
// sees only the current user message.
|
|
2476
|
+
const rootPromptMessagesJson = buildRootPromptMessagesJson(context.messages, systemPromptIds, blobStore);
|
|
2477
|
+
|
|
2478
|
+
// Preserve cached non-history state fields (todos, file states, summaries, etc.)
|
|
2479
|
+
// when the system prompt is unchanged; otherwise start fresh.
|
|
2480
|
+
const cachedPromptHead = state.conversationState?.rootPromptMessagesJson?.slice(0, systemPromptIds.length) ?? [];
|
|
2481
|
+
const hasMatchingPrompt =
|
|
2482
|
+
cachedPromptHead.length === systemPromptIds.length &&
|
|
2483
|
+
systemPromptIds.every((id, idx) => Buffer.from(cachedPromptHead[idx]).equals(id));
|
|
2484
|
+
const baseState =
|
|
2485
|
+
state.conversationState && hasMatchingPrompt
|
|
2486
|
+
? state.conversationState
|
|
2487
|
+
: create(ConversationStateStructureSchema, {
|
|
2488
|
+
rootPromptMessagesJson: systemPromptIds,
|
|
2489
|
+
turns: [],
|
|
2490
|
+
todos: [],
|
|
2491
|
+
pendingToolCalls: [],
|
|
2492
|
+
previousWorkspaceUris: [],
|
|
2493
|
+
fileStates: {},
|
|
2494
|
+
fileStatesV2: {},
|
|
2495
|
+
summaryArchives: [],
|
|
2496
|
+
turnTimings: [],
|
|
2497
|
+
subagentStates: {},
|
|
2498
|
+
selfSummaryCount: 0,
|
|
2499
|
+
readPaths: [],
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
// Always override `rootPromptMessagesJson` and `turns` with content freshly built from
|
|
2503
|
+
// `context.messages`. The server-echoed checkpoint replaces historical user entries
|
|
2504
|
+
// with empty placeholders, so we cannot rely on the cached `rootPromptMessagesJson`.
|
|
2505
|
+
const conversationState = create(ConversationStateStructureSchema, {
|
|
2506
|
+
...baseState,
|
|
2507
|
+
rootPromptMessagesJson,
|
|
2508
|
+
turns,
|
|
2509
|
+
});
|
|
2510
|
+
|
|
2511
|
+
const modelDetails = create(ModelDetailsSchema, {
|
|
2512
|
+
modelId: model.id,
|
|
2513
|
+
displayModelId: model.id,
|
|
2514
|
+
displayName: model.name,
|
|
2515
|
+
});
|
|
2516
|
+
|
|
2517
|
+
const runRequest = create(AgentRunRequestSchema, {
|
|
2518
|
+
conversationState,
|
|
2519
|
+
action,
|
|
2520
|
+
modelDetails,
|
|
2521
|
+
conversationId: state.conversationId,
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2524
|
+
options?.onPayload?.(runRequest);
|
|
2525
|
+
|
|
2526
|
+
// Tools are sent later via requestContext (exec handshake)
|
|
2527
|
+
|
|
2528
|
+
if (options?.customSystemPrompt) {
|
|
2529
|
+
runRequest.customSystemPrompt = options.customSystemPrompt;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
const clientMessage = create(AgentClientMessageSchema, {
|
|
2533
|
+
message: { case: "runRequest", value: runRequest },
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2536
|
+
const requestBytes = toBinary(AgentClientMessageSchema, clientMessage);
|
|
2537
|
+
|
|
2538
|
+
const toolNames = context.tools?.map(tool => tool.name) ?? [];
|
|
2539
|
+
const detail =
|
|
2540
|
+
$env.DEBUG_CURSOR === "2"
|
|
2541
|
+
? ` ${JSON.stringify(clientMessage.message.value, debugReplacer, 2)?.slice(0, 2000)}`
|
|
2542
|
+
: "";
|
|
2543
|
+
log("info", "builtRunRequest", {
|
|
2544
|
+
bytes: requestBytes.length,
|
|
2545
|
+
tools: toolNames.length,
|
|
2546
|
+
toolNames: toolNames.slice(0, 20),
|
|
2547
|
+
detail: detail || undefined,
|
|
2548
|
+
});
|
|
2549
|
+
|
|
2550
|
+
return { requestBytes, blobStore, conversationState };
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
function hasImages(content: (TextContent | ImageContent)[]): boolean {
|
|
2554
|
+
return content.some(item => item.type === "image");
|
|
2555
|
+
}
|
|
2556
|
+
function extractText(content: (TextContent | ImageContent)[]): string {
|
|
2557
|
+
return content
|
|
2558
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
2559
|
+
.map(c => c.text)
|
|
2560
|
+
.join("\n");
|
|
2561
|
+
}
|