@velum-labs/cursorkit 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/ckLauncher.d.ts +2 -5
- package/dist/src/ckLauncher.js +25 -206
- package/dist/src/cursorDesktopState.d.ts +10 -0
- package/dist/src/cursorDesktopState.js +204 -0
- package/dist/src/desktopConnectProxy.d.ts +10 -0
- package/dist/src/desktopConnectProxy.js +7 -1
- package/dist/src/fixtures/modelFusion.d.ts +1 -1
- package/dist/src/fixtures/modelFusion.js +1 -1
- package/dist/src/server.js +35 -2
- package/docs/model-fusion-protocol-origin.json +1 -1
- package/package.json +6 -4
package/dist/src/ckLauncher.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type ChildProcess } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
-
import {
|
|
4
|
+
import { buildLocalDesktopModelEntry, mergeLocalAgentBackendUrlsIntoApplicationUser, mergeLocalDesktopModelsIntoApplicationUser } from "./cursorDesktopState.js";
|
|
5
|
+
export { buildLocalDesktopModelEntry, mergeLocalAgentBackendUrlsIntoApplicationUser, mergeLocalDesktopModelsIntoApplicationUser, };
|
|
5
6
|
import { type DesktopConnectProxy } from "./desktopConnectProxy.js";
|
|
6
7
|
export type CkCommand = "launch" | "test" | "doctor" | "cert" | "route" | "stop";
|
|
7
8
|
export type CkProfileMode = "isolated" | "default";
|
|
@@ -155,9 +156,5 @@ export declare function registerCk(program: Command): void;
|
|
|
155
156
|
export declare function chooseBridgePort(): Promise<number>;
|
|
156
157
|
export declare function seedCursorAuthFromDefault(plan: Pick<CkLaunchPlan, "profileMode" | "userDataDir" | "seedAuthFromDefault">): CursorAuthSeedStatus;
|
|
157
158
|
export declare function seedLocalModelsIntoCursorState(plan: Pick<CkLaunchPlan, "agentHttpPort" | "bridge" | "profileMode" | "userDataDir">): CursorLocalModelSeedStatus;
|
|
158
|
-
export declare function mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser: Record<string, unknown>, agentOrigin: string): void;
|
|
159
|
-
export declare function mergeLocalDesktopModelsIntoApplicationUser(applicationUser: Record<string, unknown>, models: LocalModelConfig[]): void;
|
|
160
|
-
export declare function buildLocalDesktopModelEntry(model: LocalModelConfig): Record<string, unknown>;
|
|
161
159
|
export declare function cleanupIsolatedCursorProcesses(userDataDir: string): number[];
|
|
162
160
|
export declare function bridgeProcessMatchesState(command: string, state?: Pick<CkState, "bridgeCommand">): boolean;
|
|
163
|
-
export {};
|
package/dist/src/ckLauncher.js
CHANGED
|
@@ -6,6 +6,10 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { Command, InvalidArgumentError } from "commander";
|
|
8
8
|
import { loadConfig } from "./config.js";
|
|
9
|
+
import { buildLocalDesktopModelEntry, mergeLocalAgentBackendUrlsIntoApplicationUser, mergeLocalDesktopModelsIntoApplicationUser, } from "./cursorDesktopState.js";
|
|
10
|
+
// Re-exported for backwards compatibility (callers/tests import these from
|
|
11
|
+
// ckLauncher); the implementations now live in cursorDesktopState.
|
|
12
|
+
export { buildLocalDesktopModelEntry, mergeLocalAgentBackendUrlsIntoApplicationUser, mergeLocalDesktopModelsIntoApplicationUser, };
|
|
9
13
|
import { startDesktopConnectProxy, } from "./desktopConnectProxy.js";
|
|
10
14
|
import { DESKTOP_CERT_PATH, DESKTOP_HOSTNAME, DESKTOP_HOSTNAMES, DESKTOP_KEY_PATH, desktopCertificateStatus, desktopDnsStatus, desktopEnv, desktopTrustCommand, localModelBackendStatus, upstreamReachabilityStatus, writeDesktopCertificate, } from "./desktop.js";
|
|
11
15
|
import { AGENT_RUN_PATH, AGENT_RUN_SSE_PATH, AVAILABLE_MODELS_PATH, BIDI_APPEND_PATH, GET_DEFAULT_MODEL_FOR_CLI_PATH, GET_USABLE_MODELS_PATH, STREAM_CHAT_WITH_TOOLS_PATH, } from "./routes.js";
|
|
@@ -767,101 +771,24 @@ export function seedLocalModelsIntoCursorState(plan) {
|
|
|
767
771
|
if (typeof agentPublicOrigin === "string") {
|
|
768
772
|
mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser, agentPublicOrigin);
|
|
769
773
|
}
|
|
774
|
+
// The seed file mirrors the user's Cursor credentials (cursorCreds), so it is
|
|
775
|
+
// written with owner-only permissions and removed once sqlite3 has imported
|
|
776
|
+
// it rather than being left on disk.
|
|
770
777
|
const seedPath = path.join(globalStorageDir, "applicationUser.seed.json");
|
|
771
|
-
fs.writeFileSync(seedPath, JSON.stringify(applicationUser));
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
return "seeded";
|
|
781
|
-
}
|
|
782
|
-
export function mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser, agentOrigin) {
|
|
783
|
-
const cursorCreds = ensureRecord(applicationUser, "cursorCreds");
|
|
784
|
-
const urls = { default: agentOrigin };
|
|
785
|
-
cursorCreds.agentBackendUrlPrivacy = urls;
|
|
786
|
-
cursorCreds.agentBackendUrlNonPrivacy = urls;
|
|
787
|
-
}
|
|
788
|
-
export function mergeLocalDesktopModelsIntoApplicationUser(applicationUser, models) {
|
|
789
|
-
const localModelIds = new Set(models.map((model) => model.id));
|
|
790
|
-
const current = Array.isArray(applicationUser.availableDefaultModels2)
|
|
791
|
-
? applicationUser.availableDefaultModels2.filter((item) => {
|
|
792
|
-
if (!isPlainRecord(item) || typeof item.name !== "string") {
|
|
793
|
-
return true;
|
|
794
|
-
}
|
|
795
|
-
return !localModelIds.has(item.name);
|
|
796
|
-
})
|
|
797
|
-
: [];
|
|
798
|
-
applicationUser.availableDefaultModels2 = current;
|
|
799
|
-
const aiSettings = ensureRecord(applicationUser, "aiSettings");
|
|
800
|
-
for (const model of models) {
|
|
801
|
-
appendUnique(ensureStringArray(aiSettings, "userAddedModels"), model.id);
|
|
802
|
-
appendUnique(ensureStringArray(aiSettings, "modelOverrideEnabled"), model.id);
|
|
803
|
-
removeValue(ensureStringArray(aiSettings, "modelOverrideDisabled"), model.id);
|
|
804
|
-
}
|
|
805
|
-
const preferences = ensureRecord(aiSettings, "modelParameterPreferences");
|
|
806
|
-
const updatedAt = new Date().toISOString();
|
|
807
|
-
for (const model of models) {
|
|
808
|
-
preferences[model.id] = {
|
|
809
|
-
modelId: model.id,
|
|
810
|
-
parameters: localDesktopParameterValues(true),
|
|
811
|
-
updatedAt,
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
const firstModel = models[0];
|
|
815
|
-
if (firstModel !== undefined) {
|
|
816
|
-
applicationUser.useOpenAIKey = true;
|
|
817
|
-
applicationUser.openAIBaseUrl = firstModel.baseUrl;
|
|
818
|
-
applicationUser.openAIKey = firstModel.apiKey;
|
|
819
|
-
const modelConfig = ensureRecord(aiSettings, "modelConfig");
|
|
820
|
-
const selectedModel = {
|
|
821
|
-
modelId: firstModel.id,
|
|
822
|
-
parameters: localDesktopParameterValues(true),
|
|
823
|
-
};
|
|
824
|
-
for (const key of ["composer", "background-composer"]) {
|
|
825
|
-
modelConfig[key] = {
|
|
826
|
-
modelName: firstModel.id,
|
|
827
|
-
maxMode: true,
|
|
828
|
-
selectedModels: [selectedModel],
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
const featureModelConfigs = ensureRecord(applicationUser, "featureModelConfigs");
|
|
833
|
-
for (const value of Object.values(featureModelConfigs)) {
|
|
834
|
-
if (!isPlainRecord(value)) {
|
|
835
|
-
continue;
|
|
836
|
-
}
|
|
837
|
-
const fallbackModels = ensureStringArray(value, "fallbackModels");
|
|
838
|
-
for (const model of models) {
|
|
839
|
-
appendUnique(fallbackModels, model.id);
|
|
778
|
+
fs.writeFileSync(seedPath, JSON.stringify(applicationUser), { mode: 0o600 });
|
|
779
|
+
try {
|
|
780
|
+
const escapedSeedPath = seedPath.replaceAll("'", "''");
|
|
781
|
+
const seed = spawnSync("sqlite3", [
|
|
782
|
+
targetDb,
|
|
783
|
+
`insert or replace into ItemTable(key, value) values('${key}', cast(readfile('${escapedSeedPath}') as text));`,
|
|
784
|
+
], { encoding: "utf8" });
|
|
785
|
+
if (seed.status !== 0) {
|
|
786
|
+
return "sqlite-unavailable";
|
|
840
787
|
}
|
|
788
|
+
return "seeded";
|
|
841
789
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
if (!isPlainRecord(target[key])) {
|
|
845
|
-
target[key] = {};
|
|
846
|
-
}
|
|
847
|
-
return target[key];
|
|
848
|
-
}
|
|
849
|
-
function ensureStringArray(target, key) {
|
|
850
|
-
const values = Array.isArray(target[key])
|
|
851
|
-
? target[key].filter((value) => typeof value === "string")
|
|
852
|
-
: [];
|
|
853
|
-
target[key] = values;
|
|
854
|
-
return values;
|
|
855
|
-
}
|
|
856
|
-
function appendUnique(values, value) {
|
|
857
|
-
if (!values.includes(value)) {
|
|
858
|
-
values.push(value);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
function removeValue(values, value) {
|
|
862
|
-
const index = values.indexOf(value);
|
|
863
|
-
if (index !== -1) {
|
|
864
|
-
values.splice(index, 1);
|
|
790
|
+
finally {
|
|
791
|
+
fs.rmSync(seedPath, { force: true });
|
|
865
792
|
}
|
|
866
793
|
}
|
|
867
794
|
function defaultCursorStateDbPath() {
|
|
@@ -881,119 +808,6 @@ function readCursorStateValue(dbPath, key) {
|
|
|
881
808
|
}
|
|
882
809
|
return result.stdout;
|
|
883
810
|
}
|
|
884
|
-
export function buildLocalDesktopModelEntry(model) {
|
|
885
|
-
const tooltipData = {
|
|
886
|
-
primaryText: "",
|
|
887
|
-
secondaryText: "",
|
|
888
|
-
secondaryWarningText: false,
|
|
889
|
-
icon: "",
|
|
890
|
-
tertiaryText: "",
|
|
891
|
-
tertiaryTextUrl: "",
|
|
892
|
-
markdownContent: `**${model.displayName}**<br />Local OpenAI-compatible model served by cursorkit.<br /><br />${model.contextTokenLimit.toLocaleString()} token context window`,
|
|
893
|
-
};
|
|
894
|
-
return {
|
|
895
|
-
name: model.id,
|
|
896
|
-
serverModelName: model.id,
|
|
897
|
-
clientDisplayName: model.displayName,
|
|
898
|
-
inputboxShortModelName: model.displayName,
|
|
899
|
-
vendorName: "local",
|
|
900
|
-
vendor: { displayName: "Local" },
|
|
901
|
-
supportsAgent: true,
|
|
902
|
-
supportsCmdK: false,
|
|
903
|
-
supportsImages: false,
|
|
904
|
-
supportsMaxMode: true,
|
|
905
|
-
supportsNonMaxMode: true,
|
|
906
|
-
supportsPlanMode: true,
|
|
907
|
-
supportsSandboxing: false,
|
|
908
|
-
supportsThinking: false,
|
|
909
|
-
cloudAgentEffortModes: [],
|
|
910
|
-
defaultOn: true,
|
|
911
|
-
degradationStatus: 0,
|
|
912
|
-
isRecommendedForBackgroundComposer: false,
|
|
913
|
-
legacySlugs: [model.id],
|
|
914
|
-
idAliases: [model.id, model.displayName],
|
|
915
|
-
parameterDefinitions: localDesktopParameterDefinitions(),
|
|
916
|
-
namedModelSectionIndex: 10_000,
|
|
917
|
-
visibleInRoutedModelView: true,
|
|
918
|
-
tooltipData,
|
|
919
|
-
tooltipDataForMaxMode: tooltipData,
|
|
920
|
-
variants: [
|
|
921
|
-
localDesktopVariantConfig(model, tooltipData, false),
|
|
922
|
-
localDesktopVariantConfig(model, tooltipData, true),
|
|
923
|
-
],
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
function localDesktopVariantConfig(model, tooltipData, isMaxMode) {
|
|
927
|
-
return {
|
|
928
|
-
parameterValues: localDesktopParameterValues(isMaxMode),
|
|
929
|
-
displayName: model.displayName,
|
|
930
|
-
isMaxMode,
|
|
931
|
-
isDefaultMaxConfig: isMaxMode,
|
|
932
|
-
isDefaultNonMaxConfig: !isMaxMode,
|
|
933
|
-
tooltipData,
|
|
934
|
-
displayNameOutsidePicker: model.displayName,
|
|
935
|
-
variantStringRepresentation: localDesktopVariantString(model.id, isMaxMode),
|
|
936
|
-
legacySlug: model.id,
|
|
937
|
-
};
|
|
938
|
-
}
|
|
939
|
-
function localDesktopVariantString(modelId, isMaxMode) {
|
|
940
|
-
const context = isMaxMode ? "1m" : "272k";
|
|
941
|
-
return `${modelId}[context=${context},reasoning=medium,fast=false]`;
|
|
942
|
-
}
|
|
943
|
-
function localDesktopParameterValues(isMaxMode) {
|
|
944
|
-
return [
|
|
945
|
-
{ id: "context", value: isMaxMode ? "1m" : "272k" },
|
|
946
|
-
{ id: "reasoning", value: "medium" },
|
|
947
|
-
{ id: "fast", value: "false" },
|
|
948
|
-
];
|
|
949
|
-
}
|
|
950
|
-
function localDesktopParameterDefinitions() {
|
|
951
|
-
return [
|
|
952
|
-
{
|
|
953
|
-
id: "context",
|
|
954
|
-
name: "Context",
|
|
955
|
-
markdownTooltip: "Context size the model has available.",
|
|
956
|
-
parameterType: {
|
|
957
|
-
enumParameter: {
|
|
958
|
-
values: [
|
|
959
|
-
{ value: "272k", displayName: "272K" },
|
|
960
|
-
{ value: "1m", displayName: "1M" },
|
|
961
|
-
],
|
|
962
|
-
},
|
|
963
|
-
},
|
|
964
|
-
},
|
|
965
|
-
{
|
|
966
|
-
id: "reasoning",
|
|
967
|
-
name: "Reasoning",
|
|
968
|
-
markdownTooltip: "Reasoning effort the model uses to generate its response.",
|
|
969
|
-
parameterType: {
|
|
970
|
-
enumParameter: {
|
|
971
|
-
values: [
|
|
972
|
-
{ value: "none", displayName: "None" },
|
|
973
|
-
{ value: "low", displayName: "Low" },
|
|
974
|
-
{ value: "medium", displayName: "Medium" },
|
|
975
|
-
{ value: "high", displayName: "High" },
|
|
976
|
-
{ value: "extra-high", displayName: "Extra High" },
|
|
977
|
-
],
|
|
978
|
-
},
|
|
979
|
-
},
|
|
980
|
-
isCycleableByHotkey: true,
|
|
981
|
-
},
|
|
982
|
-
{
|
|
983
|
-
id: "fast",
|
|
984
|
-
name: "Fast",
|
|
985
|
-
markdownTooltip: "Use the provider's fast lane when supported.",
|
|
986
|
-
parameterType: {
|
|
987
|
-
booleanParameter: {
|
|
988
|
-
values: [{ value: "false" }, { value: "true", displayName: "Fast" }],
|
|
989
|
-
},
|
|
990
|
-
},
|
|
991
|
-
},
|
|
992
|
-
];
|
|
993
|
-
}
|
|
994
|
-
function isPlainRecord(value) {
|
|
995
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
996
|
-
}
|
|
997
811
|
export function cleanupIsolatedCursorProcesses(userDataDir) {
|
|
998
812
|
const killed = terminateIsolatedCursorProcesses(userDataDir, "SIGTERM");
|
|
999
813
|
spawnSync("sleep", ["1"]);
|
|
@@ -1196,9 +1010,14 @@ function configureCursorNodeTlsEnv(plan) {
|
|
|
1196
1010
|
if (plan.profileMode !== "isolated") {
|
|
1197
1011
|
return;
|
|
1198
1012
|
}
|
|
1013
|
+
// Trust only the bridge's self-signed certificate via NODE_EXTRA_CA_CERTS
|
|
1014
|
+
// instead of globally disabling TLS verification. This keeps certificate
|
|
1015
|
+
// validation intact for real upstream traffic (e.g. api2.cursor.sh) while
|
|
1016
|
+
// allowing the spawned Cursor process to connect to the local bridge.
|
|
1017
|
+
const bridgeCertPath = plan.bridge.env?.BRIDGE_CERT_PATH ?? DESKTOP_CERT_PATH;
|
|
1199
1018
|
plan.cursor.env = {
|
|
1200
1019
|
...plan.cursor.env,
|
|
1201
|
-
|
|
1020
|
+
NODE_EXTRA_CA_CERTS: bridgeCertPath,
|
|
1202
1021
|
};
|
|
1203
1022
|
}
|
|
1204
1023
|
function printPlan(plan) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LocalModelConfig } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Builders that shape the Cursor desktop `applicationUser` state and local
|
|
4
|
+
* model catalog entries. Extracted from `ckLauncher` so the (large) launcher
|
|
5
|
+
* module is not also responsible for the desktop state-seed schema. These are
|
|
6
|
+
* pure data transforms over plain JSON-shaped records.
|
|
7
|
+
*/
|
|
8
|
+
export declare function mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser: Record<string, unknown>, agentOrigin: string): void;
|
|
9
|
+
export declare function mergeLocalDesktopModelsIntoApplicationUser(applicationUser: Record<string, unknown>, models: LocalModelConfig[]): void;
|
|
10
|
+
export declare function buildLocalDesktopModelEntry(model: LocalModelConfig): Record<string, unknown>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builders that shape the Cursor desktop `applicationUser` state and local
|
|
3
|
+
* model catalog entries. Extracted from `ckLauncher` so the (large) launcher
|
|
4
|
+
* module is not also responsible for the desktop state-seed schema. These are
|
|
5
|
+
* pure data transforms over plain JSON-shaped records.
|
|
6
|
+
*/
|
|
7
|
+
export function mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser, agentOrigin) {
|
|
8
|
+
const cursorCreds = ensureRecord(applicationUser, "cursorCreds");
|
|
9
|
+
const urls = { default: agentOrigin };
|
|
10
|
+
cursorCreds.agentBackendUrlPrivacy = urls;
|
|
11
|
+
cursorCreds.agentBackendUrlNonPrivacy = urls;
|
|
12
|
+
}
|
|
13
|
+
export function mergeLocalDesktopModelsIntoApplicationUser(applicationUser, models) {
|
|
14
|
+
const localModelIds = new Set(models.map((model) => model.id));
|
|
15
|
+
const current = Array.isArray(applicationUser.availableDefaultModels2)
|
|
16
|
+
? applicationUser.availableDefaultModels2.filter((item) => {
|
|
17
|
+
if (!isPlainRecord(item) || typeof item.name !== "string") {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return !localModelIds.has(item.name);
|
|
21
|
+
})
|
|
22
|
+
: [];
|
|
23
|
+
applicationUser.availableDefaultModels2 = current;
|
|
24
|
+
const aiSettings = ensureRecord(applicationUser, "aiSettings");
|
|
25
|
+
for (const model of models) {
|
|
26
|
+
appendUnique(ensureStringArray(aiSettings, "userAddedModels"), model.id);
|
|
27
|
+
appendUnique(ensureStringArray(aiSettings, "modelOverrideEnabled"), model.id);
|
|
28
|
+
removeValue(ensureStringArray(aiSettings, "modelOverrideDisabled"), model.id);
|
|
29
|
+
}
|
|
30
|
+
const preferences = ensureRecord(aiSettings, "modelParameterPreferences");
|
|
31
|
+
const updatedAt = new Date().toISOString();
|
|
32
|
+
for (const model of models) {
|
|
33
|
+
preferences[model.id] = {
|
|
34
|
+
modelId: model.id,
|
|
35
|
+
parameters: localDesktopParameterValues(true),
|
|
36
|
+
updatedAt,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const firstModel = models[0];
|
|
40
|
+
if (firstModel !== undefined) {
|
|
41
|
+
applicationUser.useOpenAIKey = true;
|
|
42
|
+
applicationUser.openAIBaseUrl = firstModel.baseUrl;
|
|
43
|
+
applicationUser.openAIKey = firstModel.apiKey;
|
|
44
|
+
const modelConfig = ensureRecord(aiSettings, "modelConfig");
|
|
45
|
+
const selectedModel = {
|
|
46
|
+
modelId: firstModel.id,
|
|
47
|
+
parameters: localDesktopParameterValues(true),
|
|
48
|
+
};
|
|
49
|
+
for (const key of ["composer", "background-composer"]) {
|
|
50
|
+
modelConfig[key] = {
|
|
51
|
+
modelName: firstModel.id,
|
|
52
|
+
maxMode: true,
|
|
53
|
+
selectedModels: [selectedModel],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const featureModelConfigs = ensureRecord(applicationUser, "featureModelConfigs");
|
|
58
|
+
for (const value of Object.values(featureModelConfigs)) {
|
|
59
|
+
if (!isPlainRecord(value)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const fallbackModels = ensureStringArray(value, "fallbackModels");
|
|
63
|
+
for (const model of models) {
|
|
64
|
+
appendUnique(fallbackModels, model.id);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function buildLocalDesktopModelEntry(model) {
|
|
69
|
+
const tooltipData = {
|
|
70
|
+
primaryText: "",
|
|
71
|
+
secondaryText: "",
|
|
72
|
+
secondaryWarningText: false,
|
|
73
|
+
icon: "",
|
|
74
|
+
tertiaryText: "",
|
|
75
|
+
tertiaryTextUrl: "",
|
|
76
|
+
markdownContent: `**${model.displayName}**<br />Local OpenAI-compatible model served by cursorkit.<br /><br />${model.contextTokenLimit.toLocaleString()} token context window`,
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
name: model.id,
|
|
80
|
+
serverModelName: model.id,
|
|
81
|
+
clientDisplayName: model.displayName,
|
|
82
|
+
inputboxShortModelName: model.displayName,
|
|
83
|
+
vendorName: "local",
|
|
84
|
+
vendor: { displayName: "Local" },
|
|
85
|
+
supportsAgent: true,
|
|
86
|
+
supportsCmdK: false,
|
|
87
|
+
supportsImages: false,
|
|
88
|
+
supportsMaxMode: true,
|
|
89
|
+
supportsNonMaxMode: true,
|
|
90
|
+
supportsPlanMode: true,
|
|
91
|
+
supportsSandboxing: false,
|
|
92
|
+
supportsThinking: false,
|
|
93
|
+
cloudAgentEffortModes: [],
|
|
94
|
+
defaultOn: true,
|
|
95
|
+
degradationStatus: 0,
|
|
96
|
+
isRecommendedForBackgroundComposer: false,
|
|
97
|
+
legacySlugs: [model.id],
|
|
98
|
+
idAliases: [model.id, model.displayName],
|
|
99
|
+
parameterDefinitions: localDesktopParameterDefinitions(),
|
|
100
|
+
namedModelSectionIndex: 10_000,
|
|
101
|
+
visibleInRoutedModelView: true,
|
|
102
|
+
tooltipData,
|
|
103
|
+
tooltipDataForMaxMode: tooltipData,
|
|
104
|
+
variants: [
|
|
105
|
+
localDesktopVariantConfig(model, tooltipData, false),
|
|
106
|
+
localDesktopVariantConfig(model, tooltipData, true),
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function localDesktopVariantConfig(model, tooltipData, isMaxMode) {
|
|
111
|
+
return {
|
|
112
|
+
parameterValues: localDesktopParameterValues(isMaxMode),
|
|
113
|
+
displayName: model.displayName,
|
|
114
|
+
isMaxMode,
|
|
115
|
+
isDefaultMaxConfig: isMaxMode,
|
|
116
|
+
isDefaultNonMaxConfig: !isMaxMode,
|
|
117
|
+
tooltipData,
|
|
118
|
+
displayNameOutsidePicker: model.displayName,
|
|
119
|
+
variantStringRepresentation: localDesktopVariantString(model.id, isMaxMode),
|
|
120
|
+
legacySlug: model.id,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function localDesktopVariantString(modelId, isMaxMode) {
|
|
124
|
+
const context = isMaxMode ? "1m" : "272k";
|
|
125
|
+
return `${modelId}[context=${context},reasoning=medium,fast=false]`;
|
|
126
|
+
}
|
|
127
|
+
function localDesktopParameterValues(isMaxMode) {
|
|
128
|
+
return [
|
|
129
|
+
{ id: "context", value: isMaxMode ? "1m" : "272k" },
|
|
130
|
+
{ id: "reasoning", value: "medium" },
|
|
131
|
+
{ id: "fast", value: "false" },
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
function localDesktopParameterDefinitions() {
|
|
135
|
+
return [
|
|
136
|
+
{
|
|
137
|
+
id: "context",
|
|
138
|
+
name: "Context",
|
|
139
|
+
markdownTooltip: "Context size the model has available.",
|
|
140
|
+
parameterType: {
|
|
141
|
+
enumParameter: {
|
|
142
|
+
values: [
|
|
143
|
+
{ value: "272k", displayName: "272K" },
|
|
144
|
+
{ value: "1m", displayName: "1M" },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "reasoning",
|
|
151
|
+
name: "Reasoning",
|
|
152
|
+
markdownTooltip: "Reasoning effort the model uses to generate its response.",
|
|
153
|
+
parameterType: {
|
|
154
|
+
enumParameter: {
|
|
155
|
+
values: [
|
|
156
|
+
{ value: "none", displayName: "None" },
|
|
157
|
+
{ value: "low", displayName: "Low" },
|
|
158
|
+
{ value: "medium", displayName: "Medium" },
|
|
159
|
+
{ value: "high", displayName: "High" },
|
|
160
|
+
{ value: "extra-high", displayName: "Extra High" },
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
isCycleableByHotkey: true,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "fast",
|
|
168
|
+
name: "Fast",
|
|
169
|
+
markdownTooltip: "Use the provider's fast lane when supported.",
|
|
170
|
+
parameterType: {
|
|
171
|
+
booleanParameter: {
|
|
172
|
+
values: [{ value: "false" }, { value: "true", displayName: "Fast" }],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
function ensureRecord(target, key) {
|
|
179
|
+
if (!isPlainRecord(target[key])) {
|
|
180
|
+
target[key] = {};
|
|
181
|
+
}
|
|
182
|
+
return target[key];
|
|
183
|
+
}
|
|
184
|
+
function ensureStringArray(target, key) {
|
|
185
|
+
const values = Array.isArray(target[key])
|
|
186
|
+
? target[key].filter((value) => typeof value === "string")
|
|
187
|
+
: [];
|
|
188
|
+
target[key] = values;
|
|
189
|
+
return values;
|
|
190
|
+
}
|
|
191
|
+
function appendUnique(values, value) {
|
|
192
|
+
if (!values.includes(value)) {
|
|
193
|
+
values.push(value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function removeValue(values, value) {
|
|
197
|
+
const index = values.indexOf(value);
|
|
198
|
+
if (index !== -1) {
|
|
199
|
+
values.splice(index, 1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function isPlainRecord(value) {
|
|
203
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
204
|
+
}
|
|
@@ -5,9 +5,19 @@ export interface DesktopConnectProxyOptions {
|
|
|
5
5
|
bridgeHost: string;
|
|
6
6
|
bridgePort: number;
|
|
7
7
|
logPath?: string;
|
|
8
|
+
/**
|
|
9
|
+
* When true, CONNECT requests to non-Cursor hosts are tunneled through to
|
|
10
|
+
* their destination, turning this into an open forwarder. Defaults to false
|
|
11
|
+
* so only Cursor backends (cursorHostnames) are reachable.
|
|
12
|
+
*/
|
|
8
13
|
passthrough?: boolean;
|
|
9
14
|
cursorHostnames?: readonly string[];
|
|
10
15
|
headerTimeoutMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Allow binding to a non-loopback host. Off by default to avoid exposing the
|
|
18
|
+
* proxy beyond the local machine.
|
|
19
|
+
*/
|
|
20
|
+
allowExternalHost?: boolean;
|
|
11
21
|
}
|
|
12
22
|
export interface DesktopConnectProxy {
|
|
13
23
|
server: net.Server;
|
|
@@ -5,8 +5,11 @@ const CONNECT_LINE_PATTERN = /^CONNECT\s+([^\s]+)\s+HTTP\/\d(?:\.\d)?$/i;
|
|
|
5
5
|
const DEFAULT_HEADER_TIMEOUT_MS = 5_000;
|
|
6
6
|
const MAX_CONNECT_HEADER_BYTES = 32 * 1024;
|
|
7
7
|
export async function startDesktopConnectProxy(options) {
|
|
8
|
+
if (options.allowExternalHost !== true && !isLoopbackHost(options.host)) {
|
|
9
|
+
throw new Error(`Refusing to bind CONNECT proxy to non-loopback host ${options.host}. Set allowExternalHost: true to override.`);
|
|
10
|
+
}
|
|
8
11
|
const cursorHostnames = new Set(options.cursorHostnames ?? DESKTOP_HOSTNAMES);
|
|
9
|
-
const passthrough = options.passthrough ??
|
|
12
|
+
const passthrough = options.passthrough ?? false;
|
|
10
13
|
const activeSockets = new Set();
|
|
11
14
|
const server = net.createServer((client) => {
|
|
12
15
|
activeSockets.add(client);
|
|
@@ -155,6 +158,9 @@ function handleClient(client, options, cursorHostnames, passthrough, activeSocke
|
|
|
155
158
|
});
|
|
156
159
|
});
|
|
157
160
|
}
|
|
161
|
+
function isLoopbackHost(host) {
|
|
162
|
+
return host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
163
|
+
}
|
|
158
164
|
function parseConnectDestination(value) {
|
|
159
165
|
if (value.startsWith("[")) {
|
|
160
166
|
const closeBracket = value.indexOf("]");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const MODEL_FUSION_SCHEMA_BUNDLE_HASH = "sha256:
|
|
1
|
+
export declare const MODEL_FUSION_SCHEMA_BUNDLE_HASH = "sha256:aae33b89a771fd5916e21bfffc5993d2d7ef98ecfc8542ba9570a8c99074d541";
|
|
2
2
|
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
3
3
|
[key: string]: JsonValue;
|
|
4
4
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
export const MODEL_FUSION_SCHEMA_BUNDLE_HASH = "sha256:
|
|
2
|
+
export const MODEL_FUSION_SCHEMA_BUNDLE_HASH = "sha256:aae33b89a771fd5916e21bfffc5993d2d7ef98ecfc8542ba9570a8c99074d541";
|
|
3
3
|
const STATUSES = [
|
|
4
4
|
"pending",
|
|
5
5
|
"running",
|
package/dist/src/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1
2
|
import http from "node:http";
|
|
2
3
|
import http2 from "node:http2";
|
|
3
4
|
import https from "node:https";
|
|
@@ -101,7 +102,9 @@ export async function createBridgeRuntime(config, logger) {
|
|
|
101
102
|
}
|
|
102
103
|
export async function startServer(runtime) {
|
|
103
104
|
const listener = (request, response) => {
|
|
104
|
-
|
|
105
|
+
handleRequest(runtime, request, response).catch((error) => {
|
|
106
|
+
handleRequestFailure(runtime, response, error);
|
|
107
|
+
});
|
|
105
108
|
};
|
|
106
109
|
const server = runtime.config.useTls
|
|
107
110
|
? runtime.config.desktopMode
|
|
@@ -252,6 +255,35 @@ async function handleRequest(runtime, request, response) {
|
|
|
252
255
|
}
|
|
253
256
|
proxyRequest(request, response, runtime.config, runtime.logger);
|
|
254
257
|
}
|
|
258
|
+
function handleRequestFailure(runtime, response, error) {
|
|
259
|
+
runtime.logger.error("request handler crashed", {
|
|
260
|
+
error: error instanceof Error ? error.message : String(error),
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
if (response.headersSent) {
|
|
264
|
+
response.destroy();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
response.writeHead(500, { "content-type": "application/json" });
|
|
268
|
+
response.end(JSON.stringify({ error: "internal bridge error" }));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
response.destroy();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Length-independent constant-time comparison so bridge-token checks do not leak
|
|
276
|
+
* the secret via early-mismatch timing. A missing header never matches.
|
|
277
|
+
*/
|
|
278
|
+
function constantTimeEquals(value, expected) {
|
|
279
|
+
if (value === undefined)
|
|
280
|
+
return false;
|
|
281
|
+
const valueBuf = Buffer.from(value);
|
|
282
|
+
const expectedBuf = Buffer.from(expected);
|
|
283
|
+
if (valueBuf.length !== expectedBuf.length)
|
|
284
|
+
return false;
|
|
285
|
+
return timingSafeEqual(valueBuf, expectedBuf);
|
|
286
|
+
}
|
|
255
287
|
function authorizeRequest(runtime, request, response) {
|
|
256
288
|
const token = runtime.config.authToken;
|
|
257
289
|
if (token === undefined) {
|
|
@@ -259,7 +291,8 @@ function authorizeRequest(runtime, request, response) {
|
|
|
259
291
|
}
|
|
260
292
|
const authorization = headerValue(request.headers.authorization);
|
|
261
293
|
const bridgeToken = headerValue(request.headers["x-cursor-rpc-auth"]);
|
|
262
|
-
const authorized = authorization
|
|
294
|
+
const authorized = constantTimeEquals(authorization, `Bearer ${token}`) ||
|
|
295
|
+
constantTimeEquals(bridgeToken, token);
|
|
263
296
|
if (authorized) {
|
|
264
297
|
return true;
|
|
265
298
|
}
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"status": "reserved-for-future-internal-streaming-connect-grpc",
|
|
50
50
|
"notes": "Do not require protobuf/Buf for v1 package or HTTP/JSON service consumption."
|
|
51
51
|
},
|
|
52
|
-
"schemaBundleHash": "sha256:
|
|
52
|
+
"schemaBundleHash": "sha256:aae33b89a771fd5916e21bfffc5993d2d7ef98ecfc8542ba9570a8c99074d541",
|
|
53
53
|
"persistedJsonSchemas": [
|
|
54
54
|
"harness-run-request.v1",
|
|
55
55
|
"harness-run-result.v1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/cursorkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Unofficial research bridge for experimenting with Cursor's ConnectRPC protocol against a self-hosted OpenAI-compatible model.",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"modelFusionProtocol": {
|
|
20
20
|
"packageName": "@velum-labs/model-fusion-protocol",
|
|
21
|
-
"version": "0.
|
|
22
|
-
"schemaBundleHash": "sha256:
|
|
21
|
+
"version": "0.2.0",
|
|
22
|
+
"schemaBundleHash": "sha256:aae33b89a771fd5916e21bfffc5993d2d7ef98ecfc8542ba9570a8c99074d541",
|
|
23
23
|
"contracts": "json-schema+openapi-3.1",
|
|
24
24
|
"origin": "fusionkit"
|
|
25
25
|
},
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"@bufbuild/buf": "^1.70.0",
|
|
62
62
|
"@bufbuild/protoc-gen-es": "^2.12.0",
|
|
63
63
|
"@types/node": "24.10.1",
|
|
64
|
-
"@velum-labs/model-fusion-protocol": "0.
|
|
64
|
+
"@velum-labs/model-fusion-protocol": "0.2.0",
|
|
65
|
+
"@vitest/coverage-v8": "4.1.8",
|
|
65
66
|
"prettier": "3.8.4",
|
|
66
67
|
"tsx": "4.22.4",
|
|
67
68
|
"typescript": "6.0.3",
|
|
@@ -78,6 +79,7 @@
|
|
|
78
79
|
"baseline:generate": "tsx src/tools/baselineInventory.ts",
|
|
79
80
|
"baseline:check": "tsx src/tools/baselineInventory.ts --check",
|
|
80
81
|
"test": "vitest run tests",
|
|
82
|
+
"test:coverage": "vitest run --coverage",
|
|
81
83
|
"e2e:cursor-agent": "python3 tests/e2e/cursor-agent-smoke.py",
|
|
82
84
|
"model-fusion:protocol:check": "tsx src/tools/checkModelFusionProtocol.ts",
|
|
83
85
|
"release:publish:check": "tsx src/tools/checkReleasePublishConfig.ts",
|