@serkanalgur/opencode-nexus 1.0.5 → 1.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.
Files changed (4) hide show
  1. package/README.md +296 -248
  2. package/dist/index.js +2294 -432
  3. package/dist/tui.js +328 -20
  4. package/package.json +1 -1
package/dist/tui.js CHANGED
@@ -107,6 +107,64 @@ var SuspenseContext = createContext();
107
107
  // node_modules/@opencode/plugin/dist/tui/solid.js
108
108
  var PluginContext = createContext();
109
109
  // src/config.ts
110
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
111
+ import { homedir } from "node:os";
112
+ import { join, dirname } from "node:path";
113
+ function stripJsonComments(jsonc) {
114
+ const result = [];
115
+ let i = 0;
116
+ const len = jsonc.length;
117
+ while (i < len) {
118
+ const ch = jsonc[i];
119
+ if (ch === '"') {
120
+ result.push(ch);
121
+ i++;
122
+ while (i < len && jsonc[i] !== '"') {
123
+ if (jsonc[i] === "\\") {
124
+ result.push(jsonc[i], jsonc[i + 1] ?? "");
125
+ i += 2;
126
+ } else {
127
+ result.push(jsonc[i]);
128
+ i++;
129
+ }
130
+ }
131
+ if (i < len) {
132
+ result.push(jsonc[i]);
133
+ i++;
134
+ }
135
+ continue;
136
+ }
137
+ if (ch === "/" && jsonc[i + 1] === "/") {
138
+ while (i < len && jsonc[i] !== `
139
+ `)
140
+ i++;
141
+ continue;
142
+ }
143
+ if (ch === "/" && jsonc[i + 1] === "*") {
144
+ i += 2;
145
+ while (i < len && !(jsonc[i] === "*" && jsonc[i + 1] === "/"))
146
+ i++;
147
+ i += 2;
148
+ continue;
149
+ }
150
+ result.push(ch);
151
+ i++;
152
+ }
153
+ return result.join("");
154
+ }
155
+ function readJsoncFile(filePath) {
156
+ try {
157
+ const raw = readFileSync(filePath, "utf-8");
158
+ const stripped = stripJsonComments(raw);
159
+ const parsed = JSON.parse(stripped);
160
+ return parsed;
161
+ } catch (err) {
162
+ if (err.code !== "ENOENT") {
163
+ console.warn(`[nexus] Failed to load config from ${filePath}: ${err.message}`);
164
+ }
165
+ return null;
166
+ }
167
+ }
110
168
  var DEFAULT_CONFIG = {
111
169
  models: {
112
170
  architect: "anthropic/claude-sonnet-4-6",
@@ -134,12 +192,18 @@ class NexusConfigManager {
134
192
  globalConfig = null;
135
193
  storageConfig = null;
136
194
  constructor() {
137
- this.loadConfigs();
138
- }
139
- loadConfigs() {
140
195
  this.projectConfig = null;
141
196
  this.globalConfig = null;
142
197
  }
198
+ loadConfigs(basePath) {
199
+ const projectPath = join(basePath, ".opencode", "nexus.jsonc");
200
+ this.projectConfig = readJsoncFile(projectPath);
201
+ const globalPath = join(homedir(), ".config", "opencode", "nexus.jsonc");
202
+ this.globalConfig = readJsoncFile(globalPath);
203
+ }
204
+ loadFromPath(basePath) {
205
+ this.loadConfigs(basePath);
206
+ }
143
207
  getConfig() {
144
208
  return {
145
209
  models: {
@@ -197,24 +261,108 @@ class NexusConfigManager {
197
261
  }
198
262
  getRoleDisplayName(role) {
199
263
  const names = {
200
- architect: "\uD83C\uDFD7️ Architect",
201
- coder: "\uD83D\uDCBB Coder",
202
- reviewer: "\uD83D\uDD0D Reviewer",
203
- tester: "\uD83E\uDDEA Tester",
204
- explorer: "\uD83D\uDD2C Explorer",
205
- documenter: "\uD83D\uDCDD Documenter"
264
+ architect: "Architect",
265
+ coder: "Coder",
266
+ reviewer: "Reviewer",
267
+ tester: "Tester",
268
+ explorer: "Explorer",
269
+ documenter: "Documenter"
206
270
  };
207
271
  return names[role] || role;
208
272
  }
273
+ getRoleEmoji(role) {
274
+ const emojis = {
275
+ architect: "\uD83C\uDFD7️",
276
+ coder: "\uD83D\uDCBB",
277
+ reviewer: "\uD83D\uDD0D",
278
+ tester: "\uD83E\uDDEA",
279
+ explorer: "\uD83D\uDD2C",
280
+ documenter: "\uD83D\uDCDD"
281
+ };
282
+ return emojis[role] || "\uD83E\uDD16";
283
+ }
209
284
  exportConfig() {
210
285
  return this.getConfig();
211
286
  }
212
287
  importConfig(config) {
213
288
  this.storageConfig = { ...config };
214
289
  }
290
+ writeJsoncFile(filePath, config) {
291
+ const dir = dirname(filePath);
292
+ mkdirSync(dir, { recursive: true });
293
+ const jsonc = [
294
+ "// Nexus Configuration — https://github.com/serkanalgur/opencode-nexus",
295
+ "// Precedence: this file > global config > TUI settings > defaults",
296
+ "",
297
+ JSON.stringify(config, null, 2)
298
+ ].join(`
299
+ `);
300
+ writeFileSync(filePath, jsonc + `
301
+ `, "utf-8");
302
+ }
303
+ saveProjectConfig(basePath) {
304
+ const projectPath = join(basePath, ".opencode", "nexus.jsonc");
305
+ const config = this.getNonDefaultConfig();
306
+ this.writeJsoncFile(projectPath, config);
307
+ }
308
+ saveGlobalConfig() {
309
+ const globalPath = join(homedir(), ".config", "opencode", "nexus.jsonc");
310
+ const config = this.getNonDefaultConfig();
311
+ this.writeJsoncFile(globalPath, config);
312
+ }
313
+ saveConfig(level, basePath) {
314
+ if (level === "project") {
315
+ this.saveProjectConfig(basePath || process.cwd());
316
+ } else {
317
+ this.saveGlobalConfig();
318
+ }
319
+ }
320
+ getNonDefaultConfig() {
321
+ const current = this.getConfig();
322
+ const result = {};
323
+ const models = {};
324
+ for (const role of this.getRoles()) {
325
+ if (current.models[role] !== DEFAULT_CONFIG.models[role]) {
326
+ models[role] = current.models[role];
327
+ }
328
+ }
329
+ if (Object.keys(models).length > 0) {
330
+ result.models = models;
331
+ }
332
+ const budget = {};
333
+ const budgetKeys = ["maxTotalCost", "maxCostPerTask", "maxCostPerAgent", "alertThreshold"];
334
+ for (const key of budgetKeys) {
335
+ if (current.budget[key] !== DEFAULT_CONFIG.budget[key]) {
336
+ budget[key] = current.budget[key];
337
+ }
338
+ }
339
+ if (Object.keys(budget).length > 0) {
340
+ result.budget = budget;
341
+ }
342
+ const selfHealing = {};
343
+ const shKeys = ["enabled", "maxRetries", "contextTransfer"];
344
+ for (const key of shKeys) {
345
+ if (current.selfHealing[key] !== DEFAULT_CONFIG.selfHealing[key]) {
346
+ selfHealing[key] = current.selfHealing[key];
347
+ }
348
+ }
349
+ if (Object.keys(selfHealing).length > 0) {
350
+ result.selfHealing = selfHealing;
351
+ }
352
+ return result;
353
+ }
215
354
  resetToDefaults() {
216
355
  this.storageConfig = null;
217
356
  }
357
+ applyPreset(name) {
358
+ const preset = PRESETS[name];
359
+ if (!preset)
360
+ throw new Error(`Unknown preset: ${name}. Available: ${Object.keys(PRESETS).join(", ")}`);
361
+ this.storageConfig = { ...this.storageConfig, ...preset.config };
362
+ }
363
+ listPresets() {
364
+ return Object.keys(PRESETS);
365
+ }
218
366
  getSummary() {
219
367
  const config = this.getConfig();
220
368
  const lines = [
@@ -242,23 +390,125 @@ class NexusConfigManager {
242
390
  `);
243
391
  }
244
392
  }
393
+ var PRESETS = {
394
+ minimal: {
395
+ name: "Minimal",
396
+ description: "Low-cost setup with free/cheap models, minimal budget",
397
+ config: {
398
+ models: {
399
+ architect: "google/gemini-2.5-flash",
400
+ coder: "google/gemini-2.5-flash",
401
+ reviewer: "google/gemini-2.5-flash",
402
+ tester: "google/gemini-2.5-flash",
403
+ explorer: "google/gemini-2.5-flash",
404
+ documenter: "google/gemini-2.5-flash"
405
+ },
406
+ budget: {
407
+ maxTotalCost: 1,
408
+ maxCostPerTask: 0.1,
409
+ maxCostPerAgent: 0.5,
410
+ alertThreshold: 0.5
411
+ },
412
+ selfHealing: {
413
+ enabled: false,
414
+ maxRetries: 1,
415
+ contextTransfer: false
416
+ }
417
+ }
418
+ },
419
+ balanced: {
420
+ name: "Balanced",
421
+ description: "Good quality models with moderate budget",
422
+ config: {
423
+ models: {
424
+ architect: "anthropic/claude-sonnet-4-6",
425
+ coder: "anthropic/claude-sonnet-4-6",
426
+ reviewer: "openai/gpt-5-mini",
427
+ tester: "anthropic/claude-haiku-4-5",
428
+ explorer: "google/gemini-2.5-flash",
429
+ documenter: "anthropic/claude-haiku-4-5"
430
+ },
431
+ budget: {
432
+ maxTotalCost: 10,
433
+ maxCostPerTask: 1,
434
+ maxCostPerAgent: 2,
435
+ alertThreshold: 0.2
436
+ },
437
+ selfHealing: {
438
+ enabled: true,
439
+ maxRetries: 3,
440
+ contextTransfer: true
441
+ }
442
+ }
443
+ },
444
+ enterprise: {
445
+ name: "Enterprise",
446
+ description: "High-quality models with generous budget and full self-healing",
447
+ config: {
448
+ models: {
449
+ architect: "anthropic/claude-sonnet-4-6",
450
+ coder: "anthropic/claude-sonnet-4-6",
451
+ reviewer: "openai/gpt-5",
452
+ tester: "anthropic/claude-sonnet-4-6",
453
+ explorer: "anthropic/claude-sonnet-4-6",
454
+ documenter: "anthropic/claude-haiku-4-5"
455
+ },
456
+ budget: {
457
+ maxTotalCost: 50,
458
+ maxCostPerTask: 5,
459
+ maxCostPerAgent: 10,
460
+ alertThreshold: 0.1
461
+ },
462
+ selfHealing: {
463
+ enabled: true,
464
+ maxRetries: 5,
465
+ contextTransfer: true
466
+ }
467
+ }
468
+ },
469
+ "cost-optimized": {
470
+ name: "Cost-Optimized",
471
+ description: "Minimize costs while maintaining reasonable quality",
472
+ config: {
473
+ models: {
474
+ architect: "anthropic/claude-haiku-4-5",
475
+ coder: "google/gemini-2.5-flash",
476
+ reviewer: "openai/gpt-5-mini",
477
+ tester: "google/gemini-2.5-flash",
478
+ explorer: "google/gemini-2.5-flash",
479
+ documenter: "google/gemini-2.5-flash"
480
+ },
481
+ budget: {
482
+ maxTotalCost: 3,
483
+ maxCostPerTask: 0.3,
484
+ maxCostPerAgent: 1,
485
+ alertThreshold: 0.3
486
+ },
487
+ selfHealing: {
488
+ enabled: true,
489
+ maxRetries: 2,
490
+ contextTransfer: false
491
+ }
492
+ }
493
+ }
494
+ };
245
495
 
246
496
  // node_modules/@opentui/core/chunk-node-70eg2nhg.js
247
497
  import { createRequire } from "node:module";
248
498
  import { fileURLToPath } from "node:url";
249
499
  import { mkdir, writeFile as writeFileNode } from "node:fs/promises";
250
- import { dirname, isAbsolute as isAbsolute2, resolve } from "node:path";
500
+ import { dirname as dirname2, isAbsolute as isAbsolute2, resolve } from "node:path";
251
501
  import { fileURLToPath as fileURLToPath3 } from "node:url";
252
502
  import { statSync } from "node:fs";
253
- import { isAbsolute, join } from "node:path";
503
+ import { isAbsolute, join as join2 } from "node:path";
254
504
  import { fileURLToPath as fileURLToPath2 } from "node:url";
255
- import { existsSync as existsSync3, writeFileSync } from "fs";
505
+ import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
256
506
  import { EventEmitter as EventEmitter4 } from "events";
257
507
  import { Buffer as Buffer3 } from "node:buffer";
258
508
  import { EventEmitter as EventEmitter2 } from "events";
259
509
  import { resolve as resolve2, isAbsolute as isAbsolute3, parse } from "path";
260
510
  import { existsSync as existsSync2 } from "fs";
261
- import { basename, join as join2 } from "node:path";
511
+ import { basename, join as join22 } from "node:path";
262
512
  import os from "os";
263
513
  import path from "path";
264
514
  import { EventEmitter as EventEmitter3 } from "events";
@@ -1117,7 +1367,7 @@ function resolveAssetRootPath(key) {
1117
1367
  if (!isAbsolute(root)) {
1118
1368
  throw new Error(`OTUI_ASSET_ROOT must be an absolute directory, got ${JSON.stringify(root)}`);
1119
1369
  }
1120
- const assetPath = join(root, key);
1370
+ const assetPath = join2(root, key);
1121
1371
  let isFile = false;
1122
1372
  try {
1123
1373
  isFile = statSync(assetPath).isFile();
@@ -1145,7 +1395,7 @@ function standardSleep(msOrDate) {
1145
1395
  async function writeFilePortable(destination, data, options) {
1146
1396
  const destinationPath = destination instanceof URL ? fileURLToPath3(destination) : destination;
1147
1397
  if (options?.createPath) {
1148
- await mkdir(dirname(destinationPath), { recursive: true });
1398
+ await mkdir(dirname2(destinationPath), { recursive: true });
1149
1399
  }
1150
1400
  const bytes = typeof data === "string" ? TEXT_ENCODER.encode(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1151
1401
  await writeFileNode(destinationPath, bytes, { mode: options?.mode });
@@ -5408,7 +5658,7 @@ function getBunfsRootPath() {
5408
5658
  return process.platform === "win32" ? "B:\\~BUN\\root" : "/$bunfs/root";
5409
5659
  }
5410
5660
  function normalizeBunfsPath(fileName) {
5411
- return join2(getBunfsRootPath(), basename(fileName));
5661
+ return join22(getBunfsRootPath(), basename(fileName));
5412
5662
  }
5413
5663
  var WORKER_UNAVAILABLE = "OpenTUI tree-sitter workers are not available for this runtime yet.";
5414
5664
  var globalWithWorker = globalThis;
@@ -11573,7 +11823,7 @@ function convertToDebugSymbols(symbols) {
11573
11823
  if (env2.OTUI_DEBUG_FFI && globalFFILogPath) {
11574
11824
  const logPath = globalFFILogPath;
11575
11825
  const writeSync = (msg) => {
11576
- writeFileSync(logPath, msg + `
11826
+ writeFileSync2(logPath, msg + `
11577
11827
  `, { flag: "a" });
11578
11828
  };
11579
11829
  Object.entries(symbols).forEach(([key, value]) => {
@@ -20191,7 +20441,7 @@ var RendererControlState2;
20191
20441
  import { EventEmitter as EventEmitter5 } from "events";
20192
20442
  import { randomBytes } from "node:crypto";
20193
20443
  import { open as openFile, readFile, rename, unlink } from "node:fs/promises";
20194
- import { basename as basename2, dirname as dirname2, join as join3 } from "node:path";
20444
+ import { basename as basename2, dirname as dirname3, join as join3 } from "node:path";
20195
20445
  import { open, stat } from "node:fs/promises";
20196
20446
  function isGraphemeCell(char) {
20197
20447
  return char >>> 30 >= 2;
@@ -22517,7 +22767,7 @@ class AudioRecorder extends EventEmitter5 {
22517
22767
  return this.fileHandle != null || this.tempPath != null;
22518
22768
  }
22519
22769
  async openTemporaryFile() {
22520
- const directory = dirname2(this.filePath);
22770
+ const directory = dirname3(this.filePath);
22521
22771
  const destinationName = basename2(this.filePath);
22522
22772
  const tempNameLength = Math.max(1, Math.min(24, destinationName.length));
22523
22773
  const singleCharacterNames = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-";
@@ -34207,6 +34457,48 @@ var tui_default = define({
34207
34457
  variant: "success"
34208
34458
  });
34209
34459
  };
34460
+ const handleDashboard = async () => {
34461
+ const config = configManager.getConfig();
34462
+ const lines = [
34463
+ "⚡ Nexus Dashboard",
34464
+ "═══════════════════════════════════",
34465
+ "",
34466
+ "\uD83E\uDD16 Agent Models:"
34467
+ ];
34468
+ for (const role of configManager.getRoles()) {
34469
+ const model = config.models[role] || "(not set)";
34470
+ const displayName = configManager.getRoleDisplayName(role);
34471
+ lines.push(` ${displayName}: ${model}`);
34472
+ }
34473
+ lines.push("");
34474
+ lines.push("\uD83D\uDCB0 Budget:");
34475
+ lines.push(` Max Total: $${config.budget.maxTotalCost}`);
34476
+ lines.push(` Max Per Task: $${config.budget.maxCostPerTask}`);
34477
+ lines.push(` Alert Threshold: ${config.budget.alertThreshold * 100}%`);
34478
+ lines.push("");
34479
+ lines.push("\uD83D\uDEE1️ Self-Healing:");
34480
+ lines.push(` Enabled: ${config.selfHealing.enabled ? "✅" : "❌"}`);
34481
+ lines.push(` Max Retries: ${config.selfHealing.maxRetries}`);
34482
+ lines.push("");
34483
+ lines.push("\uD83D\uDCA1 Commands:");
34484
+ lines.push(" /nexus status - Show config summary");
34485
+ lines.push(" /nexus config - Configure models & budget");
34486
+ lines.push(" /nexus model - Select model for role");
34487
+ lines.push(" /nexus reset - Reset to defaults");
34488
+ lines.push("");
34489
+ lines.push("\uD83D\uDD27 Tools (use in agent prompt):");
34490
+ lines.push(" nexus.status - Orchestrator status");
34491
+ lines.push(" nexus.agents - List spawned agents");
34492
+ lines.push(" nexus.costs - Cost report");
34493
+ lines.push(" nexus.spawn - Spawn a sub-agent");
34494
+ context.ui.toast.show({
34495
+ title: "Nexus Dashboard",
34496
+ message: lines.join(`
34497
+ `),
34498
+ variant: "info",
34499
+ duration: 15000
34500
+ });
34501
+ };
34210
34502
  context.ui.slot({
34211
34503
  append: "app",
34212
34504
  render: () => {
@@ -34241,6 +34533,10 @@ var tui_default = define({
34241
34533
  duration: 8000
34242
34534
  });
34243
34535
  break;
34536
+ case "dashboard":
34537
+ case "d":
34538
+ await handleDashboard();
34539
+ break;
34244
34540
  case "model":
34245
34541
  case "m":
34246
34542
  if (parts[1]) {
@@ -34277,7 +34573,7 @@ var tui_default = define({
34277
34573
  default:
34278
34574
  context.ui.toast.show({
34279
34575
  title: "Nexus",
34280
- message: "Commands: config, status, model <role>, reset",
34576
+ message: "Commands: config, status, dashboard, model <role>, reset",
34281
34577
  variant: "info"
34282
34578
  });
34283
34579
  }
@@ -34298,6 +34594,18 @@ var tui_default = define({
34298
34594
  await handleFullConfig();
34299
34595
  }
34300
34596
  },
34597
+ {
34598
+ id: "nexus.dashboard",
34599
+ title: "Nexus Dashboard",
34600
+ group: "Nexus",
34601
+ palette: true,
34602
+ slash: { name: "nexus-dashboard", aliases: ["nd"], arguments: true },
34603
+ enabled: () => true,
34604
+ suggested: true,
34605
+ run: async () => {
34606
+ await handleDashboard();
34607
+ }
34608
+ },
34301
34609
  {
34302
34610
  id: "nexus.model",
34303
34611
  title: "Select Agent Model",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencode-nexus",
3
- "version": "1.0.5",
3
+ "version": "1.2.0",
4
4
  "description": "Adaptive Multi-Agent Orchestration with Cost Intelligence for OpenCode V2",
5
5
  "keywords": [
6
6
  "opencode",