@lunacode/opencode-plugin-intelligence 0.1.1 → 0.1.2

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  OpenCode v2 plugin that annotates model display names with the Artificial Analysis Intelligence Index.
4
4
 
5
+ ![OpenCode model picker showing Artificial Analysis intelligence scores alongside model names](.github/preview.heic)
6
+
5
7
  ## What it does
6
8
 
7
9
  Matched models appear as `Model Name (AIQ: 60)` in the `/models` picker. Scores retain up to two decimal places, for example `(AIQ: 53.97)`. AIQ is the Artificial Analysis Intelligence Index score, not a leaderboard rank or a human IQ score.
@@ -12,7 +14,7 @@ It fetches the leaderboard once and writes a local cache file. It reuses that ca
12
14
 
13
15
  ## Install
14
16
 
15
- This plugin is **OpenCode v2 only** and requires **v2.0.28 or newer v2**. It is **not compatible with OpenCode v1**.
17
+ This plugin is **OpenCode v2 only** and requires **v2.0.28 or newer v2** and **Node.js 20 or newer**. It is **not compatible with OpenCode v1**.
16
18
 
17
19
  Add the package to `opencode.json` or `opencode.jsonc`:
18
20
 
@@ -37,16 +39,18 @@ Default cache path (shared with the previous version):
37
39
 
38
40
  Environment variables (set on the OpenCode server):
39
41
 
40
- - `OPENCODE_INTELLIGENCE_FORCE_REFRESH=1` forces a re-fetch instead of reading the cache when the plugin loads.
42
+ - `OPENCODE_INTELLIGENCE_FORCE_REFRESH=1` forces a re-fetch instead of reading the cache when the plugin loads. If it fails, annotations are skipped; this mode does not fall back to stale scores.
41
43
  - `OPENCODE_INTELLIGENCE_CACHE_PATH=/path/to/leaderboard.json` overrides the cache location.
42
44
 
43
- Manual refresh:
45
+ Manual refresh from a local checkout (uses `OPENCODE_INTELLIGENCE_CACHE_PATH` if set):
44
46
 
45
47
  ```bash
46
48
  npm run refresh-cache
47
49
  ```
48
50
 
49
- Reload the plugin or restart OpenCode after manually refreshing to load the new scores. There is no background polling timer. An expired cache refreshes on the next load; if that refresh fails, the plugin warns and uses the stale cache. Separate OpenCode server processes may each issue a refresh; explicit refreshes bypass the 24-hour expiry. If no leaderboard can be loaded, the plugin warns and leaves model names unchanged.
51
+ Reload the plugin or restart OpenCode after manually refreshing to load the new scores. There is no background polling timer. An expired cache refreshes on the next load; if that refresh fails, the plugin warns and uses the stale cache. Separate OpenCode server processes may each issue a refresh; explicit refreshes bypass the 24-hour expiry. If no leaderboard can be loaded, the plugin warns and leaves model names unchanged. Malformed or unreadable cache files also skip annotations; they are not automatically replaced. Use a manual or forced refresh to replace them.
52
+
53
+ Cache loading and refresh failures also show an error toast in the OpenCode TUI. The toast says whether saved scores are still in use or scores are unavailable. Console warnings remain available on headless servers.
50
54
 
51
55
  ## Matching and limitations
52
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunacode/opencode-plugin-intelligence",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "CC-BY-NC-SA-4.0",
5
5
  "description": "Annotate OpenCode model names with the Artificial Analysis Intelligence Index.",
6
6
  "type": "module",
@@ -12,10 +12,12 @@
12
12
  "access": "public"
13
13
  },
14
14
  "exports": {
15
- ".": "./index.js"
15
+ ".": "./index.js",
16
+ "./tui": "./tui.js"
16
17
  },
17
18
  "files": [
18
19
  "index.js",
20
+ "tui.js",
19
21
  "src",
20
22
  "scripts/refresh-cache.mjs",
21
23
  "README.md",
@@ -0,0 +1,11 @@
1
+ // Read the startup result so a TUI attaching after setup still sees failures.
2
+ export const cacheStatusRpc = {
3
+ id: "opencode-plugin-intelligence",
4
+ methods: {
5
+ status: {
6
+ input: { type: "null" },
7
+ output: { anyOf: [{ type: "string" }, { type: "null" }] },
8
+ },
9
+ },
10
+ events: {},
11
+ };
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { cacheStatusRpc } from "./cache-status.js";
1
2
  import {
2
3
  readLeaderboardCache,
3
4
  refreshLeaderboardCache,
@@ -9,9 +10,13 @@ export const IntelligencePlugin = {
9
10
  id: "opencode-plugin-intelligence",
10
11
  async setup(ctx) {
11
12
  let leaderboard;
13
+ let errorMessage = null;
12
14
  try {
13
15
  const options = {
14
16
  cachePath: process.env.OPENCODE_INTELLIGENCE_CACHE_PATH,
17
+ onRefreshError(error) {
18
+ errorMessage = `Leaderboard refresh failed; using saved scores. ${error.message ?? error}`;
19
+ },
15
20
  };
16
21
  leaderboard =
17
22
  process.env.OPENCODE_INTELLIGENCE_FORCE_REFRESH === "1"
@@ -22,24 +27,31 @@ export const IntelligencePlugin = {
22
27
  "opencode-plugin-intelligence: Failed to load Artificial Analysis leaderboard cache",
23
28
  error,
24
29
  );
25
- return;
30
+ errorMessage = `Leaderboard could not be loaded; scores are unavailable. ${error.message ?? error}`;
26
31
  }
27
32
 
28
- if (!leaderboard?.models?.length) return;
29
-
30
- const annotateName = createModelNameAnnotator(leaderboard);
31
- const registration = await ctx.model.transform((editor) => {
32
- for (const model of editor.list()) {
33
- const name = annotateName(model.id, model.name);
34
- if (name !== model.name) {
35
- editor.update(model.providerID, model.id, (draft) => {
36
- draft.name = name;
37
- });
33
+ let registration;
34
+ if (leaderboard?.models?.length) {
35
+ const annotateName = createModelNameAnnotator(leaderboard);
36
+ registration = await ctx.model.transform((editor) => {
37
+ for (const model of editor.list()) {
38
+ const name = annotateName(model.id, model.name);
39
+ if (name !== model.name) {
40
+ editor.update(model.providerID, model.id, (draft) => {
41
+ draft.name = name;
42
+ });
43
+ }
38
44
  }
39
- }
45
+ });
46
+ }
47
+ const statusRegistration = await ctx.rpc.register(cacheStatusRpc, {
48
+ status: async () => errorMessage,
40
49
  });
41
50
 
42
- return () => registration.dispose();
51
+ return async () => {
52
+ await statusRegistration.dispose();
53
+ await registration?.dispose();
54
+ };
43
55
  },
44
56
  };
45
57
 
@@ -26,26 +26,23 @@ async function fetchLeaderboardHtml() {
26
26
  const controller = new AbortController();
27
27
  const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
28
28
 
29
- let response;
30
-
31
29
  try {
32
- response = await fetch(LEADERBOARD_URL, {
30
+ const response = await fetch(LEADERBOARD_URL, {
33
31
  signal: controller.signal,
34
32
  headers: {
35
33
  "user-agent": "opencode-plugin-intelligence/0.1.1",
36
34
  },
37
35
  });
36
+ if (!response.ok) {
37
+ throw new Error(
38
+ `Artificial Analysis request failed with ${response.status}`,
39
+ );
40
+ }
41
+
42
+ return await response.text();
38
43
  } finally {
39
44
  clearTimeout(timeout);
40
45
  }
41
-
42
- if (!response.ok) {
43
- throw new Error(
44
- `Artificial Analysis request failed with ${response.status}`,
45
- );
46
- }
47
-
48
- return response.text();
49
46
  }
50
47
 
51
48
  async function fetchLeaderboardSnapshot() {
@@ -92,6 +89,7 @@ export async function readLeaderboardCache({
92
89
  directory,
93
90
  worktree,
94
91
  cachePath,
92
+ onRefreshError,
95
93
  } = {}) {
96
94
  const resolvedCachePath = resolveCachePath({
97
95
  directory,
@@ -128,6 +126,7 @@ export async function readLeaderboardCache({
128
126
  "opencode-plugin-intelligence: Refresh failed; using stale leaderboard cache",
129
127
  error,
130
128
  );
129
+ onRefreshError?.(error);
131
130
  return cached;
132
131
  }
133
132
  }
@@ -15,9 +15,7 @@ function visit(value, predicate) {
15
15
  return result;
16
16
  }
17
17
  }
18
- }
19
-
20
- if (value && typeof value === "object") {
18
+ } else if (value && typeof value === "object") {
21
19
  for (const item of Object.values(value)) {
22
20
  const result = visit(item, predicate);
23
21
 
@@ -35,12 +35,6 @@ function scoreTag(score) {
35
35
  return `(AIQ: ${Number(score.toFixed(2)).toString()})`;
36
36
  }
37
37
 
38
- function setModelOverride(providerConfig, modelId, displayName) {
39
- providerConfig.models ||= {};
40
- providerConfig.models[modelId] ||= {};
41
- providerConfig.models[modelId].name = displayName;
42
- }
43
-
44
38
  function baseDisplayName(name) {
45
39
  return name.replace(INTELLIGENCE_TAG_PATTERN, "");
46
40
  }
@@ -162,65 +156,3 @@ export function createModelNameAnnotator(leaderboard) {
162
156
  return `${baseName} ${scoreTag(matchedModel.intelligenceIndex)}`;
163
157
  };
164
158
  }
165
-
166
- export function applyIntelligenceAnnotations(config, leaderboard) {
167
- if (!leaderboard?.models?.length) {
168
- return;
169
- }
170
-
171
- config.provider ||= {};
172
- const annotateName = createModelNameAnnotator(leaderboard);
173
-
174
- for (const providerConfig of Object.values(config.provider)) {
175
- providerConfig.models ||= {};
176
-
177
- for (const [modelId, modelConfig] of Object.entries(
178
- providerConfig.models,
179
- )) {
180
- const currentName = modelConfig.name || modelId;
181
- const name = annotateName(modelId, currentName);
182
- if (name !== currentName) setModelOverride(providerConfig, modelId, name);
183
- }
184
- }
185
- }
186
-
187
- export function buildProviderNamePatch(providers, leaderboard) {
188
- const config = { provider: {} };
189
- const candidateConfig = { provider: {} };
190
-
191
- for (const provider of providers) {
192
- if (!provider?.id || !provider?.models) {
193
- continue;
194
- }
195
-
196
- candidateConfig.provider[provider.id] = { models: {} };
197
-
198
- for (const [modelId, model] of Object.entries(provider.models)) {
199
- candidateConfig.provider[provider.id].models[modelId] = {
200
- name: model.name,
201
- };
202
- }
203
- }
204
-
205
- applyIntelligenceAnnotations(candidateConfig, leaderboard);
206
-
207
- for (const provider of providers) {
208
- if (!provider?.id || !provider?.models) {
209
- continue;
210
- }
211
-
212
- for (const [modelId, model] of Object.entries(provider.models)) {
213
- const nextName =
214
- candidateConfig.provider?.[provider.id]?.models?.[modelId]?.name;
215
-
216
- if (!nextName || nextName === model.name) {
217
- continue;
218
- }
219
-
220
- config.provider[provider.id] ||= { models: {} };
221
- config.provider[provider.id].models[modelId] = { name: nextName };
222
- }
223
- }
224
-
225
- return config;
226
- }
package/src/tui.js ADDED
@@ -0,0 +1,26 @@
1
+ import { cacheStatusRpc } from "./cache-status.js";
2
+
3
+ /** @type {import("@opencode/plugin/tui").Plugin.Definition} */
4
+ export default {
5
+ id: "opencode-plugin-intelligence",
6
+ async setup(ctx) {
7
+ try {
8
+ const message = await ctx.client.rpc(cacheStatusRpc).status(null, {
9
+ location: ctx.location,
10
+ });
11
+ if (message) {
12
+ ctx.ui.toast.show({
13
+ title: "Artificial Analysis",
14
+ message,
15
+ variant: "error",
16
+ duration: 10000,
17
+ });
18
+ }
19
+ } catch (error) {
20
+ console.warn(
21
+ "opencode-plugin-intelligence: Failed to read leaderboard cache status",
22
+ error,
23
+ );
24
+ }
25
+ },
26
+ };
package/tui.js ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./src/tui.js";