opencode-dux 1.0.0 → 1.1.1

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
@@ -7,9 +7,7 @@ Agent orchestration, management, and operations plugin for OpenCode. Routes task
7
7
  1. Add to `~/.config/opencode/opencode.json` and `~/.config/opencode/tui.json`:
8
8
 
9
9
  ```json
10
- {
11
- "plugin": ["opencode-dux"]
12
- }
10
+ { "plugin": ["opencode-dux"] }
13
11
  ```
14
12
 
15
13
  2. Create `~/.config/opencode/opencode-dux.jsonc`:
@@ -17,9 +15,9 @@ Agent orchestration, management, and operations plugin for OpenCode. Routes task
17
15
  ```jsonc
18
16
  {
19
17
  "$schema": "https://raw.githubusercontent.com/bakhtiar-personal-work/opencode-dux/master/opencode-dux.schema.json",
20
- "preset": "my-preset",
18
+ "preset": "default",
21
19
  "presets": {
22
- "my-preset": {
20
+ "default": {
23
21
  "orchestrator": { "model": "opencode-go/deepseek-v4-flash" },
24
22
  "oracle": { "model": "opencode-go/deepseek-v4-flash" },
25
23
  "explorer": { "model": "opencode-go/deepseek-v4-flash" },
@@ -33,11 +31,7 @@ Agent orchestration, management, and operations plugin for OpenCode. Routes task
33
31
 
34
32
  3. Authenticate: `opencode auth login`
35
33
 
36
- Or run the automated installer:
37
-
38
- ```bash
39
- bunx opencode-dux install
40
- ```
34
+ Or run the installer: `bunx opencode-dux install`
41
35
 
42
36
  ## Agents
43
37
 
@@ -54,71 +48,49 @@ bunx opencode-dux install
54
48
 
55
49
  ## Configuration
56
50
 
57
- Config file: `~/.config/opencode/opencode-dux.jsonc` (preferred) or `.json`
58
-
59
- Configs are merged from two locations (project overrides user):
60
-
61
- 1. **User**: `~/.config/opencode/opencode-dux.jsonc`
62
- 2. **Project**: `<project>/.opencode/opencode-dux.jsonc`
63
-
64
- ### `preset`
65
-
66
- Active preset name. Selects an entry from `presets` to apply as the default agent configuration. Can also be set via the `OH_MY_OPENCODE_SLIM_PRESET` environment variable.
67
-
68
- ```jsonc
69
- { "preset": "opencode-go" }
70
- ```
71
-
72
- ### `presets`
73
-
74
- Named model configurations. Each preset maps agent names to their overrides. The active preset is selected by `preset`.
75
-
76
- ```jsonc
77
- {
78
- "preset": "fast",
79
- "presets": {
80
- "fast": {
81
- "orchestrator": { "model": "opencode-go/deepseek-v4-flash" },
82
- "oracle": { "model": "opencode-go/deepseek-v4-flash" }
83
- },
84
- "pro": {
85
- "orchestrator": { "model": "neuralwatt/moonshotai/Kimi-K2.6" },
86
- "oracle": { "model": "opencode-go/deepseek-v4-pro" }
87
- }
88
- }
89
- }
90
- ```
91
-
92
- ### `agents`
93
-
94
- Per-agent overrides that apply on top of the active preset. Accepts the same per-agent options as preset entries. Useful for agent-specific settings that should apply regardless of which preset is active.
95
-
96
- ```jsonc
97
- {
98
- "agents": {
99
- "orchestrator": {
100
- "skills": { "always-load": ["find-skills"], "wildcard": false },
101
- "mcps": { "always-load": [], "wildcard": false }
102
- }
103
- }
104
- }
105
- ```
51
+ Config file: `~/.config/opencode/opencode-dux.jsonc`
52
+
53
+ Merged from two locations (project overrides user):
54
+
55
+ | Location | Path |
56
+ | ----------- | ---------------------------------------- |
57
+ | **User** | `~/.config/opencode/opencode-dux.jsonc` |
58
+ | **Project** | `<project>/.opencode/opencode-dux.jsonc` |
59
+
60
+ ### Config options
61
+
62
+ | Field | Type | Default | Description |
63
+ | ------------------------------------ | ---------- | ------- | --------------------------------------------- |
64
+ | `preset` | `string` | — | Active preset name |
65
+ | `presets` | `object` | `{}` | Named model configurations per agent |
66
+ | `agents` | `object` | `{}` | Per-agent overrides on top of active preset |
67
+ | `fallback.enabled` | `boolean` | `true` | Enable runtime model fallback on API errors |
68
+ | `fallback.chains` | `object` | `{}` | Ordered fallback model arrays per agent |
69
+ | `sessionManager.maxSessionsPerAgent` | `number` | `2` | Max concurrent sessions per agent type (1–10) |
70
+ | `sessionManager.readContextMinLines` | `number` | `10` | Min lines threshold for read context tool |
71
+ | `sessionManager.readContextMaxFiles` | `number` | `8` | Max files per read context batch |
72
+ | `todoContinuation.maxContinuations` | `number` | `5` | Max consecutive auto-continuations (1–50) |
73
+ | `todoContinuation.autoEnable` | `boolean` | `false` | Auto-enable when enough todos exist |
74
+ | `contextPressure.enabled` | `boolean` | `true` | Warn when context usage is high |
75
+ | `contextPressure.warnThresholdPct` | `number` | `75` | Trigger at this context usage % (1–99) |
76
+ | `websearch.provider` | `string` | `"exa"` | `"exa"` or `"tavily"` |
77
+ | `setDefaultAgent` | `boolean` | `true` | Sets default_agent to `orchestrator` |
78
+ | `autoUpdate` | `boolean` | `true` | Auto-update when loaded via npm name |
79
+ | `disabledMcps` | `string[]` | `[]` | Disable built-in MCPs by name |
106
80
 
107
81
  ### Per-agent options
108
82
 
109
- Each agent entry (in `presets.<name>.<agent>` or `agents.<agent>`) accepts:
110
-
111
- | Field | Type | Description |
112
- | ------------- | ---------------------- | ----------------------------------------------------------------------- |
113
- | `model` | `string` or `array` | Model ID (`provider/model`) or array of models for runtime fallback |
114
- | `temperature` | `number` (0–2) | Model temperature |
115
- | `variant` | `string` | Variant hint (e.g. `"medium"`, `"pro"`, `"flash"`) |
116
- | `options` | `object` | Provider-specific model options (e.g. `textVerbosity`, thinking budget) |
117
- | `displayName` | `string` | Custom display name for the agent |
118
- | `skills` | `object` or `string[]` | Skill access configuration |
119
- | `mcps` | `object` or `string[]` | MCP access configuration |
83
+ | Field | Type | Description |
84
+ | ------------- | ---------------------- | ------------------------------------------------- |
85
+ | `model` | `string` or `array` | Model ID (`provider/model`) or array for fallback |
86
+ | `temperature` | `number` (0–2) | Model temperature |
87
+ | `variant` | `string` | Variant hint (e.g. `"pro"`, `"flash"`) |
88
+ | `options` | `object` | Provider-specific model options |
89
+ | `displayName` | `string` | Custom agent display name |
90
+ | `skills` | `object` or `string[]` | Skill access configuration |
91
+ | `mcps` | `object` or `string[]` | MCP access configuration |
120
92
 
121
- **Model as array** — When `model` is an array, the first available model is used at startup. Runtime fallback on API errors uses the full chain:
93
+ ### Model as array
122
94
 
123
95
  ```jsonc
124
96
  {
@@ -132,319 +104,72 @@ Each agent entry (in `presets.<name>.<agent>` or `agents.<agent>`) accepts:
132
104
  }
133
105
  ```
134
106
 
135
- **Skills/MCPs syntax**:
107
+ ### Skills/MCPs syntax
136
108
 
137
109
  ```jsonc
138
110
  {
139
111
  "oracle": {
140
- "skills": {
141
- "always-load": ["simplify"], // always available
142
- "wildcard": true // allow all other skills too
143
- },
144
- "mcps": ["websearch", "context7"] // shorthand: just array of names
145
- }
146
- }
147
- ```
148
-
149
- ### `fallback`
150
-
151
- Runtime model fallback when the primary model returns an API error (rate limit, timeout, etc.).
152
-
153
- ```jsonc
154
- {
155
- "fallback": {
156
- "enabled": true,
157
- "timeoutMs": 15000,
158
- "retryDelayMs": 500,
159
- "retry_on_empty": true,
160
- "chains": {
161
- "orchestrator": [
162
- "opencode-go/deepseek-v4-pro",
163
- "opencode-go/mimo-v2.5-pro"
164
- ],
165
- "oracle": ["opencode-go/deepseek-v4-pro"]
166
- }
112
+ "skills": { "always-load": ["simplify"], "wildcard": true },
113
+ "mcps": ["websearch", "context7"]
167
114
  }
168
115
  }
169
116
  ```
170
117
 
171
- | Field | Default | Description |
172
- | ---------------- | ------- | ------------------------------------------- |
173
- | `enabled` | `true` | Enable fallback chains |
174
- | `timeoutMs` | `15000` | Per-request timeout before trigger fallback |
175
- | `retryDelayMs` | `500` | Delay between retry attempts |
176
- | `retry_on_empty` | `true` | Treat empty responses as failures |
177
- | `chains` | `{}` | Ordered fallback model arrays per agent |
118
+ ## Subscriptions / Account Commands
178
119
 
179
- ### `sessionManager`
120
+ Manage API accounts directly from the OpenCode prompt via `/subscriptions`:
180
121
 
181
- Controls how the orchestrator manages parallel subagent sessions.
122
+ - `/subscriptions list` View all accounts and their usage
123
+ - `/subscriptions add opencode-go <name> <workspace-id>` — Add OpenCode Go account
124
+ - `/subscriptions add neuralwatt <name> <api-key>` — Add Neuralwatt account
125
+ - `/subscriptions switch <name>` — Activate an account
126
+ - `/subscriptions remove <name>` — Delete an account
127
+ - `/subscriptions refresh` — Force refresh usage data
182
128
 
183
- ```jsonc
184
- {
185
- "sessionManager": {
186
- "maxSessionsPerAgent": 2,
187
- "readContextMinLines": 10,
188
- "readContextMaxFiles": 8
189
- }
190
- }
191
- ```
129
+ ### Supported providers
192
130
 
193
- | Field | Default | Description |
194
- | --------------------- | ------- | -------------------------------------------------- |
195
- | `maxSessionsPerAgent` | `2` | Max concurrent sessions per agent type (1–10) |
196
- | `readContextMinLines` | `10` | Min lines threshold for read context tool (0–1000) |
197
- | `readContextMaxFiles` | `8` | Max files per read context batch (0–50) |
131
+ | Provider | Usage tracking | Auth method |
132
+ | --------------- | ----------------------------------------------------- | -------------------------- |
133
+ | **OpenCode Go** | Dashboard scraping (rolling, weekly, monthly windows) | Workspace ID + auth cookie |
134
+ | **Neuralwatt** | REST API (credits, kWh, token usage) | API key |
198
135
 
199
- ### `todoContinuation`
200
-
201
- Auto-continue the orchestrator when todos remain incomplete.
202
-
203
- ```jsonc
204
- {
205
- "todoContinuation": {
206
- "maxContinuations": 5,
207
- "cooldownMs": 3000,
208
- "autoEnable": false,
209
- "autoEnableThreshold": 4
210
- }
211
- }
212
- ```
213
-
214
- | Field | Default | Description |
215
- | --------------------- | ------- | ------------------------------------------------------------ |
216
- | `maxContinuations` | `5` | Max consecutive auto-continuations before asking user (1–50) |
217
- | `cooldownMs` | `3000` | Delay before auto-continuing (0–30000ms) |
218
- | `autoEnable` | `false` | Automatically enable when enough todos exist |
219
- | `autoEnableThreshold` | `4` | Number of todos that triggers auto-enable (1–50) |
220
-
221
- ### `contextPressure`
222
-
223
- Warns the orchestrator when context usage is high, prompting `/compact` before the model fails.
224
-
225
- ```jsonc
226
- {
227
- "contextPressure": {
228
- "enabled": true,
229
- "warnThresholdPct": 75
230
- }
231
- }
232
- ```
233
-
234
- | Field | Default | Description |
235
- | ------------------ | ------- | -------------------------------------- |
236
- | `enabled` | `true` | Enable context pressure warnings |
237
- | `warnThresholdPct` | `75` | Trigger at this context usage % (1–99) |
238
-
239
- ### `websearch`
240
-
241
- Configure the built-in websearch MCP provider.
242
-
243
- ```jsonc
244
- {
245
- "websearch": { "provider": "exa" }
246
- }
247
- ```
248
-
249
- | Field | Default | Description |
250
- | ---------- | ------- | --------------------- |
251
- | `provider` | `"exa"` | `"exa"` or `"tavily"` |
252
-
253
- ### `setDefaultAgent`
254
-
255
- When `true` (default), sets the OpenCode `default_agent` to `"orchestrator"` on startup. Set to `false` to keep your existing default agent.
256
-
257
- ```jsonc
258
- { "setDefaultAgent": false }
259
- ```
260
-
261
- ### `autoUpdate`
262
-
263
- Enable automatic updates when the plugin is loaded via npm package name.
264
-
265
- ```jsonc
266
- { "autoUpdate": true }
267
- ```
268
-
269
- ### `scoringEngineVersion`
270
-
271
- Experimental scoring engine version selection.
272
-
273
- ```jsonc
274
- { "scoringEngineVersion": "v2" }
275
- ```
276
-
277
- ### `balanceProviderUsage`
278
-
279
- Spread requests across providers for cost/rate-limit balancing.
280
-
281
- ```jsonc
282
- { "balanceProviderUsage": true }
283
- ```
284
-
285
- ### `manualPlan`
286
-
287
- Legacy explicit 4-tier fallback plan for each agent. Superseded by `fallback.chains` — both can coexist; `fallback.chains` appends to `_modelArray`.
288
-
289
- ```jsonc
290
- {
291
- "manualPlan": {
292
- "orchestrator": {
293
- "primary": "neuralwatt/moonshotai/Kimi-K2.6",
294
- "fallback1": "opencode-go/deepseek-v4-flash",
295
- "fallback2": "opencode-go/deepseek-v4-pro",
296
- "fallback3": "opencode-go/mimo-v2.5-pro"
297
- }
298
- }
299
- }
300
- ```
301
-
302
- ## Full example config
303
-
304
- ```jsonc
305
- {
306
- "$schema": "https://raw.githubusercontent.com/bakhtiar-personal-work/opencode-dux/master/opencode-dux.schema.json",
307
- "preset": "opencode-go",
308
- "setDefaultAgent": true,
309
- "autoUpdate": true,
310
- "contextPressure": {
311
- "enabled": true,
312
- "warnThresholdPct": 75
313
- },
314
- "websearch": {
315
- "provider": "exa"
316
- },
317
- "sessionManager": {
318
- "maxSessionsPerAgent": 2,
319
- "readContextMinLines": 10,
320
- "readContextMaxFiles": 8
321
- },
322
- "todoContinuation": {
323
- "maxContinuations": 5,
324
- "cooldownMs": 3000,
325
- "autoEnable": false,
326
- "autoEnableThreshold": 4
327
- },
328
- "fallback": {
329
- "enabled": true,
330
- "timeoutMs": 15000,
331
- "retryDelayMs": 500,
332
- "retry_on_empty": true,
333
- "chains": {
334
- "orchestrator": ["opencode-go/deepseek-v4-pro"],
335
- "oracle": ["opencode-go/deepseek-v4-pro"]
336
- }
337
- },
338
- "presets": {
339
- "opencode-go": {
340
- "orchestrator": {
341
- "model": "neuralwatt/Qwen/Qwen3.5-397B-A17B-FP8",
342
- "variant": "medium",
343
- "skills": { "always-load": ["find-skills"], "wildcard": false },
344
- "mcps": { "always-load": [], "wildcard": false }
345
- },
346
- "oracle": {
347
- "model": "opencode-go/deepseek-v4-flash",
348
- "skills": { "always-load": ["simplify"], "wildcard": true },
349
- "options": { "smart": "opencode-go/deepseek-v4-pro" }
350
- },
351
- "explorer": {
352
- "model": "neuralwatt/qwen3.5-397b-fast",
353
- "skills": { "always-load": ["codemap"], "wildcard": true }
354
- },
355
- "librarian": {
356
- "model": "opencode-go/deepseek-v4-flash",
357
- "skills": { "always-load": [], "wildcard": true },
358
- "mcps": {
359
- "always-load": ["websearch", "context7", "github"],
360
- "wildcard": true
361
- }
362
- },
363
- "designer": {
364
- "model": "neuralwatt/moonshotai/Kimi-K2.6",
365
- "temperature": 0.3,
366
- "skills": { "always-load": [], "wildcard": true },
367
- "mcps": { "always-load": [], "wildcard": true }
368
- },
369
- "fixer": {
370
- "model": "opencode-go/deepseek-v4-flash",
371
- "skills": { "always-load": ["codemap"], "wildcard": true },
372
- "mcps": { "always-load": [], "wildcard": true }
373
- },
374
- "steward": {
375
- "model": "opencode-go/deepseek-v4-flash",
376
- "skills": { "always-load": [], "wildcard": false }
377
- },
378
- "interpreter": {
379
- "model": "neuralwatt/moonshotai/Kimi-K2.6",
380
- "skills": { "always-load": [], "wildcard": false }
381
- }
382
- }
383
- },
384
- "agents": {
385
- "librarian": {
386
- "mcps": { "always-load": ["websearch", "context7"], "wildcard": true }
387
- },
388
- "oracle": {
389
- "skills": { "always-load": ["simplify"], "wildcard": true }
390
- }
391
- }
392
- }
393
- ```
136
+ Usage data appears in the TUI sidebar under **API Usage**.
394
137
 
395
138
  ## Prompt overrides
396
139
 
397
- Customize agent system prompts by placing Markdown files in the prompts directory:
398
-
399
- - **Replace** the default prompt: `~/.config/opencode/opencode-dux/<agent>.md`
400
- - **Append** to the default prompt: `~/.config/opencode/opencode-dux/<agent>_append.md`
140
+ Place Markdown files in `~/.config/opencode/opencode-dux/`:
401
141
 
402
- Preset-scoped prompts are also supported place them in a subdirectory named after the preset:
403
-
404
- - `~/.config/opencode/opencode-dux/<preset>/<agent>.md`
142
+ - `<agent>.md`Replace default prompt
143
+ - `<agent>_append.md` — Append to default prompt
144
+ - `<preset>/<agent>.md` — Preset-scoped prompts
405
145
 
406
146
  ## Built-in MCPs
407
147
 
408
- Plugin provides 3 MCP servers (auto-loaded):
409
-
410
148
  | MCP | Description |
411
149
  | ----------- | ---------------------------- |
412
150
  | `websearch` | Web search (Exa or Tavily) |
413
151
  | `context7` | Library documentation lookup |
414
152
  | `grep_app` | GitHub code search |
415
153
 
416
- Disable any MCP in config:
417
-
418
- ```jsonc
419
- {
420
- "disabledMcps": ["grep_app"]
421
- }
422
- ```
154
+ Disable any: `{ "disabledMcps": ["grep_app"] }`
423
155
 
424
156
  ## Built-in Skills
425
157
 
426
- Plugin includes 2 bundled skills (auto-installed):
427
-
428
158
  - **simplify** — Code simplification and clarity improvements
429
159
  - **codemap** — Codebase mapping and structure analysis
430
160
 
431
161
  ## Skill Discovery
432
162
 
433
- Agents can recommend additional skills via `discover_skills_online` tool. When a skill is recommended:
434
-
435
- 1. Agent presents: "I recommend installing X for this task"
436
- 2. You run: `npx skills add <repo>`
437
- 3. Next session: skill appears in `<available_skills>`
163
+ Agents can recommend additional skills via `discover_skills_online`. When recommended: `npx skills add <repo>`
438
164
 
439
- ## Commands
165
+ ## Development
440
166
 
441
167
  ```bash
442
- bun run build # Build TypeScript to dist/
443
- bun run typecheck # Type checking
444
- bun test # Run tests
445
- bun run check:ci # Lint + format (CI mode)
168
+ bun run build # Build TypeScript to dist/
169
+ bun run typecheck # Type checking
170
+ bun test # Run tests
171
+ bun run check:ci # Lint + format (CI mode)
446
172
  bun run generate-schema # Regenerate JSON schema from Zod
447
- bun run verify:release # Verify release artifact
448
173
  ```
449
174
 
450
175
  ## License
package/dist/index.js CHANGED
@@ -18186,8 +18186,9 @@ var require_turndown_cjs = __commonJS((exports, module) => {
18186
18186
  });
18187
18187
 
18188
18188
  // src/index.ts
18189
- import { existsSync as existsSync10, readdirSync as readdirSync2 } from "node:fs";
18190
- import { join as join15 } from "node:path";
18189
+ import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync2 } from "node:fs";
18190
+ import { dirname as dirname7, join as join15 } from "node:path";
18191
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
18191
18192
 
18192
18193
  // src/config/constants.ts
18193
18194
  var AGENT_ALIASES = {
@@ -30931,6 +30932,18 @@ var HEALTH_CHECK = {
30931
30932
  minTools: 5,
30932
30933
  minMcps: 1
30933
30934
  };
30935
+ function readPluginVersion() {
30936
+ try {
30937
+ const modDir = dirname7(fileURLToPath2(import.meta.url));
30938
+ const rootDir = dirname7(modDir);
30939
+ const pkgPath = join15(rootDir, "package.json");
30940
+ if (existsSync10(pkgPath)) {
30941
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
30942
+ return typeof pkg.version === "string" ? pkg.version : null;
30943
+ }
30944
+ } catch {}
30945
+ return null;
30946
+ }
30934
30947
  function asNumber(value) {
30935
30948
  return typeof value === "number" && Number.isFinite(value) ? value : null;
30936
30949
  }
@@ -31251,6 +31264,27 @@ var OhMyOpenCodeLite = async (ctx) => {
31251
31264
  if (isFirstInit) {
31252
31265
  console.log(`✅ opencode-dux initialized (${Object.keys(agents).length} agents, ${toolCount} tools, ${Object.keys(builtinMcps).length} MCPs)`);
31253
31266
  didLogVerboseInit = true;
31267
+ try {
31268
+ const snap = readTuiSnapshot();
31269
+ const savedVersion = snap.pluginVersion;
31270
+ const currentVersion = readPluginVersion();
31271
+ if (savedVersion && currentVersion && savedVersion !== currentVersion) {
31272
+ appLog(ctx, "info", `Updated from v${savedVersion} to v${currentVersion}`).catch(() => {});
31273
+ ctx.client.tui.showToast({
31274
+ body: {
31275
+ title: "OpenCode Dux",
31276
+ message: `Updated to v${currentVersion}`,
31277
+ variant: "info",
31278
+ duration: 5000
31279
+ }
31280
+ }).catch(() => {});
31281
+ }
31282
+ if (currentVersion) {
31283
+ updateSnapshot((s) => {
31284
+ s.pluginVersion = currentVersion;
31285
+ });
31286
+ }
31287
+ } catch {}
31254
31288
  }
31255
31289
  } catch (err) {
31256
31290
  log("[plugin] FATAL: init failed", String(err));
@@ -50,6 +50,7 @@ export interface TuiSessionBundle {
50
50
  }
51
51
  export interface TuiSnapshot {
52
52
  version: 6;
53
+ pluginVersion?: string;
53
54
  updatedAt: number;
54
55
  sessions: Record<string, TuiSessionBundle>;
55
56
  subscriptionUsage: Record<string, SubscriptionUsageEntry>;
package/dist/tui.js CHANGED
@@ -33,6 +33,9 @@ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports,
33
33
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
34
34
 
35
35
  // src/tui.ts
36
+ import { readFileSync as readFileSync2, existsSync } from "node:fs";
37
+ import { dirname as dirname2, join as join2 } from "node:path";
38
+ import { fileURLToPath } from "node:url";
36
39
  import { createElement, insert, setProp } from "@opentui/solid";
37
40
  import { createSignal } from "solid-js";
38
41
 
@@ -900,6 +903,19 @@ function recordActiveSubscriptionForProvider(provider, name) {
900
903
  }
901
904
 
902
905
  // src/tui.ts
906
+ function getPluginVersion() {
907
+ try {
908
+ const modDir = dirname2(fileURLToPath(import.meta.url));
909
+ const rootDir = dirname2(modDir);
910
+ const pkgPath = join2(rootDir, "package.json");
911
+ if (existsSync(pkgPath)) {
912
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
913
+ return pkg.version || "0.0.0";
914
+ }
915
+ } catch {}
916
+ return "0.0.0";
917
+ }
918
+ var PLUGIN_VERSION = getPluginVersion();
903
919
  var PLUGIN_NAME = "opencode-dux";
904
920
  var BORDER = { type: "single" };
905
921
  var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -1797,6 +1813,14 @@ function renderSidebar(snapshot, theme) {
1797
1813
  paddingLeft: 0,
1798
1814
  paddingRight: 0
1799
1815
  }, [
1816
+ box({
1817
+ width: "100%",
1818
+ flexDirection: "row",
1819
+ justifyContent: "space-between"
1820
+ }, [
1821
+ text({ fg: theme.accent }, ["opencode-dux"]),
1822
+ text({ fg: theme.accent }, [`v${PLUGIN_VERSION}`])
1823
+ ]),
1800
1824
  box({
1801
1825
  width: "100%",
1802
1826
  flexDirection: "row",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-dux",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Agent orchestration, management, and operations plugin for OpenCode",
5
5
  "main": "index.ts",
6
6
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { existsSync, readdirSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
3
4
  import type { Plugin } from '@opencode-ai/plugin';
4
5
  import { createAgents, getAgentConfigs } from './agents';
5
6
  import { buildOrchestratorPrompt } from './agents/orchestrator';
@@ -107,6 +108,19 @@ const HEALTH_CHECK = {
107
108
  minMcps: 1,
108
109
  } as const;
109
110
 
111
+ function readPluginVersion(): string | null {
112
+ try {
113
+ const modDir = dirname(fileURLToPath(import.meta.url));
114
+ const rootDir = dirname(modDir);
115
+ const pkgPath = join(rootDir, 'package.json');
116
+ if (existsSync(pkgPath)) {
117
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
118
+ return typeof pkg.version === 'string' ? pkg.version : null;
119
+ }
120
+ } catch {}
121
+ return null;
122
+ }
123
+
110
124
  function asNumber(value: unknown): number | null {
111
125
  return typeof value === 'number' && Number.isFinite(value) ? value : null;
112
126
  }
@@ -659,6 +673,37 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
659
673
  if (isFirstInit) {
660
674
  console.log(`\u{2705} opencode-dux initialized (${Object.keys(agents).length} agents, ${toolCount} tools, ${Object.keys(builtinMcps).length} MCPs)`);
661
675
  didLogVerboseInit = true;
676
+
677
+ // Check if plugin was updated since last run
678
+ try {
679
+ const snap = readTuiSnapshot();
680
+ const savedVersion = snap.pluginVersion;
681
+ const currentVersion = readPluginVersion();
682
+ if (savedVersion && currentVersion && savedVersion !== currentVersion) {
683
+ appLog(
684
+ ctx,
685
+ 'info',
686
+ `Updated from v${savedVersion} to v${currentVersion}`,
687
+ ).catch(() => {});
688
+ ctx.client.tui
689
+ .showToast({
690
+ body: {
691
+ title: 'OpenCode Dux',
692
+ message: `Updated to v${currentVersion}`,
693
+ variant: 'info',
694
+ duration: 5000,
695
+ },
696
+ })
697
+ .catch(() => {});
698
+ }
699
+ if (currentVersion) {
700
+ updateSnapshot((s) => {
701
+ s.pluginVersion = currentVersion;
702
+ });
703
+ }
704
+ } catch {
705
+ // best-effort
706
+ }
662
707
  }
663
708
  } catch (err) {
664
709
  // Plugin init failed: log visibly before re-throwing so the user
package/src/tui-state.ts CHANGED
@@ -63,6 +63,7 @@ export interface TuiSessionBundle {
63
63
 
64
64
  export interface TuiSnapshot {
65
65
  version: 6;
66
+ pluginVersion?: string;
66
67
  updatedAt: number;
67
68
  sessions: Record<string, TuiSessionBundle>;
68
69
  subscriptionUsage: Record<string, SubscriptionUsageEntry>;
package/src/tui.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
1
4
  import type { TuiPluginModule } from '@opencode-ai/plugin/tui';
2
5
  import type { JSX } from '@opentui/solid';
3
6
  import { createElement, insert, setProp } from '@opentui/solid';
@@ -19,6 +22,21 @@ import {
19
22
  type TuiSnapshot,
20
23
  } from './tui-state';
21
24
 
25
+ function getPluginVersion(): string {
26
+ try {
27
+ const modDir = dirname(fileURLToPath(import.meta.url));
28
+ const rootDir = dirname(modDir);
29
+ const pkgPath = join(rootDir, 'package.json');
30
+ if (existsSync(pkgPath)) {
31
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
32
+ return pkg.version || '0.0.0';
33
+ }
34
+ } catch {}
35
+ return '0.0.0';
36
+ }
37
+
38
+ const PLUGIN_VERSION = getPluginVersion();
39
+
22
40
  const PLUGIN_NAME = 'opencode-dux';
23
41
  const BORDER = { type: 'single' };
24
42
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -1424,6 +1442,17 @@ function renderSidebar(
1424
1442
  paddingRight: 0,
1425
1443
  },
1426
1444
  [
1445
+ box(
1446
+ {
1447
+ width: '100%',
1448
+ flexDirection: 'row',
1449
+ justifyContent: 'space-between',
1450
+ },
1451
+ [
1452
+ text({ fg: theme.accent }, ['opencode-dux']),
1453
+ text({ fg: theme.accent }, [`v${PLUGIN_VERSION}`]),
1454
+ ],
1455
+ ),
1427
1456
  box(
1428
1457
  {
1429
1458
  width: '100%',