@serkanalgur/opencode-nexus 2.4.0 → 2.4.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/dist/index.js +265 -26
- package/dist/tui.js +80 -16
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8366,7 +8366,7 @@ var Event16 = { Updated: Updated12, Resolved, Definitions: inventory(Updated12,
|
|
|
8366
8366
|
// src/config.ts
|
|
8367
8367
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
8368
8368
|
import { homedir } from "node:os";
|
|
8369
|
-
import { join, dirname } from "node:path";
|
|
8369
|
+
import { join, dirname, resolve as resolve2 } from "node:path";
|
|
8370
8370
|
function stripJsonComments(jsonc) {
|
|
8371
8371
|
const result = [];
|
|
8372
8372
|
let i = 0;
|
|
@@ -8414,14 +8414,46 @@ function readJsoncFile(filePath) {
|
|
|
8414
8414
|
const raw = readFileSync(filePath, "utf-8");
|
|
8415
8415
|
const stripped = stripJsonComments(raw);
|
|
8416
8416
|
const parsed = JSON.parse(stripped);
|
|
8417
|
-
|
|
8417
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8418
|
+
console.warn(`[nexus] Ignoring config from ${filePath}: expected a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}`);
|
|
8419
|
+
return { config: null, existed: true };
|
|
8420
|
+
}
|
|
8421
|
+
return { config: parsed, existed: true };
|
|
8418
8422
|
} catch (err) {
|
|
8419
|
-
|
|
8420
|
-
|
|
8423
|
+
const code = err.code;
|
|
8424
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8425
|
+
const absent = code === "ENOENT" || code === "ENOTDIR";
|
|
8426
|
+
if (!absent) {
|
|
8427
|
+
console.warn(`[nexus] Failed to load config from ${filePath}: ${message}`);
|
|
8421
8428
|
}
|
|
8422
|
-
return null;
|
|
8429
|
+
return { config: null, existed: !absent };
|
|
8423
8430
|
}
|
|
8424
8431
|
}
|
|
8432
|
+
function nexusProjectConfigPath(basePath) {
|
|
8433
|
+
return resolve2(join(basePath, ".opencode", "nexus.jsonc"));
|
|
8434
|
+
}
|
|
8435
|
+
function nexusGlobalConfigPath() {
|
|
8436
|
+
return resolve2(join(homedir(), ".config", "opencode", "nexus.jsonc"));
|
|
8437
|
+
}
|
|
8438
|
+
function redactHome(filePath) {
|
|
8439
|
+
const home = homedir();
|
|
8440
|
+
if (filePath === home)
|
|
8441
|
+
return "~";
|
|
8442
|
+
const prefix = home.endsWith("/") ? home : `${home}/`;
|
|
8443
|
+
return filePath.startsWith(prefix) ? `~${filePath.slice(home.length)}` : filePath;
|
|
8444
|
+
}
|
|
8445
|
+
function describeFile(file) {
|
|
8446
|
+
if (file.parsed)
|
|
8447
|
+
return "loaded";
|
|
8448
|
+
if (file.existed)
|
|
8449
|
+
return "unparseable";
|
|
8450
|
+
return "absent";
|
|
8451
|
+
}
|
|
8452
|
+
function formatConfigLoadLog(info) {
|
|
8453
|
+
const models = Object.entries(info.models).map(([role, model]) => `${role}=${model}`).join(" ");
|
|
8454
|
+
const override = info.sessionOverride ? " (+session override: storage; disk edits to models are IGNORED while a preset is set — clear the preset in the TUI to hand control back to disk)" : "";
|
|
8455
|
+
return `[nexus] config loaded (#${info.loadCount} trigger=${info.trigger} at=${info.loadedAt}) ` + `project=${info.project.path} [${describeFile(info.project)}] ` + `global=${info.global.path} [${describeFile(info.global)}] ` + `models:${override} ${models}`;
|
|
8456
|
+
}
|
|
8425
8457
|
var DEFAULT_CONFIG = {
|
|
8426
8458
|
models: {
|
|
8427
8459
|
architect: "anthropic/claude-sonnet-4-6",
|
|
@@ -8448,19 +8480,51 @@ class NexusConfigManager {
|
|
|
8448
8480
|
projectConfig = null;
|
|
8449
8481
|
globalConfig = null;
|
|
8450
8482
|
storageConfig = null;
|
|
8483
|
+
loadInfo = null;
|
|
8451
8484
|
constructor() {
|
|
8452
8485
|
this.projectConfig = null;
|
|
8453
8486
|
this.globalConfig = null;
|
|
8454
8487
|
}
|
|
8455
8488
|
loadConfigs(basePath) {
|
|
8456
|
-
this.
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
const
|
|
8460
|
-
this.
|
|
8489
|
+
if (this.loadInfo === null)
|
|
8490
|
+
this.storageConfig = null;
|
|
8491
|
+
const projectPath = nexusProjectConfigPath(basePath);
|
|
8492
|
+
const project = readJsoncFile(projectPath);
|
|
8493
|
+
this.projectConfig = project.config;
|
|
8494
|
+
const globalPath = nexusGlobalConfigPath();
|
|
8495
|
+
const global = readJsoncFile(globalPath);
|
|
8496
|
+
this.globalConfig = global.config;
|
|
8497
|
+
return {
|
|
8498
|
+
project: { path: redactHome(projectPath), existed: project.existed, parsed: project.config !== null },
|
|
8499
|
+
global: { path: redactHome(globalPath), existed: global.existed, parsed: global.config !== null }
|
|
8500
|
+
};
|
|
8501
|
+
}
|
|
8502
|
+
loadFromPath(basePath, trigger = "initial") {
|
|
8503
|
+
const sources = this.loadConfigs(basePath);
|
|
8504
|
+
this.loadInfo = {
|
|
8505
|
+
project: sources.project,
|
|
8506
|
+
global: sources.global,
|
|
8507
|
+
models: this.getResolvedModels(),
|
|
8508
|
+
sessionOverride: this.hasSessionOverride(),
|
|
8509
|
+
loadedAt: new Date().toISOString(),
|
|
8510
|
+
loadCount: (this.loadInfo?.loadCount ?? 0) + 1,
|
|
8511
|
+
trigger
|
|
8512
|
+
};
|
|
8513
|
+
console.log(formatConfigLoadLog(this.loadInfo));
|
|
8461
8514
|
}
|
|
8462
|
-
|
|
8463
|
-
this.
|
|
8515
|
+
getLoadInfo() {
|
|
8516
|
+
return this.loadInfo;
|
|
8517
|
+
}
|
|
8518
|
+
getResolvedModels() {
|
|
8519
|
+
const models = {};
|
|
8520
|
+
for (const [role, model] of Object.entries(this.getConfig().models)) {
|
|
8521
|
+
if (model)
|
|
8522
|
+
models[role] = model;
|
|
8523
|
+
}
|
|
8524
|
+
return models;
|
|
8525
|
+
}
|
|
8526
|
+
hasSessionOverride() {
|
|
8527
|
+
return this.storageConfig !== null;
|
|
8464
8528
|
}
|
|
8465
8529
|
getConfig() {
|
|
8466
8530
|
return {
|
|
@@ -8563,17 +8627,17 @@ class NexusConfigManager {
|
|
|
8563
8627
|
`, "utf-8");
|
|
8564
8628
|
}
|
|
8565
8629
|
saveProjectConfig(basePath) {
|
|
8566
|
-
const projectPath =
|
|
8630
|
+
const projectPath = nexusProjectConfigPath(basePath);
|
|
8567
8631
|
const config = this.getSaveableConfig();
|
|
8568
8632
|
this.writeJsoncFile(projectPath, config);
|
|
8569
8633
|
}
|
|
8570
8634
|
saveGlobalConfig() {
|
|
8571
|
-
const globalPath =
|
|
8635
|
+
const globalPath = nexusGlobalConfigPath();
|
|
8572
8636
|
const config = this.getSaveableConfig();
|
|
8573
8637
|
this.writeJsoncFile(globalPath, config);
|
|
8574
8638
|
}
|
|
8575
8639
|
initProjectConfig(basePath) {
|
|
8576
|
-
const projectPath =
|
|
8640
|
+
const projectPath = nexusProjectConfigPath(basePath);
|
|
8577
8641
|
try {
|
|
8578
8642
|
readFileSync(projectPath, "utf-8");
|
|
8579
8643
|
return;
|
|
@@ -8581,7 +8645,7 @@ class NexusConfigManager {
|
|
|
8581
8645
|
this.writeJsoncFile(projectPath, { ...DEFAULT_CONFIG });
|
|
8582
8646
|
}
|
|
8583
8647
|
initGlobalConfig() {
|
|
8584
|
-
const globalPath =
|
|
8648
|
+
const globalPath = nexusGlobalConfigPath();
|
|
8585
8649
|
try {
|
|
8586
8650
|
readFileSync(globalPath, "utf-8");
|
|
8587
8651
|
return;
|
|
@@ -10362,6 +10426,21 @@ class NexusOrchestrator {
|
|
|
10362
10426
|
this.onStateChange?.();
|
|
10363
10427
|
}, 100);
|
|
10364
10428
|
}
|
|
10429
|
+
reloadConfigFromDisk(trigger = "event") {
|
|
10430
|
+
const projectDir = this.ctx?.location.directory;
|
|
10431
|
+
if (projectDir) {
|
|
10432
|
+
this.configManager.loadFromPath(projectDir, trigger);
|
|
10433
|
+
}
|
|
10434
|
+
const info = this.configManager.getLoadInfo();
|
|
10435
|
+
if (info) {
|
|
10436
|
+
this.notifyStateChange();
|
|
10437
|
+
this.emit("config:reloaded", info);
|
|
10438
|
+
}
|
|
10439
|
+
return info;
|
|
10440
|
+
}
|
|
10441
|
+
getConfigInfo() {
|
|
10442
|
+
return this.configManager.getLoadInfo();
|
|
10443
|
+
}
|
|
10365
10444
|
async execute(request) {
|
|
10366
10445
|
if (this.running) {
|
|
10367
10446
|
throw new Error("Orchestrator is already running");
|
|
@@ -10394,6 +10473,7 @@ class NexusOrchestrator {
|
|
|
10394
10473
|
agentsUsed: this.agents.size
|
|
10395
10474
|
};
|
|
10396
10475
|
} catch (error) {
|
|
10476
|
+
console.error("[nexus] DAG execution failed:", error);
|
|
10397
10477
|
return {
|
|
10398
10478
|
success: false,
|
|
10399
10479
|
tasks: [],
|
|
@@ -10489,7 +10569,21 @@ class NexusOrchestrator {
|
|
|
10489
10569
|
const spawnPromises = [];
|
|
10490
10570
|
for (const node of readyNodes) {
|
|
10491
10571
|
if (this.agents.size < this.config.maxConcurrency) {
|
|
10492
|
-
spawnPromises.push(this.spawnAndExecute(node))
|
|
10572
|
+
spawnPromises.push(this.spawnAndExecute(node).catch((error) => {
|
|
10573
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
10574
|
+
node.status = "failed";
|
|
10575
|
+
node.task.status = "failed";
|
|
10576
|
+
node.result = { success: false, error: err.message, duration: 0, tokensUsed: 0, cost: 0 };
|
|
10577
|
+
this.dag.markFailed(node.id, err);
|
|
10578
|
+
this.notifyStateChange();
|
|
10579
|
+
this.emit("task:failed", {
|
|
10580
|
+
taskId: node.id,
|
|
10581
|
+
taskName: node.task.name,
|
|
10582
|
+
role: node.task.requiredRole,
|
|
10583
|
+
error: err.message,
|
|
10584
|
+
duration: 0
|
|
10585
|
+
});
|
|
10586
|
+
}));
|
|
10493
10587
|
}
|
|
10494
10588
|
}
|
|
10495
10589
|
await Promise.all(spawnPromises);
|
|
@@ -10499,10 +10593,15 @@ class NexusOrchestrator {
|
|
|
10499
10593
|
async spawnAndExecute(node, transferContext) {
|
|
10500
10594
|
const complexity = this.analyzeComplexity(node.task);
|
|
10501
10595
|
const model = this.selectModel(node.task.requiredRole, complexity);
|
|
10596
|
+
if (!model.provider || !model.model) {
|
|
10597
|
+
const missing = !model.provider && !model.model ? "provider and model" : !model.provider ? "provider" : "model";
|
|
10598
|
+
throw new Error(`Model selection for role "${node.task.requiredRole}" is missing its ${missing}: ` + `got { provider: ${JSON.stringify(model.provider)}, model: ${JSON.stringify(model.model)} }. ` + `Cannot build a "providerID/modelID" reference.`);
|
|
10599
|
+
}
|
|
10600
|
+
const qualifiedModel = `${model.provider}/${model.model}`;
|
|
10502
10601
|
const agent = await this.spawnAgent({
|
|
10503
10602
|
role: node.task.requiredRole,
|
|
10504
10603
|
task: node.task,
|
|
10505
|
-
model:
|
|
10604
|
+
model: qualifiedModel
|
|
10506
10605
|
});
|
|
10507
10606
|
node.spawnedAgent = agent;
|
|
10508
10607
|
node.status = "running";
|
|
@@ -10829,7 +10928,8 @@ Please continue from where the previous agent left off.`;
|
|
|
10829
10928
|
if (match) {
|
|
10830
10929
|
modelConfig = match;
|
|
10831
10930
|
} else {
|
|
10832
|
-
|
|
10931
|
+
const source = config.model ? "requested" : "configured for role";
|
|
10932
|
+
throw new Error(`Invalid model "${modelConfig}" (${source} "${config.role}"). Use "providerID/modelID" format (e.g. "opencode-go/mimo-v2.5")`);
|
|
10833
10933
|
}
|
|
10834
10934
|
}
|
|
10835
10935
|
const slashIndex = modelConfig.indexOf("/");
|
|
@@ -11138,8 +11238,20 @@ Please continue from where the previous agent left off.`;
|
|
|
11138
11238
|
}
|
|
11139
11239
|
getStatus(detailed) {
|
|
11140
11240
|
const state = this.getState();
|
|
11241
|
+
const loadInfo = this.configManager.getLoadInfo();
|
|
11242
|
+
const config = {
|
|
11243
|
+
loaded: loadInfo !== null,
|
|
11244
|
+
loadedAt: loadInfo?.loadedAt ?? null,
|
|
11245
|
+
loadCount: loadInfo?.loadCount ?? 0,
|
|
11246
|
+
sessionOverride: this.configManager.hasSessionOverride(),
|
|
11247
|
+
diskModelsIgnored: this.configManager.hasSessionOverride(),
|
|
11248
|
+
trigger: loadInfo?.trigger ?? null,
|
|
11249
|
+
project: loadInfo?.project ?? null,
|
|
11250
|
+
global: loadInfo?.global ?? null,
|
|
11251
|
+
models: this.configManager.getResolvedModels()
|
|
11252
|
+
};
|
|
11141
11253
|
if (detailed)
|
|
11142
|
-
return JSON.stringify({ ...state, budgetExceeded: this.budgetExceeded }, null, 2);
|
|
11254
|
+
return JSON.stringify({ ...state, budgetExceeded: this.budgetExceeded, config }, null, 2);
|
|
11143
11255
|
return JSON.stringify({
|
|
11144
11256
|
running: state.running,
|
|
11145
11257
|
paused: state.paused,
|
|
@@ -11147,7 +11259,8 @@ Please continue from where the previous agent left off.`;
|
|
|
11147
11259
|
agents: state.agents.length,
|
|
11148
11260
|
tasks: state.tasks.length,
|
|
11149
11261
|
totalCost: state.totalSpent,
|
|
11150
|
-
budgetRemaining: state.budgetRemaining
|
|
11262
|
+
budgetRemaining: state.budgetRemaining,
|
|
11263
|
+
config
|
|
11151
11264
|
}, null, 2);
|
|
11152
11265
|
}
|
|
11153
11266
|
listAgents(filter) {
|
|
@@ -11557,9 +11670,126 @@ class AstGrep {
|
|
|
11557
11670
|
}
|
|
11558
11671
|
|
|
11559
11672
|
// src/index.ts
|
|
11560
|
-
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync3 } from "node:fs";
|
|
11561
|
-
import { join as join6 } from "node:path";
|
|
11673
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync3, statSync } from "node:fs";
|
|
11674
|
+
import { join as join6, resolve as resolve3, basename, dirname as dirname4 } from "node:path";
|
|
11562
11675
|
import { homedir as homedir2 } from "node:os";
|
|
11676
|
+
var CONFIG_RELOAD_DEBOUNCE_MS = 150;
|
|
11677
|
+
var CONFIG_POLL_INTERVAL_MS = 2000;
|
|
11678
|
+
function isNexusConfigFile(file, eventDirectory, projectDirectory, projectPath, globalPath) {
|
|
11679
|
+
const resolved = resolve3(file);
|
|
11680
|
+
if (resolved === projectPath || resolved === globalPath)
|
|
11681
|
+
return true;
|
|
11682
|
+
if (eventDirectory !== undefined && resolve3(eventDirectory) !== projectDirectory)
|
|
11683
|
+
return false;
|
|
11684
|
+
return basename(resolved) === "nexus.jsonc" && basename(dirname4(resolved)) === ".opencode";
|
|
11685
|
+
}
|
|
11686
|
+
var watchedContexts = new WeakSet;
|
|
11687
|
+
var MAX_RELOAD_WAIT_MS = 1000;
|
|
11688
|
+
var MIN_INTERVAL_MS = 25;
|
|
11689
|
+
var clampInterval = (ms, fallback) => Number.isFinite(ms) && ms >= MIN_INTERVAL_MS ? ms : fallback;
|
|
11690
|
+
function watchConfigFiles(ctx, orchestrator, debounceMs = CONFIG_RELOAD_DEBOUNCE_MS, pollMs = CONFIG_POLL_INTERVAL_MS) {
|
|
11691
|
+
if (watchedContexts.has(ctx))
|
|
11692
|
+
return () => {};
|
|
11693
|
+
watchedContexts.add(ctx);
|
|
11694
|
+
const debounce = clampInterval(debounceMs, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
11695
|
+
const pollEvery = clampInterval(pollMs, CONFIG_POLL_INTERVAL_MS);
|
|
11696
|
+
const projectDirectory = resolve3(ctx.location.directory);
|
|
11697
|
+
const projectPath = nexusProjectConfigPath(ctx.location.directory);
|
|
11698
|
+
const globalPath = nexusGlobalConfigPath();
|
|
11699
|
+
const watchedPaths = [projectPath, globalPath];
|
|
11700
|
+
const controller = new AbortController;
|
|
11701
|
+
let debounceTimer = null;
|
|
11702
|
+
let firstScheduledAt = 0;
|
|
11703
|
+
const clearPendingReload = () => {
|
|
11704
|
+
if (debounceTimer === null)
|
|
11705
|
+
return;
|
|
11706
|
+
clearTimeout(debounceTimer);
|
|
11707
|
+
debounceTimer = null;
|
|
11708
|
+
};
|
|
11709
|
+
const stampOf = (filePath) => {
|
|
11710
|
+
try {
|
|
11711
|
+
const stats = statSync(filePath);
|
|
11712
|
+
if (!stats.isFile())
|
|
11713
|
+
return "absent";
|
|
11714
|
+
return `${stats.mtimeMs}:${stats.size}`;
|
|
11715
|
+
} catch {
|
|
11716
|
+
return "absent";
|
|
11717
|
+
}
|
|
11718
|
+
};
|
|
11719
|
+
const lastSeen = new Map(watchedPaths.map((p) => [p, stampOf(p)]));
|
|
11720
|
+
const runReload = (trigger) => {
|
|
11721
|
+
for (const filePath of watchedPaths)
|
|
11722
|
+
lastSeen.set(filePath, stampOf(filePath));
|
|
11723
|
+
try {
|
|
11724
|
+
orchestrator.reloadConfigFromDisk(trigger);
|
|
11725
|
+
} catch (err) {
|
|
11726
|
+
console.warn(`[nexus] config reload failed (${trigger}): ${String(err)}`);
|
|
11727
|
+
}
|
|
11728
|
+
};
|
|
11729
|
+
const scheduleReload = (trigger) => {
|
|
11730
|
+
const pending = debounceTimer !== null;
|
|
11731
|
+
const waited = pending ? Date.now() - firstScheduledAt : 0;
|
|
11732
|
+
clearPendingReload();
|
|
11733
|
+
if (pending && waited >= MAX_RELOAD_WAIT_MS) {
|
|
11734
|
+
runReload(trigger);
|
|
11735
|
+
return;
|
|
11736
|
+
}
|
|
11737
|
+
if (!pending)
|
|
11738
|
+
firstScheduledAt = Date.now();
|
|
11739
|
+
debounceTimer = setTimeout(() => {
|
|
11740
|
+
debounceTimer = null;
|
|
11741
|
+
runReload(trigger);
|
|
11742
|
+
}, debounce);
|
|
11743
|
+
};
|
|
11744
|
+
const poll = () => {
|
|
11745
|
+
let changed = false;
|
|
11746
|
+
for (const filePath of watchedPaths) {
|
|
11747
|
+
const current = stampOf(filePath);
|
|
11748
|
+
if (lastSeen.get(filePath) === current)
|
|
11749
|
+
continue;
|
|
11750
|
+
lastSeen.set(filePath, current);
|
|
11751
|
+
changed = true;
|
|
11752
|
+
}
|
|
11753
|
+
if (changed)
|
|
11754
|
+
scheduleReload("poll");
|
|
11755
|
+
};
|
|
11756
|
+
const pollTimer = setInterval(poll, pollEvery);
|
|
11757
|
+
pollTimer.unref();
|
|
11758
|
+
const eventDomain = ctx.event;
|
|
11759
|
+
if (typeof eventDomain?.subscribe !== "function") {
|
|
11760
|
+
console.warn("[nexus] no config event stream on this context; polling is the only config reload trigger");
|
|
11761
|
+
return () => {
|
|
11762
|
+
clearInterval(pollTimer);
|
|
11763
|
+
clearPendingReload();
|
|
11764
|
+
controller.abort();
|
|
11765
|
+
};
|
|
11766
|
+
}
|
|
11767
|
+
const consume = async () => {
|
|
11768
|
+
try {
|
|
11769
|
+
for await (const event of eventDomain.subscribe({ signal: controller.signal })) {
|
|
11770
|
+
if (event.type !== "filesystem.changed")
|
|
11771
|
+
continue;
|
|
11772
|
+
const file = event.data?.file;
|
|
11773
|
+
if (typeof file !== "string")
|
|
11774
|
+
continue;
|
|
11775
|
+
if (!isNexusConfigFile(file, event.location?.directory, projectDirectory, projectPath, globalPath))
|
|
11776
|
+
continue;
|
|
11777
|
+
scheduleReload("event");
|
|
11778
|
+
}
|
|
11779
|
+
if (!controller.signal.aborted) {
|
|
11780
|
+
console.warn("[nexus] config event stream ended; polling remains the only config reload trigger");
|
|
11781
|
+
}
|
|
11782
|
+
} catch (err) {
|
|
11783
|
+
console.warn(`[nexus] config event stream failed; polling remains the only config reload trigger: ${String(err)}`);
|
|
11784
|
+
}
|
|
11785
|
+
};
|
|
11786
|
+
consume();
|
|
11787
|
+
return () => {
|
|
11788
|
+
clearInterval(pollTimer);
|
|
11789
|
+
clearPendingReload();
|
|
11790
|
+
controller.abort();
|
|
11791
|
+
};
|
|
11792
|
+
}
|
|
11563
11793
|
var NEXUS_AGENT_CONTENT = `---
|
|
11564
11794
|
description: Nexus multi-agent orchestrator — decomposes tasks and delegates to specialized sub-agents
|
|
11565
11795
|
mode: primary
|
|
@@ -12089,7 +12319,7 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12089
12319
|
});
|
|
12090
12320
|
editor.add({
|
|
12091
12321
|
name: "status",
|
|
12092
|
-
description: "Get orchestrator status and
|
|
12322
|
+
description: "Get orchestrator status, metrics, and the config files currently in effect (paths consulted, which existed, resolved role -> model map)",
|
|
12093
12323
|
input: {
|
|
12094
12324
|
type: "object",
|
|
12095
12325
|
properties: {
|
|
@@ -12228,7 +12458,10 @@ You are a technical writer who creates documentation that developers actually wa
|
|
|
12228
12458
|
const { name } = input;
|
|
12229
12459
|
try {
|
|
12230
12460
|
orchestrator.configManager.applyPreset(name);
|
|
12231
|
-
return {
|
|
12461
|
+
return {
|
|
12462
|
+
content: `Applied preset: ${PRESETS[name]?.name || name}
|
|
12463
|
+
` + `⚠️ This preset now overrides the \`models\` section of nexus.jsonc. ` + `Edits to models in the config file will NOT take effect until the preset is cleared. ` + `Clear it from the TUI (config manager) to hand control back to disk. ` + `Budget and self-healing values from the file still apply.`
|
|
12464
|
+
};
|
|
12232
12465
|
} catch (error) {
|
|
12233
12466
|
return { content: `Error: ${error.message}` };
|
|
12234
12467
|
}
|
|
@@ -13201,19 +13434,24 @@ ${lines.join(`
|
|
|
13201
13434
|
event.metadata = { ...event.metadata, nexusResult: result };
|
|
13202
13435
|
}
|
|
13203
13436
|
});
|
|
13437
|
+
const stopConfigWatch = watchConfigFiles(ctx, orchestrator);
|
|
13204
13438
|
return () => {
|
|
13439
|
+
stopConfigWatch();
|
|
13205
13440
|
orchestrator.shutdown();
|
|
13206
13441
|
};
|
|
13207
13442
|
}
|
|
13208
13443
|
});
|
|
13209
13444
|
export {
|
|
13210
13445
|
AstGrep,
|
|
13446
|
+
CONFIG_POLL_INTERVAL_MS,
|
|
13447
|
+
CONFIG_RELOAD_DEBOUNCE_MS,
|
|
13211
13448
|
CostForecaster,
|
|
13212
13449
|
CustomRoleManager,
|
|
13213
13450
|
DEFAULT_CONFIG,
|
|
13214
13451
|
GoalManager,
|
|
13215
13452
|
HealthMonitor,
|
|
13216
13453
|
LearningModule,
|
|
13454
|
+
MAX_RELOAD_WAIT_MS,
|
|
13217
13455
|
MessageRouter,
|
|
13218
13456
|
MessageStore,
|
|
13219
13457
|
ModuleRegistry,
|
|
@@ -13232,5 +13470,6 @@ export {
|
|
|
13232
13470
|
detectCycles,
|
|
13233
13471
|
getTemplate,
|
|
13234
13472
|
instantiateTemplate,
|
|
13235
|
-
listTemplates
|
|
13473
|
+
listTemplates,
|
|
13474
|
+
watchConfigFiles
|
|
13236
13475
|
};
|
package/dist/tui.js
CHANGED
|
@@ -9,7 +9,7 @@ var PluginContext = createContext();
|
|
|
9
9
|
// src/config.ts
|
|
10
10
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
11
11
|
import { homedir } from "os";
|
|
12
|
-
import { join, dirname } from "path";
|
|
12
|
+
import { join, dirname, resolve } from "path";
|
|
13
13
|
function stripJsonComments(jsonc) {
|
|
14
14
|
const result = [];
|
|
15
15
|
let i = 0;
|
|
@@ -57,14 +57,46 @@ function readJsoncFile(filePath) {
|
|
|
57
57
|
const raw = readFileSync(filePath, "utf-8");
|
|
58
58
|
const stripped = stripJsonComments(raw);
|
|
59
59
|
const parsed = JSON.parse(stripped);
|
|
60
|
-
|
|
60
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
61
|
+
console.warn(`[nexus] Ignoring config from ${filePath}: expected a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}`);
|
|
62
|
+
return { config: null, existed: true };
|
|
63
|
+
}
|
|
64
|
+
return { config: parsed, existed: true };
|
|
61
65
|
} catch (err) {
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
const code = err.code;
|
|
67
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
68
|
+
const absent = code === "ENOENT" || code === "ENOTDIR";
|
|
69
|
+
if (!absent) {
|
|
70
|
+
console.warn(`[nexus] Failed to load config from ${filePath}: ${message}`);
|
|
64
71
|
}
|
|
65
|
-
return null;
|
|
72
|
+
return { config: null, existed: !absent };
|
|
66
73
|
}
|
|
67
74
|
}
|
|
75
|
+
function nexusProjectConfigPath(basePath) {
|
|
76
|
+
return resolve(join(basePath, ".opencode", "nexus.jsonc"));
|
|
77
|
+
}
|
|
78
|
+
function nexusGlobalConfigPath() {
|
|
79
|
+
return resolve(join(homedir(), ".config", "opencode", "nexus.jsonc"));
|
|
80
|
+
}
|
|
81
|
+
function redactHome(filePath) {
|
|
82
|
+
const home = homedir();
|
|
83
|
+
if (filePath === home)
|
|
84
|
+
return "~";
|
|
85
|
+
const prefix = home.endsWith("/") ? home : `${home}/`;
|
|
86
|
+
return filePath.startsWith(prefix) ? `~${filePath.slice(home.length)}` : filePath;
|
|
87
|
+
}
|
|
88
|
+
function describeFile(file) {
|
|
89
|
+
if (file.parsed)
|
|
90
|
+
return "loaded";
|
|
91
|
+
if (file.existed)
|
|
92
|
+
return "unparseable";
|
|
93
|
+
return "absent";
|
|
94
|
+
}
|
|
95
|
+
function formatConfigLoadLog(info) {
|
|
96
|
+
const models = Object.entries(info.models).map(([role, model]) => `${role}=${model}`).join(" ");
|
|
97
|
+
const override = info.sessionOverride ? " (+session override: storage; disk edits to models are IGNORED while a preset is set \u2014 clear the preset in the TUI to hand control back to disk)" : "";
|
|
98
|
+
return `[nexus] config loaded (#${info.loadCount} trigger=${info.trigger} at=${info.loadedAt}) ` + `project=${info.project.path} [${describeFile(info.project)}] ` + `global=${info.global.path} [${describeFile(info.global)}] ` + `models:${override} ${models}`;
|
|
99
|
+
}
|
|
68
100
|
var DEFAULT_CONFIG = {
|
|
69
101
|
models: {
|
|
70
102
|
architect: "anthropic/claude-sonnet-4-6",
|
|
@@ -91,19 +123,51 @@ class NexusConfigManager {
|
|
|
91
123
|
projectConfig = null;
|
|
92
124
|
globalConfig = null;
|
|
93
125
|
storageConfig = null;
|
|
126
|
+
loadInfo = null;
|
|
94
127
|
constructor() {
|
|
95
128
|
this.projectConfig = null;
|
|
96
129
|
this.globalConfig = null;
|
|
97
130
|
}
|
|
98
131
|
loadConfigs(basePath) {
|
|
99
|
-
this.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
this.
|
|
132
|
+
if (this.loadInfo === null)
|
|
133
|
+
this.storageConfig = null;
|
|
134
|
+
const projectPath = nexusProjectConfigPath(basePath);
|
|
135
|
+
const project = readJsoncFile(projectPath);
|
|
136
|
+
this.projectConfig = project.config;
|
|
137
|
+
const globalPath = nexusGlobalConfigPath();
|
|
138
|
+
const global = readJsoncFile(globalPath);
|
|
139
|
+
this.globalConfig = global.config;
|
|
140
|
+
return {
|
|
141
|
+
project: { path: redactHome(projectPath), existed: project.existed, parsed: project.config !== null },
|
|
142
|
+
global: { path: redactHome(globalPath), existed: global.existed, parsed: global.config !== null }
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
loadFromPath(basePath, trigger = "initial") {
|
|
146
|
+
const sources = this.loadConfigs(basePath);
|
|
147
|
+
this.loadInfo = {
|
|
148
|
+
project: sources.project,
|
|
149
|
+
global: sources.global,
|
|
150
|
+
models: this.getResolvedModels(),
|
|
151
|
+
sessionOverride: this.hasSessionOverride(),
|
|
152
|
+
loadedAt: new Date().toISOString(),
|
|
153
|
+
loadCount: (this.loadInfo?.loadCount ?? 0) + 1,
|
|
154
|
+
trigger
|
|
155
|
+
};
|
|
156
|
+
console.log(formatConfigLoadLog(this.loadInfo));
|
|
157
|
+
}
|
|
158
|
+
getLoadInfo() {
|
|
159
|
+
return this.loadInfo;
|
|
160
|
+
}
|
|
161
|
+
getResolvedModels() {
|
|
162
|
+
const models = {};
|
|
163
|
+
for (const [role, model] of Object.entries(this.getConfig().models)) {
|
|
164
|
+
if (model)
|
|
165
|
+
models[role] = model;
|
|
166
|
+
}
|
|
167
|
+
return models;
|
|
104
168
|
}
|
|
105
|
-
|
|
106
|
-
this.
|
|
169
|
+
hasSessionOverride() {
|
|
170
|
+
return this.storageConfig !== null;
|
|
107
171
|
}
|
|
108
172
|
getConfig() {
|
|
109
173
|
return {
|
|
@@ -206,17 +270,17 @@ class NexusConfigManager {
|
|
|
206
270
|
`, "utf-8");
|
|
207
271
|
}
|
|
208
272
|
saveProjectConfig(basePath) {
|
|
209
|
-
const projectPath =
|
|
273
|
+
const projectPath = nexusProjectConfigPath(basePath);
|
|
210
274
|
const config = this.getSaveableConfig();
|
|
211
275
|
this.writeJsoncFile(projectPath, config);
|
|
212
276
|
}
|
|
213
277
|
saveGlobalConfig() {
|
|
214
|
-
const globalPath =
|
|
278
|
+
const globalPath = nexusGlobalConfigPath();
|
|
215
279
|
const config = this.getSaveableConfig();
|
|
216
280
|
this.writeJsoncFile(globalPath, config);
|
|
217
281
|
}
|
|
218
282
|
initProjectConfig(basePath) {
|
|
219
|
-
const projectPath =
|
|
283
|
+
const projectPath = nexusProjectConfigPath(basePath);
|
|
220
284
|
try {
|
|
221
285
|
readFileSync(projectPath, "utf-8");
|
|
222
286
|
return;
|
|
@@ -224,7 +288,7 @@ class NexusConfigManager {
|
|
|
224
288
|
this.writeJsoncFile(projectPath, { ...DEFAULT_CONFIG });
|
|
225
289
|
}
|
|
226
290
|
initGlobalConfig() {
|
|
227
|
-
const globalPath =
|
|
291
|
+
const globalPath = nexusGlobalConfigPath();
|
|
228
292
|
try {
|
|
229
293
|
readFileSync(globalPath, "utf-8");
|
|
230
294
|
return;
|