@rama_nigg/open-cursor 2.3.14 → 2.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -22
- package/dist/cli/discover.js +57 -8
- package/dist/cli/mcptool.js +15187 -0
- package/dist/cli/opencode-cursor.js +68 -19
- package/dist/index.js +15944 -626
- package/dist/plugin-entry.js +15833 -586
- package/package.json +7 -5
- package/src/cli/mcptool.ts +133 -0
- package/src/cli/model-discovery.ts +21 -1
- package/src/client/simple.ts +16 -3
- package/src/mcp/client-manager.ts +166 -0
- package/src/mcp/config.ts +98 -0
- package/src/mcp/tool-bridge.ts +131 -0
- package/src/models/discovery.ts +13 -3
- package/src/models/sync.ts +153 -0
- package/src/plugin.ts +127 -9
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking model auto-refresh for plugin startup.
|
|
3
|
+
*
|
|
4
|
+
* Discovers currently available models from cursor-agent and merges them
|
|
5
|
+
* into the opencode.json config. Only adds new models — never removes
|
|
6
|
+
* user-configured ones. Safe to call fire-and-forget; all errors are
|
|
7
|
+
* caught and logged silently.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
existsSync as nodeExistsSync,
|
|
11
|
+
readFileSync as nodeReadFileSync,
|
|
12
|
+
writeFileSync as nodeWriteFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { discoverModelsFromCursorAgent, type DiscoveredModel } from "../cli/model-discovery.js";
|
|
15
|
+
import { resolveOpenCodeConfigPath } from "../plugin-toggle.js";
|
|
16
|
+
import { createLogger, type Logger } from "../utils/logger.js";
|
|
17
|
+
|
|
18
|
+
const log = createLogger("model-sync");
|
|
19
|
+
const PROVIDER_ID = "cursor-acp";
|
|
20
|
+
|
|
21
|
+
type ModelConfigEntry = { name: string };
|
|
22
|
+
type ProviderConfig = { models?: Record<string, unknown> } & Record<string, unknown>;
|
|
23
|
+
type OpenCodeConfig = {
|
|
24
|
+
provider?: Record<string, ProviderConfig | undefined>;
|
|
25
|
+
} & Record<string, unknown>;
|
|
26
|
+
type AutoRefreshModelsDeps = {
|
|
27
|
+
defer: () => Promise<void>;
|
|
28
|
+
discoverModels: () => DiscoveredModel[];
|
|
29
|
+
env: NodeJS.ProcessEnv;
|
|
30
|
+
existsSync: (path: string) => boolean;
|
|
31
|
+
log: Logger;
|
|
32
|
+
readFileSync: (path: string, encoding: BufferEncoding) => string;
|
|
33
|
+
writeFileSync: (path: string, data: string, encoding: BufferEncoding) => void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const defaultDeps: AutoRefreshModelsDeps = {
|
|
37
|
+
defer: () => Promise.resolve(),
|
|
38
|
+
discoverModels: discoverModelsFromCursorAgent,
|
|
39
|
+
env: process.env,
|
|
40
|
+
existsSync: nodeExistsSync,
|
|
41
|
+
log,
|
|
42
|
+
readFileSync: nodeReadFileSync,
|
|
43
|
+
writeFileSync: nodeWriteFileSync,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseConfig(raw: string): OpenCodeConfig | null {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
return isRecord(parsed) ? (parsed as OpenCodeConfig) : null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getProviderConfig(config: OpenCodeConfig): ProviderConfig | null {
|
|
60
|
+
if (!isRecord(config.provider)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const provider = config.provider[PROVIDER_ID];
|
|
65
|
+
return isRecord(provider) ? (provider as ProviderConfig) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getExistingModels(provider: ProviderConfig): Record<string, unknown> {
|
|
69
|
+
return isRecord(provider.models) ? { ...provider.models } : {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function yieldForFireAndForget(): Promise<void> {
|
|
73
|
+
return Promise.resolve();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Auto-refresh models at plugin startup.
|
|
78
|
+
*
|
|
79
|
+
* - Reads the current opencode.json config
|
|
80
|
+
* - Queries cursor-agent for available models
|
|
81
|
+
* - Merges discovered models into the provider config (additive only)
|
|
82
|
+
* - Writes back if any new models were added
|
|
83
|
+
*
|
|
84
|
+
* This function never throws. All failures are logged at debug level
|
|
85
|
+
* and silently ignored so plugin startup is never blocked.
|
|
86
|
+
*/
|
|
87
|
+
export async function autoRefreshModels(
|
|
88
|
+
deps: Partial<AutoRefreshModelsDeps> = {},
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const resolvedDeps: AutoRefreshModelsDeps = {
|
|
91
|
+
...defaultDeps,
|
|
92
|
+
defer: yieldForFireAndForget,
|
|
93
|
+
...deps,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
await resolvedDeps.defer();
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const configPath = resolveOpenCodeConfigPath(resolvedDeps.env);
|
|
100
|
+
if (!resolvedDeps.existsSync(configPath)) {
|
|
101
|
+
resolvedDeps.log.debug("Config file not found, skipping model auto-refresh", { configPath });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const raw = resolvedDeps.readFileSync(configPath, "utf8");
|
|
106
|
+
const config = parseConfig(raw);
|
|
107
|
+
if (!config) {
|
|
108
|
+
resolvedDeps.log.debug("Config file is not valid JSON, skipping model auto-refresh");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const provider = getProviderConfig(config);
|
|
113
|
+
if (!provider) {
|
|
114
|
+
resolvedDeps.log.debug("Provider section not found in config, skipping model auto-refresh");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const existingModels = getExistingModels(provider);
|
|
119
|
+
let discovered: DiscoveredModel[];
|
|
120
|
+
try {
|
|
121
|
+
discovered = resolvedDeps.discoverModels();
|
|
122
|
+
} catch (err) {
|
|
123
|
+
resolvedDeps.log.debug("cursor-agent model discovery failed, skipping auto-refresh", {
|
|
124
|
+
error: String(err),
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let addedCount = 0;
|
|
130
|
+
for (const model of discovered) {
|
|
131
|
+
if (Object.prototype.hasOwnProperty.call(existingModels, model.id)) continue;
|
|
132
|
+
existingModels[model.id] = { name: model.name } satisfies ModelConfigEntry;
|
|
133
|
+
addedCount++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (addedCount === 0) {
|
|
137
|
+
resolvedDeps.log.debug("Model auto-refresh: no new models found", {
|
|
138
|
+
existing: Object.keys(existingModels).length,
|
|
139
|
+
discovered: discovered.length,
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
provider.models = existingModels;
|
|
145
|
+
resolvedDeps.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
146
|
+
resolvedDeps.log.info("Model auto-refresh: added new models", {
|
|
147
|
+
added: addedCount,
|
|
148
|
+
total: Object.keys(existingModels).length,
|
|
149
|
+
});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
resolvedDeps.log.debug("Model auto-refresh failed", { error: String(err) });
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -24,6 +24,10 @@ import { toOpenAiParameters, describeTool } from "./tools/schema.js";
|
|
|
24
24
|
import { ToolRouter } from "./tools/router.js";
|
|
25
25
|
import { SkillLoader } from "./tools/skills/loader.js";
|
|
26
26
|
import { SkillResolver } from "./tools/skills/resolver.js";
|
|
27
|
+
import { autoRefreshModels } from "./models/sync.js";
|
|
28
|
+
import { readMcpConfigs } from "./mcp/config.js";
|
|
29
|
+
import { McpClientManager } from "./mcp/client-manager.js";
|
|
30
|
+
import { buildMcpToolHookEntries, buildMcpToolDefinitions } from "./mcp/tool-bridge.js";
|
|
27
31
|
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
28
32
|
import { ToolRegistry as CoreRegistry } from "./tools/core/registry.js";
|
|
29
33
|
import { LocalExecutor } from "./tools/executors/local.js";
|
|
@@ -76,6 +80,61 @@ function debugLogToFile(message: string, data: any): void {
|
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
interface McpToolSummary {
|
|
84
|
+
serverName: string;
|
|
85
|
+
toolName: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
params?: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildAvailableToolsSystemMessage(
|
|
91
|
+
lastToolNames: string[],
|
|
92
|
+
lastToolMap: Array<{ id: string; name: string }>,
|
|
93
|
+
mcpToolDefs: any[],
|
|
94
|
+
mcpToolSummaries?: McpToolSummary[],
|
|
95
|
+
): string | null {
|
|
96
|
+
const parts: string[] = [];
|
|
97
|
+
|
|
98
|
+
if (lastToolNames.length > 0 || lastToolMap.length > 0) {
|
|
99
|
+
const names = lastToolNames.join(", ");
|
|
100
|
+
const mapping = lastToolMap.map((m) => `${m.id} -> ${m.name}`).join("; ");
|
|
101
|
+
parts.push(`Available OpenCode tools (use via tool calls): ${names}. Original skill ids mapped as: ${mapping}. Aliases include oc_skill_* and oc_superskill_* when applicable.`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (mcpToolSummaries && mcpToolSummaries.length > 0) {
|
|
105
|
+
const servers = new Map<string, McpToolSummary[]>();
|
|
106
|
+
for (const s of mcpToolSummaries) {
|
|
107
|
+
const list = servers.get(s.serverName) ?? [];
|
|
108
|
+
list.push(s);
|
|
109
|
+
servers.set(s.serverName, list);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const lines: string[] = [
|
|
113
|
+
"MCP TOOLS — Use via Shell with the `mcptool` CLI.",
|
|
114
|
+
"Syntax: mcptool call <server> <tool> [json-args]",
|
|
115
|
+
"",
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
for (const [server, tools] of servers) {
|
|
119
|
+
lines.push(`Server: ${server}`);
|
|
120
|
+
for (const t of tools) {
|
|
121
|
+
const paramHint = t.params?.length ? ` (params: ${t.params.join(", ")})` : "";
|
|
122
|
+
lines.push(` - ${t.toolName}${paramHint}${t.description ? " — " + t.description : ""}`);
|
|
123
|
+
}
|
|
124
|
+
if (tools.length > 0) {
|
|
125
|
+
const ex = tools[0];
|
|
126
|
+
const exArgs = ex.params?.length ? ` '{"${ex.params[0]}":"..."}'` : "";
|
|
127
|
+
lines.push(` Example: mcptool call ${server} ${ex.toolName}${exArgs}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push("");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
parts.push(lines.join("\n"));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
79
138
|
export async function ensurePluginDirectory(): Promise<void> {
|
|
80
139
|
const configHome = process.env.XDG_CONFIG_HOME
|
|
81
140
|
? resolve(process.env.XDG_CONFIG_HOME)
|
|
@@ -1735,8 +1794,51 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
|
|
|
1735
1794
|
});
|
|
1736
1795
|
await ensurePluginDirectory();
|
|
1737
1796
|
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1797
|
+
// Auto-refresh model list from cursor-agent (non-blocking, fire-and-forget)
|
|
1798
|
+
autoRefreshModels().catch(() => {});
|
|
1799
|
+
|
|
1800
|
+
// MCP tool bridge: connect to MCP servers and register their tools.
|
|
1801
|
+
// We await init so tools are available before the plugin returns its tool hook.
|
|
1802
|
+
const mcpManager = new McpClientManager();
|
|
1803
|
+
let mcpToolEntries: Record<string, any> = {};
|
|
1804
|
+
let mcpToolDefs: any[] = [];
|
|
1805
|
+
let mcpToolSummaries: McpToolSummary[] = [];
|
|
1806
|
+
const mcpEnabled = process.env.CURSOR_ACP_MCP_BRIDGE !== "false"; // default ON
|
|
1807
|
+
|
|
1808
|
+
if (mcpEnabled) {
|
|
1809
|
+
try {
|
|
1810
|
+
const configs = readMcpConfigs();
|
|
1811
|
+
if (configs.length === 0) {
|
|
1812
|
+
log.debug("No MCP servers configured, skipping MCP bridge");
|
|
1813
|
+
} else {
|
|
1814
|
+
log.debug("MCP bridge: connecting to servers", { count: configs.length });
|
|
1815
|
+
|
|
1816
|
+
await Promise.allSettled(configs.map((c) => mcpManager.connectServer(c)));
|
|
1817
|
+
|
|
1818
|
+
const tools = mcpManager.listTools();
|
|
1819
|
+
if (tools.length === 0) {
|
|
1820
|
+
log.debug("MCP bridge: no tools discovered");
|
|
1821
|
+
} else {
|
|
1822
|
+
mcpToolEntries = buildMcpToolHookEntries(tools, mcpManager);
|
|
1823
|
+
mcpToolDefs = buildMcpToolDefinitions(tools);
|
|
1824
|
+
mcpToolSummaries = tools.map((t) => ({
|
|
1825
|
+
serverName: t.serverName,
|
|
1826
|
+
toolName: t.name,
|
|
1827
|
+
description: t.description,
|
|
1828
|
+
params: t.inputSchema
|
|
1829
|
+
? Object.keys((t.inputSchema as any).properties ?? {})
|
|
1830
|
+
: undefined,
|
|
1831
|
+
}));
|
|
1832
|
+
log.info("MCP bridge: registered tools", {
|
|
1833
|
+
servers: mcpManager.connectedServers.length,
|
|
1834
|
+
tools: Object.keys(mcpToolEntries).length,
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
} catch (err) {
|
|
1839
|
+
log.debug("MCP bridge init failed", { error: String(err) });
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1740
1842
|
|
|
1741
1843
|
// Initialize toast service for MCP pass-through notifications
|
|
1742
1844
|
toastService.setClient(client);
|
|
@@ -1861,7 +1963,7 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
|
|
|
1861
1963
|
const toolHookEntries = buildToolHookEntries(localRegistry, workspaceDirectory);
|
|
1862
1964
|
|
|
1863
1965
|
return {
|
|
1864
|
-
tool: toolHookEntries,
|
|
1966
|
+
tool: { ...toolHookEntries, ...mcpToolEntries },
|
|
1865
1967
|
auth: {
|
|
1866
1968
|
provider: CURSOR_PROVIDER_ID,
|
|
1867
1969
|
async loader(_getAuth: () => Promise<Auth>) {
|
|
@@ -1934,16 +2036,32 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
|
|
|
1934
2036
|
log.debug("Failed to refresh tools", { error: String(err) });
|
|
1935
2037
|
}
|
|
1936
2038
|
}
|
|
2039
|
+
|
|
2040
|
+
// Append MCP bridge tool definitions so the model can call them
|
|
2041
|
+
if (mcpToolDefs.length > 0) {
|
|
2042
|
+
const beforeTools = Array.isArray(output.options.tools) ? output.options.tools : [];
|
|
2043
|
+
if (Array.isArray(output.options.tools)) {
|
|
2044
|
+
output.options.tools = [...output.options.tools, ...mcpToolDefs];
|
|
2045
|
+
} else {
|
|
2046
|
+
output.options.tools = mcpToolDefs;
|
|
2047
|
+
}
|
|
2048
|
+
const afterTools = Array.isArray(output.options.tools) ? output.options.tools : [];
|
|
2049
|
+
log.debug("Injected MCP tool definitions into chat.params", {
|
|
2050
|
+
injectedCount: mcpToolDefs.length,
|
|
2051
|
+
beforeCount: beforeTools.length,
|
|
2052
|
+
afterCount: afterTools.length,
|
|
2053
|
+
mcpNames: mcpToolDefs.slice(0, 10).map((t: any) => t?.function?.name ?? t?.name ?? "unknown"),
|
|
2054
|
+
tailNames: afterTools.slice(-10).map((t: any) => t?.function?.name ?? t?.name ?? "unknown"),
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
1937
2057
|
},
|
|
1938
2058
|
|
|
1939
2059
|
async "experimental.chat.system.transform"(input: any, output: { system: string[] }) {
|
|
1940
|
-
if (!toolsEnabled
|
|
1941
|
-
const
|
|
1942
|
-
|
|
2060
|
+
if (!toolsEnabled) return;
|
|
2061
|
+
const systemMessage = buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries);
|
|
2062
|
+
if (!systemMessage) return;
|
|
1943
2063
|
output.system = output.system || [];
|
|
1944
|
-
output.system.push(
|
|
1945
|
-
`Available OpenCode tools (use via tool calls): ${names}. Original skill ids mapped as: ${mapping}. Aliases include oc_skill_* and oc_superskill_* when applicable.`
|
|
1946
|
-
);
|
|
2064
|
+
output.system.push(systemMessage);
|
|
1947
2065
|
},
|
|
1948
2066
|
};
|
|
1949
2067
|
};
|