@serkanalgur/opencode-nexus 1.0.3 → 1.0.7

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 (3) hide show
  1. package/dist/index.js +269 -170
  2. package/dist/tui.js +259 -209
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8514,11 +8514,53 @@ class NexusOrchestrator {
8514
8514
  memory = new Map;
8515
8515
  eventHandlers = new Map;
8516
8516
  configManager;
8517
+ ctx = null;
8518
+ onStateChange = null;
8517
8519
  constructor(config) {
8518
8520
  this.config = this.mergeConfig(config);
8519
8521
  this.budget = this.config.budget;
8520
8522
  this.configManager = new NexusConfigManager;
8521
8523
  }
8524
+ initialize(ctx, onStateChange) {
8525
+ this.ctx = ctx;
8526
+ this.onStateChange = onStateChange ?? null;
8527
+ }
8528
+ getState() {
8529
+ const agents = Array.from(this.agents.values()).map((a) => ({
8530
+ id: a.id,
8531
+ name: a.name,
8532
+ role: a.role,
8533
+ status: a.status,
8534
+ model: `${a.model.provider}/${a.model.model}`,
8535
+ sessionID: a.sessionID,
8536
+ spawnedAt: a.spawnedAt.toISOString(),
8537
+ tasksCompleted: a.metrics.tasksCompleted,
8538
+ tasksFailed: a.metrics.tasksFailed,
8539
+ totalCost: a.metrics.totalCost
8540
+ }));
8541
+ const tasks = Array.from(this.tasks.values()).map((t) => ({
8542
+ id: t.id,
8543
+ name: t.name,
8544
+ role: t.requiredRole,
8545
+ status: t.status,
8546
+ assignedAgent: t.assignedAgent,
8547
+ result: t.result ? {
8548
+ success: t.result.success,
8549
+ output: t.result.output?.slice(0, 500),
8550
+ error: t.result.error,
8551
+ duration: t.result.duration
8552
+ } : undefined
8553
+ }));
8554
+ return {
8555
+ running: this.running,
8556
+ paused: this.paused,
8557
+ agents,
8558
+ tasks,
8559
+ totalSpent: this.totalSpent,
8560
+ budgetRemaining: this.budget.maxTotalCost - this.totalSpent,
8561
+ lastUpdated: new Date().toISOString()
8562
+ };
8563
+ }
8522
8564
  mergeConfig(partial) {
8523
8565
  const defaults = {
8524
8566
  maxConcurrency: 5,
@@ -8556,7 +8598,7 @@ class NexusOrchestrator {
8556
8598
  syncInterval: 5000
8557
8599
  },
8558
8600
  dashboard: {
8559
- enabled: false,
8601
+ enabled: true,
8560
8602
  port: 4747,
8561
8603
  host: "127.0.0.1"
8562
8604
  },
@@ -8584,12 +8626,19 @@ class NexusOrchestrator {
8584
8626
  learning: { ...defaults.learning, ...partial?.learning }
8585
8627
  };
8586
8628
  }
8629
+ notifyStateChange() {
8630
+ this.onStateChange?.();
8631
+ }
8587
8632
  async execute(request) {
8588
8633
  if (this.running) {
8589
8634
  throw new Error("Orchestrator is already running");
8590
8635
  }
8636
+ if (!this.ctx) {
8637
+ throw new Error("Orchestrator not initialized - call initialize(ctx) first");
8638
+ }
8591
8639
  this.running = true;
8592
8640
  const startTime = Date.now();
8641
+ this.notifyStateChange();
8593
8642
  try {
8594
8643
  this.dag = this.buildDAG(request.tasks);
8595
8644
  if (request.budget) {
@@ -8615,6 +8664,7 @@ class NexusOrchestrator {
8615
8664
  };
8616
8665
  } finally {
8617
8666
  this.running = false;
8667
+ this.notifyStateChange();
8618
8668
  }
8619
8669
  }
8620
8670
  buildDAG(tasks) {
@@ -8692,11 +8742,13 @@ class NexusOrchestrator {
8692
8742
  async executeDAG() {
8693
8743
  while (!this.dag.isComplete() && !this.paused) {
8694
8744
  const readyNodes = this.dag.getReadyNodes();
8745
+ const spawnPromises = [];
8695
8746
  for (const node of readyNodes) {
8696
8747
  if (this.agents.size < this.config.maxConcurrency) {
8697
- await this.spawnAndExecute(node);
8748
+ spawnPromises.push(this.spawnAndExecute(node));
8698
8749
  }
8699
8750
  }
8751
+ await Promise.all(spawnPromises);
8700
8752
  await this.sleep(this.config.schedulerInterval);
8701
8753
  }
8702
8754
  }
@@ -8710,86 +8762,150 @@ class NexusOrchestrator {
8710
8762
  });
8711
8763
  node.spawnedAgent = agent;
8712
8764
  node.status = "running";
8713
- this.monitorAgent(agent, node);
8765
+ node.task.assignedAgent = agent.id;
8766
+ this.notifyStateChange();
8767
+ await this.executeTask(agent, node);
8714
8768
  }
8715
- async monitorAgent(agent, node) {
8716
- const timeout = node.task.timeout || this.config.defaultTimeout;
8717
- setTimeout(() => {
8718
- if (node.status === "running") {
8719
- this.handleAgentTimeout(agent, node);
8720
- }
8721
- }, timeout);
8722
- }
8723
- handleAgentTimeout(agent, node) {
8724
- if (this.config.selfHealing.enabled) {
8725
- this.handleFailure(agent, node, new Error("Agent timed out"));
8726
- } else {
8769
+ async executeTask(agent, node) {
8770
+ if (!this.ctx || !agent.sessionID) {
8727
8771
  node.status = "failed";
8728
8772
  node.result = {
8729
8773
  success: false,
8730
- error: "Agent timed out",
8731
- duration: node.task.timeout || this.config.defaultTimeout,
8774
+ error: "No session available for agent",
8775
+ duration: 0,
8732
8776
  tokensUsed: 0,
8733
8777
  cost: 0
8734
8778
  };
8779
+ this.notifyStateChange();
8780
+ return;
8735
8781
  }
8782
+ const startTime = Date.now();
8783
+ const timeout = node.task.timeout || this.config.defaultTimeout;
8784
+ try {
8785
+ const rolePrompt = this.buildRolePrompt(node.task.requiredRole);
8786
+ const taskPrompt = `${rolePrompt}
8787
+
8788
+ ## Task
8789
+ ${node.task.name}
8790
+
8791
+ ${node.task.description}
8792
+
8793
+ ## Scope
8794
+ Files: ${node.task.files.include.join(", ")}`;
8795
+ await this.ctx.session.prompt({
8796
+ sessionID: agent.sessionID,
8797
+ text: taskPrompt
8798
+ });
8799
+ const waitPromise = this.ctx.session.wait({ sessionID: agent.sessionID });
8800
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Task timed out")), timeout));
8801
+ await Promise.race([waitPromise, timeoutPromise]);
8802
+ const messages = await this.ctx.session.context({ sessionID: agent.sessionID });
8803
+ const lastAssistantMsg = messages.filter((m) => m.role === "assistant").pop();
8804
+ const output = lastAssistantMsg?.content || "Task completed";
8805
+ const duration = Date.now() - startTime;
8806
+ const result = {
8807
+ success: true,
8808
+ output: typeof output === "string" ? output : JSON.stringify(output),
8809
+ duration,
8810
+ tokensUsed: 0,
8811
+ cost: this.estimateModelCost(agent.model.model)
8812
+ };
8813
+ this.dag.markComplete(node.id, result);
8814
+ agent.metrics.tasksCompleted++;
8815
+ agent.metrics.totalCost += result.cost;
8816
+ this.totalSpent += result.cost;
8817
+ this.costByAgent.set(agent.id, (this.costByAgent.get(agent.id) || 0) + result.cost);
8818
+ this.checkBudget();
8819
+ } catch (error) {
8820
+ const duration = Date.now() - startTime;
8821
+ const result = {
8822
+ success: false,
8823
+ error: error.message || "Task failed",
8824
+ duration,
8825
+ tokensUsed: 0,
8826
+ cost: 0
8827
+ };
8828
+ this.dag.markFailed(node.id, new Error(result.error));
8829
+ agent.metrics.tasksFailed++;
8830
+ if (this.config.selfHealing.enabled) {
8831
+ await this.handleFailure(agent, node, new Error(result.error));
8832
+ }
8833
+ } finally {
8834
+ agent.status = "idle";
8835
+ node.task.status = node.status === "completed" ? "completed" : "failed";
8836
+ this.notifyStateChange();
8837
+ }
8838
+ }
8839
+ buildRolePrompt(role) {
8840
+ const rolePrompts = {
8841
+ architect: "You are a software architect. Focus on system design, architecture patterns, and high-level technical decisions. Analyze requirements and propose structured solutions.",
8842
+ coder: "You are a senior software engineer. Write clean, efficient, well-documented code. Follow best practices and coding standards.",
8843
+ reviewer: "You are a code reviewer. Review code for correctness, security, performance, and maintainability. Provide constructive feedback.",
8844
+ tester: "You are a QA engineer. Write comprehensive tests, identify edge cases, and ensure code quality.",
8845
+ explorer: "You are a code explorer. Navigate and analyze codebases, understand architecture, and provide detailed reports.",
8846
+ documenter: "You are a technical writer. Create clear, comprehensive documentation for code and APIs."
8847
+ };
8848
+ return rolePrompts[role] || `You are a ${role}. Complete the assigned task professionally.`;
8736
8849
  }
8737
8850
  async handleFailure(agent, node, error) {
8738
8851
  const failureCount = agent.metrics.tasksFailed + 1;
8739
- agent.metrics.tasksFailed++;
8740
8852
  if (failureCount < this.config.selfHealing.maxRetries) {
8741
8853
  const delay = this.config.selfHealing.retryDelay * Math.pow(this.config.selfHealing.backoffMultiplier, failureCount - 1);
8742
- setTimeout(async () => {
8743
- await this.retryAgent(agent, node);
8744
- }, delay);
8745
- } else if (this.config.selfHealing.contextTransfer) {
8746
- await this.respawnAgent(agent, node);
8854
+ await this.sleep(delay);
8855
+ if (agent.sessionID) {
8856
+ node.status = "running";
8857
+ this.notifyStateChange();
8858
+ await this.executeTask(agent, node);
8859
+ }
8747
8860
  } else {
8748
8861
  node.status = "failed";
8749
8862
  node.result = {
8750
8863
  success: false,
8751
- error: error.message,
8864
+ error: `Max retries (${this.config.selfHealing.maxRetries}) exceeded: ${error.message}`,
8752
8865
  duration: 0,
8753
8866
  tokensUsed: 0,
8754
8867
  cost: 0
8755
8868
  };
8869
+ this.notifyStateChange();
8756
8870
  }
8757
8871
  }
8758
- async retryAgent(agent, node) {
8759
- agent.status = "working";
8760
- node.status = "running";
8761
- this.emit("agent:retry", { agent, node });
8762
- }
8763
- async respawnAgent(oldAgent, node) {
8764
- const newAgent = await this.spawnAgent({
8765
- role: oldAgent.role,
8766
- task: node.task,
8767
- model: oldAgent.model.model
8768
- });
8769
- if (this.config.selfHealing.contextTransfer) {
8770
- await this.transferContext(oldAgent, newAgent);
8771
- }
8772
- await this.terminateAgent(oldAgent.id);
8773
- node.spawnedAgent = newAgent;
8774
- node.status = "running";
8775
- this.monitorAgent(newAgent, node);
8776
- }
8777
- async transferContext(oldAgent, newAgent) {
8778
- this.emit("context:transfer", { from: oldAgent, to: newAgent });
8779
- }
8780
8872
  async spawnAgent(config) {
8873
+ if (!this.ctx) {
8874
+ throw new Error("Orchestrator not initialized");
8875
+ }
8781
8876
  const agentId = `agent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
8877
+ const displayName = `${config.role}-${agentId.slice(0, 8)}`;
8878
+ const session = await this.ctx.session.create({
8879
+ title: `[Nexus] ${displayName}`
8880
+ });
8881
+ const modelConfig = this.configManager.getModelForRole(config.role);
8882
+ const [provider, ...modelParts] = modelConfig.split("/");
8883
+ const modelName = modelParts.join("/");
8884
+ try {
8885
+ await this.ctx.session.switchAgent({
8886
+ sessionID: session.id,
8887
+ agent: config.role === "architect" ? "build" : config.role === "reviewer" ? "build" : "build"
8888
+ });
8889
+ } catch {}
8890
+ if (modelName) {
8891
+ try {
8892
+ await this.ctx.session.switchModel({
8893
+ sessionID: session.id,
8894
+ model: { providerID: provider, id: modelName }
8895
+ });
8896
+ } catch {}
8897
+ }
8782
8898
  const agent = {
8783
8899
  id: agentId,
8784
- name: `${config.role}-${agentId.slice(0, 8)}`,
8900
+ name: displayName,
8785
8901
  role: config.role,
8786
- status: "spawning",
8902
+ status: "idle",
8787
8903
  model: {
8788
- provider: "opencode",
8789
- model: config.model || "default",
8904
+ provider,
8905
+ model: modelName || "default",
8790
8906
  estimatedCost: 0,
8791
8907
  estimatedQuality: 0.5,
8792
- reasoning: "Default selection"
8908
+ reasoning: `Configured for ${config.role}`
8793
8909
  },
8794
8910
  spawnedAt: new Date,
8795
8911
  lastActivity: new Date,
@@ -8800,12 +8916,12 @@ class NexusOrchestrator {
8800
8916
  totalCost: 0,
8801
8917
  averageResponseTime: 0,
8802
8918
  errorRate: 0
8803
- }
8919
+ },
8920
+ sessionID: session.id
8804
8921
  };
8805
8922
  this.agents.set(agentId, agent);
8806
8923
  this.emit("agent:spawned", agent);
8807
- await this.sleep(this.config.agents.spawnDelay);
8808
- agent.status = "idle";
8924
+ this.notifyStateChange();
8809
8925
  return agent;
8810
8926
  }
8811
8927
  async terminateAgent(agentId) {
@@ -8814,53 +8930,34 @@ class NexusOrchestrator {
8814
8930
  agent.status = "terminated";
8815
8931
  this.agents.delete(agentId);
8816
8932
  this.emit("agent:terminated", agent);
8933
+ this.notifyStateChange();
8817
8934
  }
8818
8935
  }
8819
8936
  analyzeComplexity(task) {
8820
8937
  const fileCount = task.files.include.length;
8821
- const codeLines = this.estimateCodeLines(task);
8938
+ const codeLines = task.files.include.length * 50;
8822
8939
  const dependencyDepth = task.dependencies.length;
8823
- const domainKnowledge = this.estimateDomainKnowledge(task);
8824
- const riskLevel = this.estimateRiskLevel(task);
8825
- const overall = Math.min(100, fileCount * 10 + codeLines / 10 + dependencyDepth * 15 + domainKnowledge * 20 + (riskLevel === "high" ? 30 : riskLevel === "medium" ? 15 : 0));
8826
- return {
8827
- overall,
8828
- factors: {
8829
- fileCount,
8830
- codeLines,
8831
- dependencyDepth,
8832
- domainKnowledge,
8833
- riskLevel
8834
- }
8835
- };
8836
- }
8837
- estimateCodeLines(task) {
8838
- return task.files.include.length * 50;
8839
- }
8840
- estimateDomainKnowledge(task) {
8940
+ let domainKnowledge = 0;
8841
8941
  const keywords = ["security", "auth", "payment", "crypto", "database"];
8842
- let score = 0;
8843
8942
  for (const keyword of keywords) {
8844
8943
  if (task.description.toLowerCase().includes(keyword)) {
8845
- score += 20;
8944
+ domainKnowledge += 20;
8846
8945
  }
8847
8946
  }
8848
- return Math.min(100, score);
8849
- }
8850
- estimateRiskLevel(task) {
8947
+ domainKnowledge = Math.min(100, domainKnowledge);
8948
+ let riskLevel = "low";
8851
8949
  const highRiskKeywords = ["migration", "production", "security", "payment"];
8852
- const mediumRiskKeywords = ["refactor", "update", "modify"];
8853
8950
  for (const keyword of highRiskKeywords) {
8854
8951
  if (task.description.toLowerCase().includes(keyword)) {
8855
- return "high";
8856
- }
8857
- }
8858
- for (const keyword of mediumRiskKeywords) {
8859
- if (task.description.toLowerCase().includes(keyword)) {
8860
- return "medium";
8952
+ riskLevel = "high";
8953
+ break;
8861
8954
  }
8862
8955
  }
8863
- return "low";
8956
+ const overall = Math.min(100, fileCount * 10 + codeLines / 10 + dependencyDepth * 15 + domainKnowledge * 20 + (riskLevel === "high" ? 30 : riskLevel === "medium" ? 15 : 0));
8957
+ return {
8958
+ overall,
8959
+ factors: { fileCount, codeLines, dependencyDepth, domainKnowledge, riskLevel }
8960
+ };
8864
8961
  }
8865
8962
  selectModel(role, complexity) {
8866
8963
  const configModel = this.configManager.getModelForRole(role);
@@ -8915,6 +9012,7 @@ class NexusOrchestrator {
8915
9012
  const modelCost = this.costByModel.get(model) || 0;
8916
9013
  this.costByModel.set(model, modelCost + cost);
8917
9014
  this.checkBudget();
9015
+ this.notifyStateChange();
8918
9016
  }
8919
9017
  checkBudget() {
8920
9018
  const remaining = this.budget.maxTotalCost - this.totalSpent;
@@ -8943,21 +9041,10 @@ class NexusOrchestrator {
8943
9041
  this.subscribers.set(topic, handlers);
8944
9042
  return () => {
8945
9043
  const idx = handlers.indexOf(handler);
8946
- if (idx > -1) {
9044
+ if (idx > -1)
8947
9045
  handlers.splice(idx, 1);
8948
- }
8949
9046
  };
8950
9047
  }
8951
- send(agentId, message) {
8952
- const fullMessage = {
8953
- ...message,
8954
- id: `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
8955
- to: agentId,
8956
- timestamp: new Date
8957
- };
8958
- this.messageQueue.push(fullMessage);
8959
- this.emit("message:sent", fullMessage);
8960
- }
8961
9048
  setMemory(scope, key, value, author) {
8962
9049
  const entry = {
8963
9050
  id: `mem-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
@@ -8975,71 +9062,42 @@ class NexusOrchestrator {
8975
9062
  getMemory(scope, key) {
8976
9063
  return this.memory.get(`${scope}:${key}`);
8977
9064
  }
8978
- searchMemory(query, scope) {
8979
- const results = [];
8980
- this.memory.forEach((entry) => {
8981
- if (scope && entry.scope !== scope)
8982
- return;
8983
- if (entry.key.includes(query) || JSON.stringify(entry.value).includes(query)) {
8984
- results.push(entry);
8985
- }
8986
- });
8987
- return results;
8988
- }
8989
9065
  getStatus(detailed) {
8990
- const status = {
8991
- running: this.running,
8992
- paused: this.paused,
8993
- agents: this.agents.size,
8994
- tasks: this.tasks.size,
8995
- totalCost: this.totalSpent,
8996
- budgetRemaining: this.budget.maxTotalCost - this.totalSpent
8997
- };
8998
- if (detailed) {
8999
- return JSON.stringify({
9000
- ...status,
9001
- agentsByStatus: this.getAgentsByStatus(),
9002
- costByAgent: Object.fromEntries(this.costByAgent),
9003
- costByModel: Object.fromEntries(this.costByModel)
9004
- }, null, 2);
9005
- }
9006
- return JSON.stringify(status, null, 2);
9066
+ const state = this.getState();
9067
+ if (detailed)
9068
+ return JSON.stringify(state, null, 2);
9069
+ return JSON.stringify({
9070
+ running: state.running,
9071
+ paused: state.paused,
9072
+ agents: state.agents.length,
9073
+ tasks: state.tasks.length,
9074
+ totalCost: state.totalSpent,
9075
+ budgetRemaining: state.budgetRemaining
9076
+ }, null, 2);
9007
9077
  }
9008
9078
  listAgents(filter) {
9009
- const agents = Array.from(this.agents.values());
9010
- const filtered = filter ? agents.filter((a) => a.status === filter) : agents;
9079
+ const state = this.getState();
9080
+ const filtered = filter ? state.agents.filter((a) => a.status === filter) : state.agents;
9011
9081
  return JSON.stringify(filtered, null, 2);
9012
9082
  }
9013
9083
  getCostReport() {
9014
- const report = {
9015
- totalSpent: this.totalSpent,
9016
- budgetRemaining: this.budget.maxTotalCost - this.totalSpent,
9017
- byAgent: new Map,
9018
- byModel: new Map,
9019
- timeline: []
9020
- };
9021
- this.agents.forEach((agent) => {
9022
- report.byAgent.set(agent.id, {
9023
- agentId: agent.id,
9024
- agentName: agent.name,
9025
- totalCost: agent.metrics.totalCost,
9026
- tokenCount: agent.metrics.totalTokens,
9027
- taskCount: agent.metrics.tasksCompleted
9028
- });
9029
- });
9084
+ const state = this.getState();
9030
9085
  return JSON.stringify({
9031
- ...report,
9032
- byAgent: Object.fromEntries(report.byAgent),
9033
- byModel: Object.fromEntries(report.byModel)
9086
+ totalSpent: state.totalSpent,
9087
+ budgetRemaining: state.budgetRemaining,
9088
+ byAgent: Object.fromEntries(this.costByAgent),
9089
+ byModel: Object.fromEntries(this.costByModel)
9034
9090
  }, null, 2);
9035
9091
  }
9036
9092
  pause() {
9037
9093
  this.paused = true;
9038
9094
  this.emit("orchestrator:paused", {});
9095
+ this.notifyStateChange();
9039
9096
  }
9040
9097
  resume() {
9041
9098
  this.paused = false;
9042
9099
  this.emit("orchestrator:resumed", {});
9100
+ this.notifyStateChange();
9043
9101
  }
9044
9102
  shutdown() {
9045
9103
  this.agents.forEach((agent) => {
@@ -9048,25 +9106,18 @@ class NexusOrchestrator {
9048
9106
  this.agents.clear();
9049
9107
  this.running = false;
9050
9108
  this.emit("orchestrator:shutdown", {});
9109
+ this.notifyStateChange();
9051
9110
  }
9052
9111
  collectResults() {
9053
9112
  const results = [];
9054
9113
  if (this.dag) {
9055
9114
  this.dag.nodes.forEach((node) => {
9056
- if (node.result) {
9115
+ if (node.result)
9057
9116
  results.push(node.result);
9058
- }
9059
9117
  });
9060
9118
  }
9061
9119
  return results;
9062
9120
  }
9063
- getAgentsByStatus() {
9064
- const counts = {};
9065
- this.agents.forEach((agent) => {
9066
- counts[agent.status] = (counts[agent.status] || 0) + 1;
9067
- });
9068
- return counts;
9069
- }
9070
9121
  sleep(ms) {
9071
9122
  return new Promise((resolve) => setTimeout(resolve, ms));
9072
9123
  }
@@ -9076,9 +9127,8 @@ class NexusOrchestrator {
9076
9127
  this.eventHandlers.set(event, handlers);
9077
9128
  return () => {
9078
9129
  const idx = handlers.indexOf(handler);
9079
- if (idx > -1) {
9130
+ if (idx > -1)
9080
9131
  handlers.splice(idx, 1);
9081
- }
9082
9132
  };
9083
9133
  }
9084
9134
  emit(event, data) {
@@ -9090,22 +9140,21 @@ class NexusOrchestrator {
9090
9140
  const command = parts[1];
9091
9141
  switch (command) {
9092
9142
  case "status":
9093
- console.log(this.getStatus(true));
9094
- break;
9143
+ return this.getStatus(true);
9095
9144
  case "agents":
9096
- console.log(this.listAgents(parts[2]));
9097
- break;
9145
+ return this.listAgents(parts[2]);
9098
9146
  case "costs":
9099
- console.log(this.getCostReport());
9100
- break;
9147
+ return this.getCostReport();
9101
9148
  case "pause":
9102
9149
  this.pause();
9103
- break;
9150
+ return "Orchestrator paused";
9104
9151
  case "resume":
9105
9152
  this.resume();
9106
- break;
9153
+ return "Orchestrator resumed";
9154
+ case "dashboard":
9155
+ return JSON.stringify(this.getState(), null, 2);
9107
9156
  default:
9108
- console.log("Unknown command. Use /nexus help");
9157
+ return "Unknown command. Available: status, agents, costs, pause, resume, dashboard";
9109
9158
  }
9110
9159
  }
9111
9160
  }
@@ -9115,6 +9164,11 @@ var src_default = define({
9115
9164
  id: "nexus",
9116
9165
  async setup(ctx) {
9117
9166
  const orchestrator = new NexusOrchestrator;
9167
+ orchestrator.initialize(ctx, () => {
9168
+ const state = orchestrator.getState();
9169
+ ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(state))).catch(() => {});
9170
+ });
9171
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
9118
9172
  await ctx.tool.transform((editor) => {
9119
9173
  editor.namespace({
9120
9174
  name: "nexus",
@@ -9132,7 +9186,9 @@ var src_default = define({
9132
9186
  },
9133
9187
  execute: async (input) => {
9134
9188
  const { detailed } = input;
9135
- return { content: orchestrator.getStatus(detailed) };
9189
+ const status = orchestrator.getStatus(detailed);
9190
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
9191
+ return { content: status };
9136
9192
  }
9137
9193
  });
9138
9194
  editor.add({
@@ -9162,10 +9218,53 @@ var src_default = define({
9162
9218
  return { content: orchestrator.getCostReport() };
9163
9219
  }
9164
9220
  });
9221
+ editor.add({
9222
+ name: "dashboard",
9223
+ description: "Get full orchestrator state for dashboard display",
9224
+ input: {
9225
+ type: "object",
9226
+ properties: {},
9227
+ additionalProperties: false
9228
+ },
9229
+ execute: async () => {
9230
+ const state = orchestrator.getState();
9231
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(state)));
9232
+ return { content: JSON.stringify(state, null, 2) };
9233
+ }
9234
+ });
9235
+ editor.add({
9236
+ name: "spawn",
9237
+ description: "Spawn a sub-agent for a task",
9238
+ input: {
9239
+ type: "object",
9240
+ properties: {
9241
+ role: { type: "string", description: "Agent role (architect, coder, reviewer, tester, explorer, documenter)" },
9242
+ task: { type: "string", description: "Task description" },
9243
+ model: { type: "string", description: "Model override (optional)" }
9244
+ },
9245
+ required: ["role", "task"],
9246
+ additionalProperties: false
9247
+ },
9248
+ execute: async (input) => {
9249
+ const { role, task, model } = input;
9250
+ try {
9251
+ const agent = await orchestrator.spawnAgent({ role, model });
9252
+ await orchestrator.ctx.session.prompt({
9253
+ sessionID: agent.sessionID,
9254
+ text: task
9255
+ });
9256
+ await ctx.storage.set("orchestrator-state", JSON.parse(JSON.stringify(orchestrator.getState())));
9257
+ return { content: `Spawned ${role} agent (${agent.id}) on session ${agent.sessionID}. Task sent.` };
9258
+ } catch (error) {
9259
+ return { content: `Failed to spawn agent: ${error.message}` };
9260
+ }
9261
+ }
9262
+ });
9165
9263
  });
9166
9264
  await ctx.session.hook("prompt", (event) => {
9167
- if (event.prompt.text.includes("/nexus")) {
9168
- orchestrator.handleCommand(event.prompt.text);
9265
+ if (event.prompt.text.startsWith("/nexus")) {
9266
+ const result = orchestrator.handleCommand(event.prompt.text);
9267
+ event.metadata = { ...event.metadata, nexusResult: result };
9169
9268
  }
9170
9269
  });
9171
9270
  return () => {
package/dist/tui.js CHANGED
@@ -62,9 +62,6 @@ function createContext(defaultValue) {
62
62
  defaultValue
63
63
  };
64
64
  }
65
- function useContext(context) {
66
- return Owner && Owner.context && Owner.context[context.id] !== undefined ? Owner.context[context.id] : context.defaultValue;
67
- }
68
65
  function children(fn) {
69
66
  const memo = createMemo(() => resolveChildren(fn()));
70
67
  memo.toArray = () => {
@@ -109,12 +106,6 @@ var SuspenseContext = createContext();
109
106
 
110
107
  // node_modules/@opencode/plugin/dist/tui/solid.js
111
108
  var PluginContext = createContext();
112
- function usePlugin() {
113
- const context = useContext(PluginContext);
114
- if (!context)
115
- throw new Error("PluginContextProvider is missing");
116
- return context;
117
- }
118
109
  // src/config.ts
119
110
  var DEFAULT_CONFIG = {
120
111
  models: {
@@ -34140,113 +34131,242 @@ function jsx(type, props = {}) {
34140
34131
  function jsxDEV(type, props = {}) {
34141
34132
  return jsx(type, props);
34142
34133
  }
34134
+ function Fragment(props) {
34135
+ return props.children ?? null;
34136
+ }
34143
34137
  // src/tui.tsx
34144
- function NexusKeymapSlot() {
34145
- const context = usePlugin();
34146
- const configManager = new NexusConfigManager;
34147
- const handleModelSelect = async (role) => {
34148
- if (!configManager.getRoles().includes(role)) {
34149
- context.ui.toast.show({
34150
- title: "Nexus",
34151
- message: `Unknown role: ${role}. Valid: ${configManager.getRoles().join(", ")}`,
34152
- variant: "error"
34138
+ var tui_default = define({
34139
+ id: "nexus.cli",
34140
+ setup(context) {
34141
+ const configManager = new NexusConfigManager;
34142
+ const handleModelSelect = async (role) => {
34143
+ if (!configManager.getRoles().includes(role)) {
34144
+ context.ui.toast.show({
34145
+ title: "Nexus",
34146
+ message: `Unknown role: ${role}. Valid: ${configManager.getRoles().join(", ")}`,
34147
+ variant: "error"
34148
+ });
34149
+ return;
34150
+ }
34151
+ const location = context.location ?? context.data.location.default();
34152
+ await context.data.location.model.sync(location);
34153
+ const availableModels = context.data.location.model.list(location) ?? [];
34154
+ const options = availableModels.map((m) => ({
34155
+ title: m.name || m.id,
34156
+ value: `${m.providerID}/${m.id}`,
34157
+ description: m.providerID
34158
+ }));
34159
+ options.push({
34160
+ title: "Use default",
34161
+ value: "",
34162
+ description: "Reset to default model"
34153
34163
  });
34154
- return;
34155
- }
34156
- const location = context.location ?? context.data.location.default();
34157
- await context.data.location.model.sync(location);
34158
- const availableModels = context.data.location.model.list(location) ?? [];
34159
- const options = availableModels.map((m) => ({
34160
- title: m.name || m.id,
34161
- value: `${m.providerID}/${m.id}`,
34162
- description: m.providerID
34163
- }));
34164
- options.push({
34165
- title: "Use default",
34166
- value: "",
34167
- description: "Reset to default model"
34168
- });
34169
- const current = configManager.getModelForRole(role);
34170
- const selected = await context.ui.dialog.select({
34171
- title: `${configManager.getRoleDisplayName(role)} Model`,
34172
- current: current || "",
34173
- options
34174
- });
34175
- if (selected !== null && selected !== undefined) {
34176
- configManager.setModel(role, selected);
34177
- context.ui.toast.show({
34178
- title: "Nexus",
34179
- message: `${role} → ${selected || "default"}`,
34180
- variant: "success"
34164
+ const current = configManager.getModelForRole(role);
34165
+ const selected = await context.ui.dialog.select({
34166
+ title: `${configManager.getRoleDisplayName(role)} Model`,
34167
+ current: current || "",
34168
+ options
34181
34169
  });
34182
- }
34183
- };
34184
- const handleConfigDialog = async () => {
34185
- const config = configManager.getConfig();
34186
- const budgetStr = await context.ui.dialog.prompt({
34187
- title: "Max Total Budget ($)",
34188
- placeholder: String(config.budget.maxTotalCost)
34189
- });
34190
- if (budgetStr !== null && budgetStr !== undefined) {
34191
- const budget = parseFloat(budgetStr);
34192
- if (!isNaN(budget) && budget > 0) {
34193
- configManager.updateStorageConfig({
34194
- budget: { ...config.budget, maxTotalCost: budget }
34195
- });
34170
+ if (selected !== null && selected !== undefined) {
34171
+ configManager.setModel(role, selected);
34196
34172
  context.ui.toast.show({
34197
34173
  title: "Nexus",
34198
- message: `Budget updated: $${budget}`,
34174
+ message: `${role} → ${selected || "default"}`,
34199
34175
  variant: "success"
34200
34176
  });
34201
34177
  }
34202
- }
34203
- };
34204
- const handleFullConfig = async () => {
34205
- for (const role of configManager.getRoles()) {
34206
- await handleModelSelect(role);
34207
- }
34208
- await handleConfigDialog();
34209
- context.ui.toast.show({
34210
- title: "Nexus",
34211
- message: "Configuration complete!",
34212
- variant: "success"
34213
- });
34214
- };
34215
- context.keymap.layer(() => ({
34216
- mode: "global",
34217
- priority: 10,
34218
- commands: [
34219
- {
34220
- id: "nexus",
34221
- title: "Nexus Configuration",
34222
- group: "Nexus",
34223
- bind: "ctrl+n",
34224
- palette: true,
34225
- slash: { name: "nexus", aliases: [], arguments: true },
34226
- enabled: () => true,
34227
- suggested: true,
34228
- run: async (input) => {
34229
- if (input) {
34230
- const parts = input.split(" ");
34231
- const cmd = parts[0];
34232
- switch (cmd) {
34233
- case "config":
34234
- case "c":
34178
+ };
34179
+ const handleConfigDialog = async () => {
34180
+ const config = configManager.getConfig();
34181
+ const budgetStr = await context.ui.dialog.prompt({
34182
+ title: "Max Total Budget ($)",
34183
+ placeholder: String(config.budget.maxTotalCost)
34184
+ });
34185
+ if (budgetStr !== null && budgetStr !== undefined) {
34186
+ const budget = parseFloat(budgetStr);
34187
+ if (!isNaN(budget) && budget > 0) {
34188
+ configManager.updateStorageConfig({
34189
+ budget: { ...config.budget, maxTotalCost: budget }
34190
+ });
34191
+ context.ui.toast.show({
34192
+ title: "Nexus",
34193
+ message: `Budget updated: $${budget}`,
34194
+ variant: "success"
34195
+ });
34196
+ }
34197
+ }
34198
+ };
34199
+ const handleFullConfig = async () => {
34200
+ for (const role of configManager.getRoles()) {
34201
+ await handleModelSelect(role);
34202
+ }
34203
+ await handleConfigDialog();
34204
+ context.ui.toast.show({
34205
+ title: "Nexus",
34206
+ message: "Configuration complete!",
34207
+ variant: "success"
34208
+ });
34209
+ };
34210
+ const handleDashboard = async () => {
34211
+ const config = configManager.getConfig();
34212
+ const lines = [
34213
+ "⚡ Nexus Dashboard",
34214
+ "═══════════════════════════════════",
34215
+ "",
34216
+ "\uD83E\uDD16 Agent Models:"
34217
+ ];
34218
+ for (const role of configManager.getRoles()) {
34219
+ const model = config.models[role] || "(not set)";
34220
+ const displayName = configManager.getRoleDisplayName(role);
34221
+ lines.push(` ${displayName}: ${model}`);
34222
+ }
34223
+ lines.push("");
34224
+ lines.push("\uD83D\uDCB0 Budget:");
34225
+ lines.push(` Max Total: $${config.budget.maxTotalCost}`);
34226
+ lines.push(` Max Per Task: $${config.budget.maxCostPerTask}`);
34227
+ lines.push(` Alert Threshold: ${config.budget.alertThreshold * 100}%`);
34228
+ lines.push("");
34229
+ lines.push("\uD83D\uDEE1️ Self-Healing:");
34230
+ lines.push(` Enabled: ${config.selfHealing.enabled ? "✅" : "❌"}`);
34231
+ lines.push(` Max Retries: ${config.selfHealing.maxRetries}`);
34232
+ lines.push("");
34233
+ lines.push("\uD83D\uDCA1 Commands:");
34234
+ lines.push(" /nexus status - Show config summary");
34235
+ lines.push(" /nexus config - Configure models & budget");
34236
+ lines.push(" /nexus model - Select model for role");
34237
+ lines.push(" /nexus reset - Reset to defaults");
34238
+ lines.push("");
34239
+ lines.push("\uD83D\uDD27 Tools (use in agent prompt):");
34240
+ lines.push(" nexus.status - Orchestrator status");
34241
+ lines.push(" nexus.agents - List spawned agents");
34242
+ lines.push(" nexus.costs - Cost report");
34243
+ lines.push(" nexus.spawn - Spawn a sub-agent");
34244
+ context.ui.toast.show({
34245
+ title: "Nexus Dashboard",
34246
+ message: lines.join(`
34247
+ `),
34248
+ variant: "info",
34249
+ duration: 15000
34250
+ });
34251
+ };
34252
+ context.ui.slot({
34253
+ append: "app",
34254
+ render: () => {
34255
+ context.keymap.layer(() => ({
34256
+ mode: "global",
34257
+ priority: 10,
34258
+ commands: [
34259
+ {
34260
+ id: "nexus",
34261
+ title: "Nexus Configuration",
34262
+ group: "Nexus",
34263
+ bind: "ctrl+n",
34264
+ palette: true,
34265
+ slash: { name: "nexus", aliases: [], arguments: true },
34266
+ enabled: () => true,
34267
+ suggested: true,
34268
+ run: async (input) => {
34269
+ if (input) {
34270
+ const parts = input.split(" ");
34271
+ const cmd = parts[0];
34272
+ switch (cmd) {
34273
+ case "config":
34274
+ case "c":
34275
+ await handleFullConfig();
34276
+ break;
34277
+ case "status":
34278
+ case "s":
34279
+ context.ui.toast.show({
34280
+ title: "Nexus Status",
34281
+ message: configManager.getSummary(),
34282
+ variant: "info",
34283
+ duration: 8000
34284
+ });
34285
+ break;
34286
+ case "dashboard":
34287
+ case "d":
34288
+ await handleDashboard();
34289
+ break;
34290
+ case "model":
34291
+ case "m":
34292
+ if (parts[1]) {
34293
+ await handleModelSelect(parts[1]);
34294
+ } else {
34295
+ const role = await context.ui.dialog.select({
34296
+ title: "Select Agent Role",
34297
+ options: configManager.getRoles().map((r) => ({
34298
+ title: configManager.getRoleDisplayName(r),
34299
+ value: r,
34300
+ description: `Current: ${configManager.getModelForRole(r)}`
34301
+ }))
34302
+ });
34303
+ if (role) {
34304
+ await handleModelSelect(role);
34305
+ }
34306
+ }
34307
+ break;
34308
+ case "reset":
34309
+ const confirmed = await context.ui.dialog.confirm({
34310
+ title: "Reset Configuration",
34311
+ message: "Reset all Nexus settings to defaults?",
34312
+ label: { confirm: "Reset", cancel: "Cancel" }
34313
+ });
34314
+ if (confirmed) {
34315
+ configManager.resetToDefaults();
34316
+ context.ui.toast.show({
34317
+ title: "Nexus",
34318
+ message: "Configuration reset to defaults",
34319
+ variant: "success"
34320
+ });
34321
+ }
34322
+ break;
34323
+ default:
34324
+ context.ui.toast.show({
34325
+ title: "Nexus",
34326
+ message: "Commands: config, status, dashboard, model <role>, reset",
34327
+ variant: "info"
34328
+ });
34329
+ }
34330
+ } else {
34331
+ await handleFullConfig();
34332
+ }
34333
+ }
34334
+ },
34335
+ {
34336
+ id: "nexus.config",
34337
+ title: "Nexus Configuration",
34338
+ group: "Nexus",
34339
+ palette: true,
34340
+ slash: { name: "nexus-config", aliases: ["nc"], arguments: true },
34341
+ enabled: () => true,
34342
+ suggested: true,
34343
+ run: async () => {
34235
34344
  await handleFullConfig();
34236
- break;
34237
- case "status":
34238
- case "s":
34239
- context.ui.toast.show({
34240
- title: "Nexus Status",
34241
- message: configManager.getSummary(),
34242
- variant: "info",
34243
- duration: 8000
34244
- });
34245
- break;
34246
- case "model":
34247
- case "m":
34248
- if (parts[1]) {
34249
- await handleModelSelect(parts[1]);
34345
+ }
34346
+ },
34347
+ {
34348
+ id: "nexus.dashboard",
34349
+ title: "Nexus Dashboard",
34350
+ group: "Nexus",
34351
+ palette: true,
34352
+ slash: { name: "nexus-dashboard", aliases: ["nd"], arguments: true },
34353
+ enabled: () => true,
34354
+ suggested: true,
34355
+ run: async () => {
34356
+ await handleDashboard();
34357
+ }
34358
+ },
34359
+ {
34360
+ id: "nexus.model",
34361
+ title: "Select Agent Model",
34362
+ group: "Nexus",
34363
+ palette: true,
34364
+ slash: { name: "nexus-model", aliases: ["nm"], arguments: true },
34365
+ enabled: () => true,
34366
+ suggested: true,
34367
+ run: async (input) => {
34368
+ if (input) {
34369
+ await handleModelSelect(input);
34250
34370
  } else {
34251
34371
  const role = await context.ui.dialog.select({
34252
34372
  title: "Select Agent Role",
@@ -34260,8 +34380,33 @@ function NexusKeymapSlot() {
34260
34380
  await handleModelSelect(role);
34261
34381
  }
34262
34382
  }
34263
- break;
34264
- case "reset":
34383
+ }
34384
+ },
34385
+ {
34386
+ id: "nexus.status",
34387
+ title: "Nexus Status",
34388
+ group: "Nexus",
34389
+ palette: true,
34390
+ slash: { name: "nexus-status", aliases: ["ns"], arguments: true },
34391
+ enabled: () => true,
34392
+ suggested: true,
34393
+ run: async () => {
34394
+ context.ui.toast.show({
34395
+ title: "Nexus Status",
34396
+ message: configManager.getSummary(),
34397
+ variant: "info",
34398
+ duration: 8000
34399
+ });
34400
+ }
34401
+ },
34402
+ {
34403
+ id: "nexus.reset",
34404
+ title: "Reset Nexus Config",
34405
+ group: "Nexus",
34406
+ palette: true,
34407
+ slash: { name: "nexus-reset", aliases: [], arguments: true },
34408
+ enabled: () => true,
34409
+ run: async () => {
34265
34410
  const confirmed = await context.ui.dialog.confirm({
34266
34411
  title: "Reset Configuration",
34267
34412
  message: "Reset all Nexus settings to defaults?",
@@ -34275,108 +34420,13 @@ function NexusKeymapSlot() {
34275
34420
  variant: "success"
34276
34421
  });
34277
34422
  }
34278
- break;
34279
- default:
34280
- context.ui.toast.show({
34281
- title: "Nexus",
34282
- message: "Commands: config, status, model <role>, reset",
34283
- variant: "info"
34284
- });
34285
- }
34286
- } else {
34287
- await handleFullConfig();
34288
- }
34289
- }
34290
- },
34291
- {
34292
- id: "nexus.config",
34293
- title: "Nexus Configuration",
34294
- group: "Nexus",
34295
- palette: true,
34296
- slash: { name: "nexus-config", aliases: ["nc"], arguments: true },
34297
- enabled: () => true,
34298
- suggested: true,
34299
- run: async () => {
34300
- await handleFullConfig();
34301
- }
34302
- },
34303
- {
34304
- id: "nexus.model",
34305
- title: "Select Agent Model",
34306
- group: "Nexus",
34307
- palette: true,
34308
- slash: { name: "nexus-model", aliases: ["nm"], arguments: true },
34309
- enabled: () => true,
34310
- suggested: true,
34311
- run: async (input) => {
34312
- if (input) {
34313
- await handleModelSelect(input);
34314
- } else {
34315
- const role = await context.ui.dialog.select({
34316
- title: "Select Agent Role",
34317
- options: configManager.getRoles().map((r) => ({
34318
- title: configManager.getRoleDisplayName(r),
34319
- value: r,
34320
- description: `Current: ${configManager.getModelForRole(r)}`
34321
- }))
34322
- });
34323
- if (role) {
34324
- await handleModelSelect(role);
34423
+ }
34325
34424
  }
34326
- }
34327
- }
34328
- },
34329
- {
34330
- id: "nexus.status",
34331
- title: "Nexus Status",
34332
- group: "Nexus",
34333
- palette: true,
34334
- slash: { name: "nexus-status", aliases: ["ns"], arguments: true },
34335
- enabled: () => true,
34336
- suggested: true,
34337
- run: async () => {
34338
- context.ui.toast.show({
34339
- title: "Nexus Status",
34340
- message: configManager.getSummary(),
34341
- variant: "info",
34342
- duration: 8000
34343
- });
34344
- }
34345
- },
34346
- {
34347
- id: "nexus.reset",
34348
- title: "Reset Nexus Config",
34349
- group: "Nexus",
34350
- palette: true,
34351
- slash: { name: "nexus-reset", aliases: [], arguments: true },
34352
- enabled: () => true,
34353
- run: async () => {
34354
- const confirmed = await context.ui.dialog.confirm({
34355
- title: "Reset Configuration",
34356
- message: "Reset all Nexus settings to defaults?",
34357
- label: { confirm: "Reset", cancel: "Cancel" }
34358
- });
34359
- if (confirmed) {
34360
- configManager.resetToDefaults();
34361
- context.ui.toast.show({
34362
- title: "Nexus",
34363
- message: "Configuration reset to defaults",
34364
- variant: "success"
34365
- });
34366
- }
34367
- }
34425
+ ],
34426
+ bindings: ["nexus"]
34427
+ }));
34428
+ return /* @__PURE__ */ jsxDEV(Fragment, {}, undefined, false, undefined, this);
34368
34429
  }
34369
- ],
34370
- bindings: ["nexus"]
34371
- }));
34372
- return null;
34373
- }
34374
- var tui_default = define({
34375
- id: "nexus.cli",
34376
- setup(context) {
34377
- context.ui.slot({
34378
- append: "app",
34379
- render: () => /* @__PURE__ */ jsxDEV(NexusKeymapSlot, {}, undefined, false, undefined, this)
34380
34430
  });
34381
34431
  context.ui.toast.show({
34382
34432
  title: "⚡ Nexus Loaded",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serkanalgur/opencode-nexus",
3
- "version": "1.0.3",
3
+ "version": "1.0.7",
4
4
  "description": "Adaptive Multi-Agent Orchestration with Cost Intelligence for OpenCode V2",
5
5
  "keywords": [
6
6
  "opencode",