@velum-labs/cursorkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/DISCLAIMER.md +12 -0
  2. package/README.md +157 -0
  3. package/dist/src/agentTools/diff.d.ts +11 -0
  4. package/dist/src/agentTools/diff.js +88 -0
  5. package/dist/src/agentTools/policy.d.ts +3 -0
  6. package/dist/src/agentTools/policy.js +12 -0
  7. package/dist/src/agentTools/registry.d.ts +114 -0
  8. package/dist/src/agentTools/registry.js +663 -0
  9. package/dist/src/agentTools/results.d.ts +14 -0
  10. package/dist/src/agentTools/results.js +117 -0
  11. package/dist/src/agentTools/schemas.d.ts +3 -0
  12. package/dist/src/agentTools/schemas.js +89 -0
  13. package/dist/src/agentTools/surface.d.ts +11 -0
  14. package/dist/src/agentTools/surface.js +251 -0
  15. package/dist/src/certs.d.ts +8 -0
  16. package/dist/src/certs.js +34 -0
  17. package/dist/src/ck.d.ts +2 -0
  18. package/dist/src/ck.js +6 -0
  19. package/dist/src/ckLauncher.d.ts +150 -0
  20. package/dist/src/ckLauncher.js +1496 -0
  21. package/dist/src/cli.d.ts +2 -0
  22. package/dist/src/cli.js +265 -0
  23. package/dist/src/config.d.ts +52 -0
  24. package/dist/src/config.js +210 -0
  25. package/dist/src/connectEnvelope.d.ts +16 -0
  26. package/dist/src/connectEnvelope.js +70 -0
  27. package/dist/src/desktop.d.ts +19 -0
  28. package/dist/src/desktop.js +167 -0
  29. package/dist/src/desktopConnectProxy.d.ts +26 -0
  30. package/dist/src/desktopConnectProxy.js +175 -0
  31. package/dist/src/extensions/index.d.ts +2 -0
  32. package/dist/src/extensions/index.js +1 -0
  33. package/dist/src/extensions/registry.d.ts +8 -0
  34. package/dist/src/extensions/registry.js +52 -0
  35. package/dist/src/extensions/types.d.ts +42 -0
  36. package/dist/src/extensions/types.js +1 -0
  37. package/dist/src/fixtures/modelFusion.d.ts +103 -0
  38. package/dist/src/fixtures/modelFusion.js +404 -0
  39. package/dist/src/fixtures/replay.d.ts +9 -0
  40. package/dist/src/fixtures/replay.js +41 -0
  41. package/dist/src/fixtures/sanitizer.d.ts +9 -0
  42. package/dist/src/fixtures/sanitizer.js +43 -0
  43. package/dist/src/fixtures/schema.d.ts +38 -0
  44. package/dist/src/fixtures/schema.js +33 -0
  45. package/dist/src/gen/agent/v1/agent_pb.d.ts +21577 -0
  46. package/dist/src/gen/agent/v1/agent_pb.js +5325 -0
  47. package/dist/src/gen/aiserver/v1/aiserver_pb.d.ts +135242 -0
  48. package/dist/src/gen/aiserver/v1/aiserver_pb.js +34430 -0
  49. package/dist/src/gen/anyrun/v1/anyrun_pb.d.ts +1163 -0
  50. package/dist/src/gen/anyrun/v1/anyrun_pb.js +374 -0
  51. package/dist/src/gen/google/protobuf/google_pb.d.ts +142 -0
  52. package/dist/src/gen/google/protobuf/google_pb.js +54 -0
  53. package/dist/src/gen/internapi/v1/internapi_pb.d.ts +121 -0
  54. package/dist/src/gen/internapi/v1/internapi_pb.js +79 -0
  55. package/dist/src/logger.d.ts +8 -0
  56. package/dist/src/logger.js +37 -0
  57. package/dist/src/modelFusion/cursorHarness.d.ts +146 -0
  58. package/dist/src/modelFusion/cursorHarness.js +647 -0
  59. package/dist/src/modelFusion/index.d.ts +4 -0
  60. package/dist/src/modelFusion/index.js +2 -0
  61. package/dist/src/models/registry.d.ts +22 -0
  62. package/dist/src/models/registry.js +30 -0
  63. package/dist/src/proto.d.ts +13 -0
  64. package/dist/src/proto.js +61 -0
  65. package/dist/src/providers/openai.d.ts +64 -0
  66. package/dist/src/providers/openai.js +355 -0
  67. package/dist/src/redaction.d.ts +4 -0
  68. package/dist/src/redaction.js +65 -0
  69. package/dist/src/routeInventory.d.ts +16 -0
  70. package/dist/src/routeInventory.js +39 -0
  71. package/dist/src/routes.d.ts +37 -0
  72. package/dist/src/routes.js +227 -0
  73. package/dist/src/server.d.ts +50 -0
  74. package/dist/src/server.js +1353 -0
  75. package/dist/src/services/agent.d.ts +1 -0
  76. package/dist/src/services/agent.js +7 -0
  77. package/dist/src/services/agentRun.d.ts +60 -0
  78. package/dist/src/services/agentRun.js +391 -0
  79. package/dist/src/services/chat.d.ts +11 -0
  80. package/dist/src/services/chat.js +47 -0
  81. package/dist/src/services/models.d.ts +10 -0
  82. package/dist/src/services/models.js +216 -0
  83. package/dist/src/services/serverConfig.d.ts +2 -0
  84. package/dist/src/services/serverConfig.js +19 -0
  85. package/dist/src/testing/artifacts.d.ts +14 -0
  86. package/dist/src/testing/artifacts.js +92 -0
  87. package/dist/src/testing/cli.d.ts +4 -0
  88. package/dist/src/testing/cli.js +192 -0
  89. package/dist/src/testing/localBackend.d.ts +24 -0
  90. package/dist/src/testing/localBackend.js +310 -0
  91. package/dist/src/testing/processRunner.d.ts +7 -0
  92. package/dist/src/testing/processRunner.js +74 -0
  93. package/dist/src/testing/runner.d.ts +9 -0
  94. package/dist/src/testing/runner.js +85 -0
  95. package/dist/src/testing/scenarios.d.ts +3 -0
  96. package/dist/src/testing/scenarios.js +2535 -0
  97. package/dist/src/testing/types.d.ts +66 -0
  98. package/dist/src/testing/types.js +1 -0
  99. package/dist/src/tools/baselineInventory.d.ts +12 -0
  100. package/dist/src/tools/baselineInventory.js +680 -0
  101. package/dist/src/tools/checkModelFusionProtocol.d.ts +1 -0
  102. package/dist/src/tools/checkModelFusionProtocol.js +274 -0
  103. package/dist/src/tools/checkReleasePublishConfig.d.ts +1 -0
  104. package/dist/src/tools/checkReleasePublishConfig.js +99 -0
  105. package/dist/src/tools/generateProtoInventory.d.ts +1 -0
  106. package/dist/src/tools/generateProtoInventory.js +89 -0
  107. package/dist/src/tools/normalizeGeneratedCode.d.ts +1 -0
  108. package/dist/src/tools/normalizeGeneratedCode.js +18 -0
  109. package/dist/src/tools/releaseCheck.d.ts +26 -0
  110. package/dist/src/tools/releaseCheck.js +367 -0
  111. package/dist/src/trace.d.ts +39 -0
  112. package/dist/src/trace.js +106 -0
  113. package/dist/src/translation.d.ts +6 -0
  114. package/dist/src/translation.js +22 -0
  115. package/dist/src/upstream.d.ts +20 -0
  116. package/dist/src/upstream.js +270 -0
  117. package/docs/configuration.md +55 -0
  118. package/docs/cursor-app.md +263 -0
  119. package/docs/implementation-inventory.json +609 -0
  120. package/docs/learnings.md +363 -0
  121. package/docs/model-fusion-protocol-origin.json +126 -0
  122. package/docs/model-fusion-protocol.md +110 -0
  123. package/docs/plugin-authoring.md +24 -0
  124. package/docs/proto-inventory.md +1477 -0
  125. package/docs/protocol-surface-audit.md +92 -0
  126. package/docs/protocol.md +52 -0
  127. package/docs/refreshing-protos.md +78 -0
  128. package/docs/release-gates.md +110 -0
  129. package/docs/release-summary.json +86 -0
  130. package/docs/route-contract-manifest.json +288 -0
  131. package/docs/route-policy.json +133 -0
  132. package/docs/service-manifest.json +9490 -0
  133. package/docs/test-manifest.json +155 -0
  134. package/docs/testing-harness.md +204 -0
  135. package/docs/troubleshooting.md +36 -0
  136. package/docs/type-manifest-summary.json +28927 -0
  137. package/package.json +93 -0
  138. package/proto/agent/v1/agent.proto +5371 -0
  139. package/proto/aiserver/v1/aiserver.proto +32944 -0
  140. package/proto/anyrun/v1/anyrun.proto +294 -0
  141. package/proto/google/protobuf/google.proto +37 -0
  142. package/proto/internapi/v1/internapi.proto +32 -0
@@ -0,0 +1,30 @@
1
+ export class ModelRegistry {
2
+ models = new Map();
3
+ register(model) {
4
+ if (this.models.has(model.id)) {
5
+ throw new Error(`Model already registered: ${model.id}`);
6
+ }
7
+ this.models.set(model.id, model);
8
+ }
9
+ get(id) {
10
+ if (id === undefined) {
11
+ return undefined;
12
+ }
13
+ return this.models.get(id);
14
+ }
15
+ list() {
16
+ return Array.from(this.models.values());
17
+ }
18
+ }
19
+ export function registerConfiguredModels(registry, configs, providerFactory) {
20
+ for (const config of configs) {
21
+ registry.register({
22
+ id: config.id,
23
+ displayName: config.displayName,
24
+ baseUrl: config.baseUrl,
25
+ apiKey: config.apiKey,
26
+ contextTokenLimit: config.contextTokenLimit,
27
+ provider: providerFactory(config),
28
+ });
29
+ }
30
+ }
@@ -0,0 +1,13 @@
1
+ import protobuf from "protobufjs";
2
+ export interface CursorProto {
3
+ root: protobuf.Root;
4
+ AvailableModelsRequest: protobuf.Type;
5
+ AvailableModelsResponse: protobuf.Type;
6
+ StreamUnifiedChatRequestWithTools: protobuf.Type;
7
+ StreamUnifiedChatResponseWithTools: protobuf.Type;
8
+ }
9
+ export declare function loadCursorProto(): Promise<CursorProto>;
10
+ export declare function resolveProtoDirectory(): string;
11
+ export declare function listProtoFiles(directory: string): string[];
12
+ export declare function encodeMessage(type: protobuf.Type, value: Record<string, unknown>): Buffer;
13
+ export declare function decodeMessage(type: protobuf.Type, payload: Uint8Array): Record<string, unknown>;
@@ -0,0 +1,61 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import protobuf from "protobufjs";
5
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
6
+ export async function loadCursorProto() {
7
+ const root = await protobuf.load(listProtoFiles(resolveProtoDirectory()));
8
+ return {
9
+ root,
10
+ AvailableModelsRequest: root.lookupType("aiserver.v1.AvailableModelsRequest"),
11
+ AvailableModelsResponse: root.lookupType("aiserver.v1.AvailableModelsResponse"),
12
+ StreamUnifiedChatRequestWithTools: root.lookupType("aiserver.v1.StreamUnifiedChatRequestWithTools"),
13
+ StreamUnifiedChatResponseWithTools: root.lookupType("aiserver.v1.StreamUnifiedChatResponseWithTools"),
14
+ };
15
+ }
16
+ export function resolveProtoDirectory() {
17
+ for (const candidate of candidateRoots()) {
18
+ const protoDir = path.join(candidate, "proto");
19
+ if (fs.existsSync(path.join(protoDir, "aiserver", "v1", "aiserver.proto"))) {
20
+ return protoDir;
21
+ }
22
+ }
23
+ throw new Error("Unable to locate proto/aiserver/v1/aiserver.proto");
24
+ }
25
+ export function listProtoFiles(directory) {
26
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
27
+ const files = [];
28
+ for (const entry of entries) {
29
+ const fullPath = path.join(directory, entry.name);
30
+ if (entry.isDirectory()) {
31
+ files.push(...listProtoFiles(fullPath));
32
+ }
33
+ else if (entry.isFile() && entry.name.endsWith(".proto")) {
34
+ files.push(fullPath);
35
+ }
36
+ }
37
+ return files.sort();
38
+ }
39
+ export function encodeMessage(type, value) {
40
+ const error = type.verify(value);
41
+ if (error) {
42
+ throw new Error(`${type.fullName} verification failed: ${error}`);
43
+ }
44
+ return Buffer.from(type.encode(type.create(value)).finish());
45
+ }
46
+ export function decodeMessage(type, payload) {
47
+ return type.toObject(type.decode(payload), {
48
+ defaults: false,
49
+ longs: String,
50
+ enums: Number,
51
+ bytes: Buffer,
52
+ });
53
+ }
54
+ function candidateRoots() {
55
+ return [
56
+ process.cwd(),
57
+ path.resolve(currentDir, ".."),
58
+ path.resolve(currentDir, "..", ".."),
59
+ path.resolve(currentDir, "..", "..", ".."),
60
+ ];
61
+ }
@@ -0,0 +1,64 @@
1
+ import type { LocalModelConfig } from "../config.js";
2
+ import type { Logger } from "../logger.js";
3
+ import type { ModelProvider } from "../models/registry.js";
4
+ export interface ChatMessage {
5
+ role: "system" | "user" | "assistant" | "tool";
6
+ content: string;
7
+ tool_call_id?: string;
8
+ tool_calls?: OpenAIToolCall[];
9
+ }
10
+ export interface OpenAIToolDefinition {
11
+ type: "function";
12
+ function: {
13
+ name: string;
14
+ description: string;
15
+ parameters: Record<string, unknown>;
16
+ };
17
+ }
18
+ export interface OpenAIToolCall {
19
+ id: string;
20
+ type: "function";
21
+ function: {
22
+ name: string;
23
+ arguments: string;
24
+ };
25
+ }
26
+ export type OpenAICompletionEvent = {
27
+ type: "text";
28
+ text: string;
29
+ } | {
30
+ type: "tool_calls";
31
+ toolCalls: OpenAIToolCall[];
32
+ };
33
+ export type OpenAIBackendErrorCode = "http_error" | "malformed_sse" | "request_aborted" | "request_timeout";
34
+ export interface OpenAIStreamOptions {
35
+ signal?: AbortSignal;
36
+ timeoutMs?: number;
37
+ /**
38
+ * Observability correlation id forwarded to the model backend (gateway) as
39
+ * `x-fusion-trace-id`. The gateway honors it so the Cursor edge shares one
40
+ * observable session with the downstream fusion run.
41
+ */
42
+ traceId?: string;
43
+ }
44
+ export declare class OpenAIBackendError extends Error {
45
+ readonly code: OpenAIBackendErrorCode;
46
+ constructor(code: OpenAIBackendErrorCode, message: string, options?: ErrorOptions);
47
+ }
48
+ export declare class OpenAIStreamParseError extends OpenAIBackendError {
49
+ constructor(message: string, options?: ErrorOptions);
50
+ }
51
+ export declare class OpenAICompatibleProvider implements ModelProvider {
52
+ private readonly config;
53
+ private readonly logger?;
54
+ private readonly options;
55
+ readonly name = "openai-compatible";
56
+ constructor(config: LocalModelConfig, logger?: Logger | undefined, options?: {
57
+ logFullPayload: boolean;
58
+ });
59
+ streamCompletion(messages: ChatMessage[], options?: OpenAIStreamOptions): AsyncGenerator<string>;
60
+ streamCompletionEvents(messages: ChatMessage[], tools?: OpenAIToolDefinition[], options?: OpenAIStreamOptions): AsyncGenerator<OpenAICompletionEvent>;
61
+ }
62
+ export declare function parseOpenAIStream(stream: AsyncIterable<Uint8Array>): AsyncGenerator<string | Extract<OpenAICompletionEvent, {
63
+ type: "tool_calls";
64
+ }>>;
@@ -0,0 +1,355 @@
1
+ import { emitTrace, newSpanId, TRACE_ID_HEADER, TRACE_SPAN_HEADER, } from "../trace.js";
2
+ export class OpenAIBackendError extends Error {
3
+ code;
4
+ constructor(code, message, options) {
5
+ super(message, options);
6
+ this.code = code;
7
+ this.name = "OpenAIBackendError";
8
+ }
9
+ }
10
+ export class OpenAIStreamParseError extends OpenAIBackendError {
11
+ constructor(message, options) {
12
+ super("malformed_sse", message, options);
13
+ this.name = "OpenAIStreamParseError";
14
+ }
15
+ }
16
+ const DEFAULT_BACKEND_REQUEST_TIMEOUT_MS = 120_000;
17
+ export class OpenAICompatibleProvider {
18
+ config;
19
+ logger;
20
+ options;
21
+ name = "openai-compatible";
22
+ constructor(config, logger, options = {
23
+ logFullPayload: false,
24
+ }) {
25
+ this.config = config;
26
+ this.logger = logger;
27
+ this.options = options;
28
+ }
29
+ async *streamCompletion(messages, options = {}) {
30
+ for await (const event of this.streamCompletionEvents(messages, [], options)) {
31
+ if (event.type === "text") {
32
+ yield event.text;
33
+ }
34
+ }
35
+ }
36
+ async *streamCompletionEvents(messages, tools = [], options = {}) {
37
+ if (this.config.hardcodedResponse !== undefined) {
38
+ this.logger?.info("model backend hardcoded response", {
39
+ modelId: this.config.id,
40
+ providerModel: this.config.providerModel,
41
+ responseChars: this.config.hardcodedResponse.length,
42
+ });
43
+ yield { type: "text", text: this.config.hardcodedResponse };
44
+ return;
45
+ }
46
+ const url = `${this.config.baseUrl.replace(/\/$/, "")}/chat/completions`;
47
+ const requestBody = {
48
+ model: this.config.providerModel,
49
+ messages,
50
+ stream: true,
51
+ ...(tools.length > 0 ? { tools } : {}),
52
+ };
53
+ const requestTimeoutMs = options.timeoutMs ??
54
+ this.config.requestTimeoutMs ??
55
+ DEFAULT_BACKEND_REQUEST_TIMEOUT_MS;
56
+ const traceId = options.traceId ?? process.env.FUSION_TRACE_ID ?? undefined;
57
+ const traceSpan = newSpanId();
58
+ const traceHeaders = traceId !== undefined
59
+ ? { [TRACE_ID_HEADER]: traceId, [TRACE_SPAN_HEADER]: traceSpan }
60
+ : {};
61
+ emitTrace({
62
+ event_type: "cursor.route",
63
+ traceId,
64
+ spanId: traceSpan,
65
+ modelId: this.config.id,
66
+ payload: {
67
+ message: "model backend request",
68
+ provider_model: this.config.providerModel,
69
+ url,
70
+ tool_count: tools.length,
71
+ message_count: messages.length,
72
+ },
73
+ });
74
+ this.logger?.info("model backend request", {
75
+ modelId: this.config.id,
76
+ providerModel: this.config.providerModel,
77
+ url,
78
+ stream: true,
79
+ requestTimeoutMs,
80
+ toolCount: tools.length,
81
+ toolNames: tools.map((tool) => tool.function.name),
82
+ ...summarizeMessages(messages),
83
+ ...(this.options.logFullPayload
84
+ ? {
85
+ payload: requestBody,
86
+ }
87
+ : {}),
88
+ });
89
+ const started = Date.now();
90
+ const abortContext = createRequestAbortContext(requestTimeoutMs, options.signal);
91
+ let response;
92
+ try {
93
+ response = await fetch(url, {
94
+ method: "POST",
95
+ headers: {
96
+ connection: "close",
97
+ "content-type": "application/json",
98
+ ...(this.config.apiKey
99
+ ? { authorization: `Bearer ${this.config.apiKey}` }
100
+ : {}),
101
+ ...traceHeaders,
102
+ },
103
+ body: JSON.stringify(requestBody),
104
+ signal: abortContext.signal,
105
+ });
106
+ }
107
+ catch (error) {
108
+ const classified = classifyBackendError(error, abortContext);
109
+ this.logger?.warn("model backend request failed", {
110
+ modelId: this.config.id,
111
+ providerModel: this.config.providerModel,
112
+ url,
113
+ durationMs: Date.now() - started,
114
+ error: classified instanceof Error ? classified.message : String(error),
115
+ code: classified instanceof OpenAIBackendError
116
+ ? classified.code
117
+ : undefined,
118
+ cause: error instanceof Error && error.cause instanceof Error
119
+ ? error.cause.message
120
+ : undefined,
121
+ });
122
+ abortContext.dispose();
123
+ throw classified;
124
+ }
125
+ if (!response.ok || response.body === null) {
126
+ const body = await response.text().catch(() => "");
127
+ const error = new OpenAIBackendError("http_error", `Model backend returned ${response.status}: ${body}`);
128
+ this.logger?.warn("model backend response failed", {
129
+ modelId: this.config.id,
130
+ providerModel: this.config.providerModel,
131
+ url,
132
+ status: response.status,
133
+ durationMs: Date.now() - started,
134
+ bodyPreview: body.slice(0, 500),
135
+ code: error.code,
136
+ });
137
+ abortContext.dispose();
138
+ throw error;
139
+ }
140
+ let chunkCount = 0;
141
+ let responseChars = 0;
142
+ try {
143
+ for await (const text of parseOpenAIStream(response.body)) {
144
+ if (typeof text === "string") {
145
+ chunkCount += 1;
146
+ responseChars += text.length;
147
+ yield { type: "text", text };
148
+ }
149
+ else if (text.type === "tool_calls") {
150
+ chunkCount += 1;
151
+ yield text;
152
+ }
153
+ }
154
+ this.logger?.info("model backend response complete", {
155
+ modelId: this.config.id,
156
+ providerModel: this.config.providerModel,
157
+ url,
158
+ status: response.status,
159
+ durationMs: Date.now() - started,
160
+ chunkCount,
161
+ responseChars,
162
+ });
163
+ emitTrace({
164
+ event_type: "cursor.route",
165
+ traceId,
166
+ spanId: traceSpan,
167
+ modelId: this.config.id,
168
+ payload: {
169
+ message: "model backend response complete",
170
+ status: response.status,
171
+ duration_ms: Date.now() - started,
172
+ chunk_count: chunkCount,
173
+ response_chars: responseChars,
174
+ },
175
+ });
176
+ }
177
+ catch (error) {
178
+ const classified = classifyBackendError(error, abortContext);
179
+ this.logger?.warn("model backend stream failed", {
180
+ modelId: this.config.id,
181
+ providerModel: this.config.providerModel,
182
+ url,
183
+ status: response.status,
184
+ durationMs: Date.now() - started,
185
+ chunkCount,
186
+ responseChars,
187
+ error: classified instanceof Error ? classified.message : String(error),
188
+ code: classified instanceof OpenAIBackendError
189
+ ? classified.code
190
+ : undefined,
191
+ });
192
+ throw classified;
193
+ }
194
+ finally {
195
+ abortContext.dispose();
196
+ }
197
+ }
198
+ }
199
+ function summarizeMessages(messages) {
200
+ return {
201
+ messageCount: messages.length,
202
+ messageRoles: messages.map((message) => message.role),
203
+ messageChars: messages.reduce((total, message) => total + message.content.length, 0),
204
+ firstMessagePreviewChars: messagePreviewChars(messages[0]?.content),
205
+ lastMessagePreviewChars: messagePreviewChars(messages.at(-1)?.content),
206
+ };
207
+ }
208
+ function messagePreviewChars(value) {
209
+ return value === undefined
210
+ ? undefined
211
+ : value.replace(/\s+/g, " ").trim().slice(0, 500).length;
212
+ }
213
+ function preview(value) {
214
+ if (value === undefined) {
215
+ return undefined;
216
+ }
217
+ return value.replace(/\s+/g, " ").trim().slice(0, 500);
218
+ }
219
+ export async function* parseOpenAIStream(stream) {
220
+ const decoder = new TextDecoder("utf-8", { fatal: true });
221
+ let buffer = "";
222
+ const state = {
223
+ done: false,
224
+ toolCalls: new Map(),
225
+ };
226
+ for await (const chunk of stream) {
227
+ buffer += decodeStreamText(decoder, chunk, true);
228
+ const lines = buffer.split(/\r?\n/);
229
+ buffer = lines.pop() ?? "";
230
+ yield* parseSseLines(lines, state);
231
+ if (state.done) {
232
+ return;
233
+ }
234
+ }
235
+ buffer += decodeStreamText(decoder, undefined, false);
236
+ if (buffer.length > 0 && !state.done) {
237
+ yield* parseSseLines(buffer.split(/\r?\n/), state);
238
+ }
239
+ }
240
+ function* parseSseLines(lines, state) {
241
+ for (const line of lines) {
242
+ const trimmed = line.trim();
243
+ if (trimmed.length === 0 || trimmed.startsWith(":")) {
244
+ continue;
245
+ }
246
+ if (!trimmed.startsWith("data:")) {
247
+ continue;
248
+ }
249
+ const data = trimmed.slice("data:".length).trim();
250
+ if (data.length === 0) {
251
+ continue;
252
+ }
253
+ if (data === "[DONE]") {
254
+ state.done = true;
255
+ return;
256
+ }
257
+ let parsed;
258
+ try {
259
+ parsed = JSON.parse(data);
260
+ }
261
+ catch (error) {
262
+ throw new OpenAIStreamParseError(`OpenAI stream contained malformed JSON data: ${preview(data)}`, error instanceof Error ? { cause: error } : undefined);
263
+ }
264
+ const content = parsed.choices?.[0]?.delta?.content;
265
+ if (typeof content === "string" && content.length > 0) {
266
+ yield content;
267
+ }
268
+ const deltas = parsed.choices?.[0]?.delta?.tool_calls;
269
+ if (Array.isArray(deltas)) {
270
+ mergeToolCallDeltas(state.toolCalls, deltas);
271
+ }
272
+ if (parsed.choices?.[0]?.finish_reason === "tool_calls") {
273
+ yield {
274
+ type: "tool_calls",
275
+ toolCalls: Array.from(state.toolCalls.values()),
276
+ };
277
+ state.toolCalls.clear();
278
+ }
279
+ }
280
+ }
281
+ function mergeToolCallDeltas(toolCalls, deltas) {
282
+ for (const delta of deltas) {
283
+ const index = delta.index ?? toolCalls.size;
284
+ const current = toolCalls.get(index) ??
285
+ {
286
+ id: delta.id ?? `tool-${String(index)}`,
287
+ type: "function",
288
+ function: { name: "", arguments: "" },
289
+ };
290
+ if (delta.id !== undefined) {
291
+ current.id = delta.id;
292
+ }
293
+ if (delta.type === "function") {
294
+ current.type = "function";
295
+ }
296
+ if (delta.function?.name !== undefined) {
297
+ current.function.name += delta.function.name;
298
+ }
299
+ if (delta.function?.arguments !== undefined) {
300
+ current.function.arguments += delta.function.arguments;
301
+ }
302
+ toolCalls.set(index, current);
303
+ }
304
+ }
305
+ function decodeStreamText(decoder, chunk, stream) {
306
+ try {
307
+ return chunk === undefined
308
+ ? decoder.decode()
309
+ : decoder.decode(chunk, { stream });
310
+ }
311
+ catch (error) {
312
+ throw new OpenAIStreamParseError("OpenAI stream contained malformed UTF-8", error instanceof Error ? { cause: error } : undefined);
313
+ }
314
+ }
315
+ function createRequestAbortContext(timeoutMs, upstreamSignal) {
316
+ const controller = new AbortController();
317
+ let didTimeout = false;
318
+ const timer = setTimeout(() => {
319
+ didTimeout = true;
320
+ controller.abort();
321
+ }, timeoutMs);
322
+ const abortFromUpstream = () => controller.abort();
323
+ if (upstreamSignal !== undefined) {
324
+ if (upstreamSignal.aborted) {
325
+ controller.abort();
326
+ }
327
+ else {
328
+ upstreamSignal.addEventListener("abort", abortFromUpstream, {
329
+ once: true,
330
+ });
331
+ }
332
+ }
333
+ return {
334
+ signal: controller.signal,
335
+ timedOut: () => didTimeout,
336
+ dispose: () => {
337
+ clearTimeout(timer);
338
+ upstreamSignal?.removeEventListener("abort", abortFromUpstream);
339
+ },
340
+ };
341
+ }
342
+ function classifyBackendError(error, abortContext) {
343
+ if (error instanceof OpenAIBackendError) {
344
+ return error;
345
+ }
346
+ if (abortContext.signal.aborted) {
347
+ const code = abortContext.timedOut()
348
+ ? "request_timeout"
349
+ : "request_aborted";
350
+ return new OpenAIBackendError(code, code === "request_timeout"
351
+ ? "Model backend request timed out"
352
+ : "Model backend request was aborted", error instanceof Error ? { cause: error } : undefined);
353
+ }
354
+ return error;
355
+ }
@@ -0,0 +1,4 @@
1
+ export declare function redactValue(value: string): string;
2
+ export declare function redactHeaders(headers: Headers | NodeJS.Dict<string | string[] | undefined>): Record<string, string>;
3
+ export declare function redactHeaderValue(name: string, value: string): string;
4
+ export declare function redactForLogging(value: unknown): unknown;
@@ -0,0 +1,65 @@
1
+ const SENSITIVE_HEADER_NAMES = new Set([
2
+ "authorization",
3
+ "cookie",
4
+ "set-cookie",
5
+ "x-cursor-token",
6
+ "x-api-key",
7
+ "api-key",
8
+ ]);
9
+ const SENSITIVE_OBJECT_KEY_PATTERN = /authorization|cookie|set-cookie|x-cursor-token|x-api-key|api[-_]?key|(?:^|[-_])token$|access[-_]?token|secret|password/i;
10
+ const SECRET_VALUE_PATTERN = /(bearer\s+)[a-z0-9._~+/-]+|([?&](?:token|key|api_key|access_token)=)[^&\s]+/gi;
11
+ export function redactValue(value) {
12
+ return value.replace(SECRET_VALUE_PATTERN, (_match, bearerPrefix, queryPrefix) => {
13
+ if (bearerPrefix !== undefined) {
14
+ return `${bearerPrefix}[REDACTED]`;
15
+ }
16
+ if (queryPrefix !== undefined) {
17
+ return `${queryPrefix}[REDACTED]`;
18
+ }
19
+ return "[REDACTED]";
20
+ });
21
+ }
22
+ export function redactHeaders(headers) {
23
+ const result = {};
24
+ if (headers instanceof Headers) {
25
+ headers.forEach((value, key) => {
26
+ result[key] = redactHeaderValue(key, value);
27
+ });
28
+ return result;
29
+ }
30
+ for (const [key, value] of Object.entries(headers)) {
31
+ if (value === undefined) {
32
+ continue;
33
+ }
34
+ result[key] = redactHeaderValue(key, Array.isArray(value) ? value.join(", ") : value);
35
+ }
36
+ return result;
37
+ }
38
+ export function redactHeaderValue(name, value) {
39
+ if (SENSITIVE_HEADER_NAMES.has(name.toLowerCase())) {
40
+ return "[REDACTED]";
41
+ }
42
+ return redactValue(value);
43
+ }
44
+ export function redactForLogging(value) {
45
+ return redactObjectValue(value, undefined);
46
+ }
47
+ function redactObjectValue(value, key) {
48
+ if (key !== undefined && SENSITIVE_OBJECT_KEY_PATTERN.test(key)) {
49
+ return "[REDACTED]";
50
+ }
51
+ if (typeof value === "string") {
52
+ return redactValue(value);
53
+ }
54
+ if (Array.isArray(value)) {
55
+ return value.map((item) => redactObjectValue(item, undefined));
56
+ }
57
+ if (typeof value !== "object" || value === null) {
58
+ return value;
59
+ }
60
+ const result = {};
61
+ for (const [entryKey, entryValue] of Object.entries(value)) {
62
+ result[entryKey] = redactObjectValue(entryValue, entryKey);
63
+ }
64
+ return result;
65
+ }
@@ -0,0 +1,16 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { BridgeConfig } from "./config.js";
3
+ import type { Logger } from "./logger.js";
4
+ import type { RouteDecision } from "./routes.js";
5
+ export type RouteOutcome = "health" | "plugin" | "intercept" | "pass-through";
6
+ export interface RouteInventoryEntry {
7
+ method: string;
8
+ path: string;
9
+ contentType: string;
10
+ status: number;
11
+ framing: string;
12
+ policy: string;
13
+ outcome: RouteOutcome;
14
+ }
15
+ export declare function attachRouteInventoryLogger(config: BridgeConfig, logger: Logger, request: IncomingMessage, response: ServerResponse, decision: RouteDecision, outcome: () => RouteOutcome): void;
16
+ export declare function framingForContentType(value: string | string[] | undefined): string;
@@ -0,0 +1,39 @@
1
+ import { redactValue } from "./redaction.js";
2
+ export function attachRouteInventoryLogger(config, logger, request, response, decision, outcome) {
3
+ if (!config.routeInventoryEnabled) {
4
+ return;
5
+ }
6
+ response.once("finish", () => {
7
+ logger.info("desktop route inventory", {
8
+ method: request.method ?? "GET",
9
+ path: redactValue(decision.path),
10
+ contentType: headerValue(request.headers["content-type"]),
11
+ status: response.statusCode,
12
+ framing: framingForContentType(request.headers["content-type"]),
13
+ policy: decision.policy,
14
+ outcome: outcome(),
15
+ });
16
+ });
17
+ }
18
+ export function framingForContentType(value) {
19
+ const contentType = headerValue(value).toLowerCase();
20
+ if (contentType.includes("application/connect+proto")) {
21
+ return "connect-proto";
22
+ }
23
+ if (contentType.includes("application/proto")) {
24
+ return "proto";
25
+ }
26
+ if (contentType.includes("application/json")) {
27
+ return "json";
28
+ }
29
+ if (contentType.length === 0) {
30
+ return "none";
31
+ }
32
+ return "other";
33
+ }
34
+ function headerValue(value) {
35
+ if (value === undefined) {
36
+ return "";
37
+ }
38
+ return Array.isArray(value) ? value.join(", ") : value;
39
+ }
@@ -0,0 +1,37 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ export declare const AVAILABLE_MODELS_PATH = "/aiserver.v1.AiService/AvailableModels";
3
+ export declare const GET_USABLE_MODELS_PATH = "/aiserver.v1.AiService/GetUsableModels";
4
+ export declare const GET_DEFAULT_MODEL_FOR_CLI_PATH = "/aiserver.v1.AiService/GetDefaultModelForCli";
5
+ export declare const GET_DEFAULT_MODEL_PATH = "/aiserver.v1.AiService/GetDefaultModel";
6
+ export declare const NAME_AGENT_PATH = "/aiserver.v1.AiService/NameAgent";
7
+ export declare const GET_SERVER_CONFIG_PATH = "/aiserver.v1.ServerConfigService/GetServerConfig";
8
+ export declare const AGENT_RUN_PATH = "/agent.v1.AgentService/Run";
9
+ export declare const AGENT_RUN_SSE_PATH = "/agent.v1.AgentService/RunSSE";
10
+ export declare const BIDI_APPEND_PATH = "/aiserver.v1.BidiService/BidiAppend";
11
+ export declare const STREAM_CHAT_WITH_TOOLS_PATH = "/aiserver.v1.ChatService/StreamUnifiedChatWithTools";
12
+ export declare const UPLOAD_ISSUE_TRACE_PATH = "/aiserver.v1.AnalyticsService/UploadIssueTrace";
13
+ export declare const AUTH_FULL_STRIPE_PROFILE_PATH = "/auth/full_stripe_profile";
14
+ export declare const AUTH_STRIPE_PROFILE_PATH = "/auth/stripe_profile";
15
+ export type RoutePolicy = "intercept" | "observe-only" | "pass-through";
16
+ export type RouteSupportLevel = "implemented" | "conditional" | "observe-first" | "unsupported";
17
+ export interface RouteContractSeed {
18
+ path: string;
19
+ packageName?: string;
20
+ service?: string;
21
+ method?: string;
22
+ policy: RoutePolicy;
23
+ supportLevel: RouteSupportLevel;
24
+ owner: "bridge-core" | "desktop-observation" | "upstream";
25
+ expectedMethods: string[];
26
+ expectedContentTypes: string[];
27
+ reason: string;
28
+ uncertainty: string;
29
+ }
30
+ export interface RouteDecision {
31
+ path: string;
32
+ policy: RoutePolicy;
33
+ reason: string;
34
+ }
35
+ export declare const ROUTE_CONTRACT_SEEDS: readonly RouteContractSeed[];
36
+ export declare const INTERCEPTABLE_ROUTE_PATHS: string[];
37
+ export declare function classifyRoute(request: IncomingMessage): RouteDecision;