opencode-cursor-provider 0.1.0 → 0.2.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.
- package/README.md +13 -0
- package/dist/catalog/source.d.ts +11 -4
- package/dist/catalog/source.js +74 -21
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +34 -12
- package/package.json +20 -4
package/README.md
CHANGED
|
@@ -107,6 +107,19 @@ bun run build
|
|
|
107
107
|
|
|
108
108
|
The source code is in `src/`. The build writes the package files to `dist/`.
|
|
109
109
|
|
|
110
|
+
## Release
|
|
111
|
+
|
|
112
|
+
Releases use [Release Please](https://github.com/googleapis/release-please) with conventional commits (`feat:`, `fix:`, `docs:`, …). Merging to `main` opens or updates a release PR. Merging that PR tags a GitHub release and publishes to npm.
|
|
113
|
+
|
|
114
|
+
Configure an npm trusted publisher with these values:
|
|
115
|
+
|
|
116
|
+
- Organization or user: `pragmaticivan`
|
|
117
|
+
- Repository: `opencode-cursor-provider`
|
|
118
|
+
- Workflow filename: `release-please.yml`
|
|
119
|
+
- Allowed action: `npm publish`
|
|
120
|
+
|
|
121
|
+
The workflow uses OpenID Connect and does not need an npm token.
|
|
122
|
+
|
|
110
123
|
## License
|
|
111
124
|
|
|
112
125
|
Apache License 2.0. See [`LICENSE`](LICENSE).
|
package/dist/catalog/source.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { CursorLink } from "../auth/link.ts";
|
|
1
|
+
import type { CursorApiKey } from "../ids.ts";
|
|
3
2
|
import { type CursorModelDescriptor } from "./catalog.ts";
|
|
3
|
+
export interface ModelSourceDependencies {
|
|
4
|
+
restoreCredential(): Promise<void>;
|
|
5
|
+
resolveApiKey(): Promise<CursorApiKey | undefined>;
|
|
6
|
+
rejectCredential(): void;
|
|
7
|
+
reloadCatalog(): Promise<void>;
|
|
8
|
+
listModels?(apiKey: CursorApiKey): Promise<unknown>;
|
|
9
|
+
}
|
|
4
10
|
export interface ModelSource {
|
|
5
11
|
list(): readonly CursorModelDescriptor[];
|
|
6
|
-
refresh(
|
|
12
|
+
refresh(): Promise<void>;
|
|
13
|
+
close(): Promise<void>;
|
|
7
14
|
}
|
|
8
|
-
export declare function createModelSource(
|
|
15
|
+
export declare function createModelSource(dependencies: ModelSourceDependencies): ModelSource;
|
package/dist/catalog/source.js
CHANGED
|
@@ -1,30 +1,83 @@
|
|
|
1
1
|
import { AuthenticationError, Cursor } from "@cursor/sdk";
|
|
2
2
|
import { parseListedModels, SEED_MODELS } from "./catalog.js";
|
|
3
|
-
export function createModelSource(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
3
|
+
export function createModelSource(dependencies) {
|
|
4
|
+
const listModels = dependencies.listModels ?? ((apiKey) => Cursor.models.list({ apiKey }));
|
|
5
|
+
let snapshot = { kind: "seed", models: SEED_MODELS };
|
|
6
|
+
let catalogDirty = false;
|
|
7
|
+
let activeDrain;
|
|
8
|
+
let trailingRefresh = false;
|
|
9
|
+
let closed = false;
|
|
10
|
+
let closing;
|
|
11
|
+
async function reloadDirtyCatalog() {
|
|
12
|
+
if (!catalogDirty)
|
|
13
|
+
return;
|
|
14
|
+
try {
|
|
15
|
+
await dependencies.reloadCatalog();
|
|
16
|
+
catalogDirty = false;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function selectSeeds() {
|
|
23
|
+
if (snapshot.kind === "seed")
|
|
24
|
+
return;
|
|
25
|
+
snapshot = { kind: "seed", models: SEED_MODELS };
|
|
26
|
+
catalogDirty = true;
|
|
27
|
+
}
|
|
28
|
+
async function refreshOnce() {
|
|
29
|
+
await dependencies.restoreCredential();
|
|
30
|
+
const apiKey = await dependencies.resolveApiKey();
|
|
31
|
+
if (apiKey === undefined) {
|
|
32
|
+
selectSeeds();
|
|
33
|
+
await reloadDirtyCatalog();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
snapshot = { kind: "listed", models: parseListedModels(await listModels(apiKey)) };
|
|
38
|
+
catalogDirty = true;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof AuthenticationError) {
|
|
42
|
+
dependencies.rejectCredential();
|
|
43
|
+
selectSeeds();
|
|
21
44
|
}
|
|
45
|
+
}
|
|
46
|
+
await reloadDirtyCatalog();
|
|
47
|
+
}
|
|
48
|
+
function startDrain() {
|
|
49
|
+
return (async () => {
|
|
22
50
|
try {
|
|
23
|
-
|
|
51
|
+
do {
|
|
52
|
+
trailingRefresh = false;
|
|
53
|
+
await refreshOnce();
|
|
54
|
+
} while (trailingRefresh);
|
|
24
55
|
}
|
|
25
|
-
|
|
26
|
-
|
|
56
|
+
finally {
|
|
57
|
+
activeDrain = undefined;
|
|
27
58
|
}
|
|
59
|
+
})();
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
list() {
|
|
63
|
+
return snapshot.models;
|
|
64
|
+
},
|
|
65
|
+
refresh() {
|
|
66
|
+
if (closed)
|
|
67
|
+
return Promise.resolve();
|
|
68
|
+
if (activeDrain !== undefined) {
|
|
69
|
+
trailingRefresh = true;
|
|
70
|
+
return activeDrain;
|
|
71
|
+
}
|
|
72
|
+
activeDrain = startDrain();
|
|
73
|
+
return activeDrain;
|
|
74
|
+
},
|
|
75
|
+
close() {
|
|
76
|
+
if (closing !== undefined)
|
|
77
|
+
return closing;
|
|
78
|
+
closed = true;
|
|
79
|
+
closing = activeDrain ?? Promise.resolve();
|
|
80
|
+
return closing;
|
|
28
81
|
},
|
|
29
82
|
};
|
|
30
83
|
}
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import { Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
export declare const plugin: Plugin.Plugin;
|
|
3
|
+
export interface ModelWatcherDependencies {
|
|
4
|
+
onLinked(listener: () => void): () => void;
|
|
5
|
+
subscribe(signal: AbortSignal): AsyncIterable<{
|
|
6
|
+
readonly type: string;
|
|
7
|
+
}>;
|
|
8
|
+
refresh(): Promise<void>;
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare function watchModels(dependencies: ModelWatcherDependencies): () => Promise<void>;
|
package/dist/plugin.js
CHANGED
|
@@ -13,7 +13,12 @@ export const plugin = Plugin.define({
|
|
|
13
13
|
id: "opencode-cursor-provider",
|
|
14
14
|
async setup(ctx) {
|
|
15
15
|
const link = createCursorLink();
|
|
16
|
-
const models = createModelSource(
|
|
16
|
+
const models = createModelSource({
|
|
17
|
+
restoreCredential: () => link.restore(ctx),
|
|
18
|
+
resolveApiKey: () => link.resolve(),
|
|
19
|
+
rejectCredential: () => link.reject(),
|
|
20
|
+
reloadCatalog: () => ctx.catalog.reload(),
|
|
21
|
+
});
|
|
17
22
|
const bridge = createSessionAgentBridge({
|
|
18
23
|
link,
|
|
19
24
|
models: () => models.list(),
|
|
@@ -54,9 +59,14 @@ export const plugin = Plugin.define({
|
|
|
54
59
|
event.language = languageModel(event.model, event.model.id, event.options);
|
|
55
60
|
}, { providerID: PROVIDER_ID }),
|
|
56
61
|
];
|
|
57
|
-
const stopRefresh = watchModels(
|
|
62
|
+
const stopRefresh = watchModels({
|
|
63
|
+
onLinked: (listener) => link.onLinked(listener),
|
|
64
|
+
subscribe: (signal) => ctx.event.subscribe({ signal }),
|
|
65
|
+
refresh: () => models.refresh(),
|
|
66
|
+
close: () => models.close(),
|
|
67
|
+
});
|
|
58
68
|
return async () => {
|
|
59
|
-
stopRefresh();
|
|
69
|
+
await stopRefresh();
|
|
60
70
|
for (const registration of registrations.toReversed()) {
|
|
61
71
|
await registration.dispose();
|
|
62
72
|
}
|
|
@@ -94,16 +104,20 @@ function writeSystem(system, stamped) {
|
|
|
94
104
|
system[index] = { type: "text", text };
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
|
-
function watchModels(
|
|
107
|
+
export function watchModels(dependencies) {
|
|
98
108
|
const events = new AbortController();
|
|
109
|
+
let stopped = false;
|
|
110
|
+
let cleanup;
|
|
99
111
|
const refresh = () => {
|
|
100
|
-
|
|
112
|
+
if (stopped)
|
|
113
|
+
return;
|
|
114
|
+
void dependencies.refresh().catch(() => { });
|
|
101
115
|
};
|
|
102
|
-
const stopLinked =
|
|
116
|
+
const stopLinked = dependencies.onLinked(refresh);
|
|
103
117
|
refresh();
|
|
104
|
-
|
|
118
|
+
const eventTask = (async () => {
|
|
105
119
|
try {
|
|
106
|
-
for await (const event of
|
|
120
|
+
for await (const event of dependencies.subscribe(events.signal)) {
|
|
107
121
|
if (event.type === "credential.updated" || event.type === "credential.switched")
|
|
108
122
|
refresh();
|
|
109
123
|
}
|
|
@@ -119,9 +133,17 @@ function watchModels(ctx, link, models) {
|
|
|
119
133
|
}, REFRESH_FOR_MS);
|
|
120
134
|
stop.unref?.();
|
|
121
135
|
return () => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
136
|
+
if (cleanup !== undefined)
|
|
137
|
+
return cleanup;
|
|
138
|
+
cleanup = (async () => {
|
|
139
|
+
stopped = true;
|
|
140
|
+
stopLinked();
|
|
141
|
+
events.abort();
|
|
142
|
+
clearInterval(retry);
|
|
143
|
+
clearTimeout(stop);
|
|
144
|
+
await eventTask;
|
|
145
|
+
await dependencies.close();
|
|
146
|
+
})();
|
|
147
|
+
return cleanup;
|
|
126
148
|
};
|
|
127
149
|
}
|
package/package.json
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-cursor-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "OpenCode 2 plugin that connects Cursor via the official SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/pragmaticivan/opencode-cursor-provider.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/pragmaticivan/opencode-cursor-provider/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/pragmaticivan/opencode-cursor-provider#readme",
|
|
7
15
|
"engines": {
|
|
8
16
|
"node": ">=22.13"
|
|
9
17
|
},
|
|
10
|
-
"main": "./
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
11
20
|
"exports": {
|
|
12
|
-
".":
|
|
13
|
-
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./server": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
}
|
|
14
29
|
},
|
|
15
30
|
"files": [
|
|
16
31
|
"dist"
|
|
17
32
|
],
|
|
18
33
|
"scripts": {
|
|
19
34
|
"build": "tsc -p tsconfig.build.json",
|
|
35
|
+
"prepublishOnly": "bun run build",
|
|
20
36
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
21
37
|
"test": "bun test"
|
|
22
38
|
},
|